Deep in one of my projects I have a function that slightly changes behavior depending on a boolean keyword. It seems if the keyword is Val{Bool}
there are no allocations but if it is Bool
, then it allocates. Here is a contrived minimal example:
using BenchmarkTools
""" Some work object - it seems the example actually needs something as complicated """
struct Object{VecType<:AbstractVector{<:Unsigned}, MatType<:AbstractMatrix{<:Unsigned}}
v::VecType
m::MatType
end
""" Work function that happens to slightly change behavior depending on Val keyword """
function work_val(obj::Object; extrawork::Val{B}=Val(true)) where B
r,c = size(obj.m)
@inbounds for i in 1:r
@inbounds for j in i+1:r
for k in 1:c obj.m[i,k] ⊻= obj.m[j,k] end
if B
obj.v[i] ⊻= obj.m[i,j]
end
end
end
end
"""A "public interface" function that uses a Bool instead of Val{Bool} """
function work_bool_to_val(obj::Object; extrawork::Bool=true)
work_val(obj; extrawork=Val(extrawork))
end
n = 10
obj = Object(rand(UInt,n),rand(UInt,n,n))
The one that uses Val
does not allocate.
@benchmark work_val($obj)
BenchmarkTools.Trial: 10000 samples with 244 evaluations.
Range (min … max): 310.254 ns … 753.303 ns ┊ GC (min … max): 0.00% … 0.00%
Memory estimate: 0 bytes, allocs estimate: 0.
But the one that uses a Bool
and then puts it inside of a Val
does allocate even though it simply calls a non-allocating inner function.
@benchmark work_bool_to_val($obj)
BenchmarkTools.Trial: 10000 samples with 160 evaluations.
Range (min … max): 668.881 ns … 15.615 μs ┊ GC (min … max): 0.00% … 93.26%
Memory estimate: 32 bytes, allocs estimate: 1.
If I simply rewrite work_val
to directly use Bool
then things work fine. However, in the real case where I see this problem, such a rewrite is not possible. Thus my question is If I can not modify work_val
, what can I do in order to make work_bool_to_val
not allocate?.
This is not a question about refactoring and rethinking the structure of a code base, rather a very targeted question about why a Bool
keyword argument causes an allocation when the rest of the body of the function is not allocating.
By the way, a minor simplification of the example makes everything non-allocating. This is deeply confusing to me. If `work_val` never allocates, why do changes to it matter to whether `work_bool_to_val` allocates? Click here to see this example.
using BenchmarkTools
"""Just some contrived work function that happens to slightly change behavior depending on Val keyword"""
function simple_work_val(obj::Vector; extrawork::Val{B}=Val(true)) where B
l = size(obj,1)
@inbounds for i in 1:l
@inbounds for j in i+1:l
obj[i] ⊻= obj[j]
if B
obj[i] += obj[i]
end
end
end
end
"""A "public interface" function that uses a Bool keyword instead of a Val keyword"""
function simple_work_bool_to_val(obj::Vector; extrawork::Bool=true)
simple_work_val(obj; extrawork=Val(extrawork))
end
n = 10
obj = rand(UInt,n)
@benchmark simple_work_val($obj)
@benchmark simple_work_bool_to_val($obj)
So, maybe it has something to do with the complicated Object
type? Insight on this would be greatly appreciated.
Version Info: 1.8.0 (click to expand)
Julia Version 1.8.0
Commit 5544a0fab76 (2022-08-17 13:38 UTC)
Platform Info:
OS: Linux (x86_64-linux-gnu)
CPU: 16 Ă— AMD Ryzen 7 1700 Eight-Core Processor
WORD_SIZE: 64
LIBM: libopenlibm
LLVM: libLLVM-13.0.1 (ORCJIT, znver1)
Threads: 1 on 16 virtual cores
Environment:
JULIA_EDITOR = code
JULIA_NUM_THREADS = 1