In several cases I’ve come across the following pattern:
function f(x; kwargs...)
#intense pre-computation that depend on kwargs but not x
result = 0.0 * x; # one way to preallocate for e.g. x::Vector, but not for scalars
for i in contributions
if isa(x, Number)
result += @. (complicated expression involving x, i, and the precomputations)
else
@. result += (same expression)
end
end
return result
end
The main issue is that @. result += ... errors if result is a scalar. The intense precomputations make externally mapping f costly. One solution is to dispatch f(x::Number; kwargs...) = f([x]; kwargs...), but that array allocation looks awkard to me. The one decent solution I can think of is putting the loop over contributions behind a function barrier, e.g. return sumofcontributions(x,...), but that in my cases requires passing many variables from f’s precomputations to sumofcontributions, which also feels inelegant.
Have you tried to convert x into a 1-element tuple (x,) instead of a vector? Broadcasting works with tuples, too, and you can also mix tuples and vectors. Alternatively, you could try a fixed-length vector, for example SVector from StaticArrays.
EDIT: I see that you need result to be mutable. So maybe x can be made into an SVector and result into a (mutable) MVector. If you then return SVector(result), chances are good that there will be no allocation.
@matthias314 I’d expect that creating an MVector of length 1 would still allocate within f? My other hesitation is that spelling out a type for result (MVector or otherwise) in the body of f also seems not particularly idiomatic.
Not necessarily if the MVector is not returned as such:
julia> using StaticArrays
julia> f(x) = (v = MVector(x); v .*= 2; v[1])
f (generic function with 1 method)
julia> @allocated f(1.0)
0
Something else: Can’t you define the complicated expression as a function g(x) within f (so that you have access to f’s local variables) and then use g either with or without broadcasting?
If any of these captured variables were reassigned, they’ll get boxed and allocated in the current implementation even if they aren’t reassigned after the closure is instantiated.
MVector{1, typeof(x)}(x) is type-generic and statically knowable from specialization over the argument x. Note that StaticArrays.jl really only supports isbits elements; for example, you could instantiate MVector{1, BigInt}, but setindex! would error.