@oschulz and I have been talking about a deterministic RNG, with the main application being to generate an arbitrary point in the support of a given distribution. Hereβs our current implementation:
export FixedRNG
struct FixedRNG <: AbstractRNG end
Base.rand(::FixedRNG) = one(Float64) / 2
Random.randn(::FixedRNG) = zero(Float64)
Random.randexp(::FixedRNG) = one(Float64)
Base.rand(::FixedRNG, ::Type{T}) where {T<:Real} = one(T) / 2
Random.randn(::FixedRNG, ::Type{T}) where {T<:Real} = zero(T)
Random.randexp(::FixedRNG, ::Type{T}) where {T<:Real} = one(T)
# We need concrete type parameters to avoid ambiguity for these cases
for T in [Float16, Float32, Float64]
@eval begin
Base.rand(::FixedRNG, ::Type{$T}) = one($T) / 2
Random.randn(::FixedRNG, ::Type{$T}) = zero($T)
Random.randexp(::FixedRNG, ::Type{$T}) = one($T)
end
end
In a quick test of this, we were very impressed with the constant propagation:
julia> @code_typed rand(FixedRNG(), TDist(Ο))
CodeInfo(
1 β nothing::Nothing
βββ return 0.0
) => Float64
But such great results seem specific to irrationals. Hereβs the same call with a Float64
instead:
Click to expand
julia> @code_typed rand(FixedRNG(), Dists.TDist(3.1))
CodeInfo(
1 ββ %1 = Base.getfield(d, :Ξ½)::Float64
β %2 = Base.ne_float(%1, %1)::Bool
β %3 = Base.not_int(%2)::Bool
β %4 = Base.sub_float(%1, %1)::Float64
β %5 = Base.eq_float(%4, 0.0)::Bool
β %6 = Base.and_int(%5, true)::Bool
β %7 = Base.and_int(%6, true)::Bool
β %8 = Base.not_int(%7)::Bool
β %9 = Base.and_int(%3, %8)::Bool
ββββ goto #3 if not %9
2 ββ goto #26
3 ββ %12 = Base.getfield(d, :Ξ½)::Float64
ββββ goto #8 if not true
4 ββ %14 = Base.lt_float(0.0, %12)::Bool
ββββ goto #6 if not %14
5 ββ goto #7
6 ββ %17 = invoke Distributions.DomainError(%12::Any, "Chisq: the condition Ξ½ > zero(Ξ½) is not satisfied."::Any)::DomainError
β Distributions.throw(%17)::Union{}
ββββ unreachable
7 ββ nothing::Nothing
8 ββ Distributions.nothing::Nothing
ββββ goto #9
9 ββ goto #10
10 β goto #11
11 β %25 = Base.div_float(%12, 2.0)::Float64
ββββ goto #18 if not true
12 β %27 = Base.lt_float(0.0, %25)::Bool
ββββ goto #16 if not %27
13 β %29 = Base.lt_float(0.0, 2.0)::Bool
ββββ goto #15 if not %29
14 β goto #17
15 β %32 = invoke Distributions.DomainError(2.0::Any, "Gamma: the condition ΞΈ > zero(ΞΈ) is not satisfied."::Any)::DomainError
β Distributions.throw(%32)::Union{}
ββββ unreachable
16 β %35 = invoke Distributions.DomainError(%25::Any, "Gamma: the condition Ξ± > zero(Ξ±) is not satisfied."::Any)::DomainError
β Distributions.throw(%35)::Union{}
ββββ unreachable
17 β nothing::Nothing
18 β Distributions.nothing::Nothing
ββββ goto #19
19 β %41 = %new(Distributions.Gamma{Float64}, %25, 2.0)::Distributions.Gamma{Float64}
ββββ goto #20
20 β goto #21
21 β %44 = invoke Distributions.rand(rng::FixedRNG, %41::Distributions.Gamma{Float64})::Float64
ββββ goto #22
22 β %46 = Base.getfield(d, :Ξ½)::Float64
β %47 = Base.div_float(%44, %46)::Float64
β %48 = Base.lt_float(%47, 0.0)::Bool
ββββ goto #24 if not %48
23 β invoke Base.Math.throw_complex_domainerror(:sqrt::Symbol, %47::Float64)::Union{}
ββββ unreachable
24 β %52 = Base.Math.sqrt_llvm(%47)::Float64
ββββ goto #25
25 β nothing::Nothing
26 β %55 = Ο (#2 => false, #25 => true)::Bool
β %56 = Ο (#2 => true, #25 => false)::Bool
β %57 = Ο (#2 => 1, #25 => %52)::Union{Float64, Int64}
ββββ goto #28 if not %55
27 β %59 = Ο (%57, Float64)
β %60 = Base.div_float(0.0, %59)::Float64
ββββ goto #31
28 β goto #30 if not %56
29 β %63 = Ο (%57, Int64)
β %64 = Base.sitofp(Float64, %63)::Float64
β %65 = Base.div_float(0.0, %64)::Float64
ββββ goto #31
30 β Core.throw(ErrorException("fatal error in type inference (type bound)"))::Union{}
ββββ unreachable
31 β %69 = Ο (#27 => %60, #29 => %65)::Float64
ββββ return %69
) => Float64
After typing this, I thought to try Static.StaticFloat64
s, which again are almost magical (cc @Zach_Christensen):
julia> @code_typed rand(FixedRNG(), TDist(static(3.1)))
CodeInfo(
1 β nothing::Nothing
βββ return 0.0
) => Float64
Can the Float64
call be improved, or is this a case where performance requires type-level values?
EDIT: Here are some performance results, varying between great and pretty bad:
julia> for j in 2:-1:-4
Ξ½ = exp10(j)
result = rand(FixedRNG(), StudentT(Ξ½))
println("rand(FixedRNG(), StudentT($Ξ½)) == $result")
@btime rand(FixedRNG(), StudentT(Ξ½)) setup=(Ξ½=$Ξ½)
println()
end
rand(FixedRNG(), StudentT(100.0)) == 0.0
1.409 ns (0 allocations: 0 bytes)
rand(FixedRNG(), StudentT(10.0)) == 0.0
1.408 ns (0 allocations: 0 bytes)
rand(FixedRNG(), StudentT(1.0)) == 0.0
43.211 ns (0 allocations: 0 bytes)
rand(FixedRNG(), StudentT(0.1)) == 0.0
42.997 ns (0 allocations: 0 bytes)
rand(FixedRNG(), StudentT(0.01)) == 0.0
42.757 ns (0 allocations: 0 bytes)
rand(FixedRNG(), StudentT(0.001)) == NaN
32.614 ns (0 allocations: 0 bytes)
rand(FixedRNG(), StudentT(0.0001)) == NaN
32.609 ns (0 allocations: 0 bytes)