Unfortunately parse doesn’t have methods which accept a symbol.
It appears we can do this though:
julia> Base.@assume_effects foldable bar(::Type{T}) where T = parse(T, "3")
bar (generic function with 1 method)
julia> @btime bar(Int)
1.000 ns (0 allocations: 0 bytes)
3
This works for this function, but when embedding string operations into impure functions it might not be so easy.
I presume the inability to const-prop strings isn’t a fundamental limitation, but is instead just an implementation detail of the compiler as currently implemented?
Constant propagation is an implementation detail of the compiler. To encourage the compiler to perform constant propagation on a function, regardless of its purity, you can use the syntax Base.@constprop :aggressive f(...) = . The Base.@assume_effects annotation does a slightly different thing (constant-folding, a.k.a. compiler-time evaluation) and should only be used on impure functions.
julia> Base.@constprop :aggressive bar(::Type{T}) where T = parse(T, "3")
bar (generic function with 1 method)
julia> @btime bar(Int)
16.884 ns (0 allocations: 0 bytes)
3
You mean foldable should only be used on pure functions, yes?
Constant prop is a different thing from constant folding (from the compiler optimization point of view) so that’s expected. If you really need to optimize away a call to a complex function, assume_effects based constand folding is the last resort for it.
But yes, it should only be used for “pure” function. Read carefully the documentation of assume_effects for the details.
I seem to run into this problem frequently (and should probably learn more about the recent improved effect analysis) when writing higher precision algorithms. But it would be really convenient if a structure like.
function foo(x::T) where T
P = ("1.23", "1.24", "1.2345")
return evalpoly(x, parse.(T, P))
end
# or storing big floats as constants
const P2 = (big"1.23", big"1.24", big"1.2345")
function foo2(x::T) where T
return evalpoly(x, T.(P2))
end
could just work. A recent similar discussion was at here and I posted a related question here.
Generally, with explicit coefficients like this the algorithm isn’t arbitrary precision so the precision of BigFloats can be fixed. But ideally, we would be able to write algorithms for fixed higher precisions (Float128, Float256, Double64) that users could also potentially use any external package and not experience all these allocations.
It looks like though the external packages could also handle this conversion better. I must admit most of this issue is my general aversion to specifically loading these packages. Or perhaps a general desire for a julia native Float128 or Int256 type.
Considering that it’s impractical to avoid BigInt and BigFloat, and it’s impossible (?) to make them constant-fold, probably the better approach for you will be to use a @generated function. See definition and performance of foo6, which matches the “hand-crafted” foo5:
julia> using DoubleFloats, Quadmath, BenchmarkTools
Ts = (Float64, Double64, Float128);
julia> function foo(x::T) where T
P = ("1.23", "1.24", "1.2345")
return evalpoly(x, parse.(T, P))
end
const P2 = (big"1.23", big"1.24", big"1.2345")
function foo2(x::T) where T
return evalpoly(x, T.(P2))
end
Base.@assume_effects foldable foo3(x::T) where T = let P = ("1.23", "1.24", "1.2345"); evalpoly(x, parse.(T, P)) end
Base.@assume_effects foldable foo4(x::T) where T = evalpoly(x, T.(P2))
foreach(Ts) do T; eval(:(let P = map($T, (big"1.23", big"1.24", big"1.2345")); global foo5(x::$T) = evalpoly(x, P) end)) end
@generated foo6(x::T) where T = let P = map(T, (big"1.23", big"1.24", big"1.2345")); :(evalpoly(x, $P)) end
foos = (foo, foo2, foo3, foo4, foo5, foo6);
julia> foreach(foos) do f; println(f, ":"); foreach(Ts) do T; eval(:( @btime $f($T($(Expr(:$, 1.2)))) )) end end
foo:
1.310 μs (0 allocations: 0 bytes)
1.590 μs (15 allocations: 641 bytes)
380.788 ns (0 allocations: 0 bytes)
foo2:
107.309 ns (2 allocations: 80 bytes)
606.250 ns (8 allocations: 392 bytes)
575.824 ns (14 allocations: 656 bytes)
foo3:
1.310 μs (0 allocations: 0 bytes)
1.580 μs (15 allocations: 641 bytes)
382.266 ns (0 allocations: 0 bytes)
foo4:
107.459 ns (2 allocations: 80 bytes)
609.659 ns (8 allocations: 392 bytes)
577.473 ns (14 allocations: 656 bytes)
foo5:
2.000 ns (0 allocations: 0 bytes)
17.000 ns (0 allocations: 0 bytes)
79.545 ns (0 allocations: 0 bytes)
foo6:
2.000 ns (0 allocations: 0 bytes)
17.034 ns (0 allocations: 0 bytes)
79.442 ns (0 allocations: 0 bytes)
julia> all(Ts) do T; all(broadcast((f,x)->f(T(x)), foos, 1.2)) do x; x≡first(foos)(T(1.2)) end end # equality check
true