I don’t think heap allocation is “bad” - in most cases it is wanted and needed. First of all stack is usually limited to few megabytes and stack space is tightly assigned to a specific thread.
Consider such example
input1 = rand(1000000)
input2 = rand(1000000)
We need to calculate sum of squares of element-wise differences
function with_heap_allocations(input1, intput2)
result = sum((input1 .- input2).^2)
end
function no_heap_allocation(input1, input2)
result2 = 0.0
for i in 1:length(input1)
result2 += (input1[i] - input2[i])^2
end
return result2
end
Timing results
julia> @time with_heap_allocations(input1, input2)
0.002924 seconds (13 allocations: 7.630 MiB)
166623.91955443815
julia> @time no_heap_allocation(input1, input2)
0.001143 seconds (5 allocations: 176 bytes)
166623.91955444182
In the first example broadcast operator is doing in-memory projection of its results before passing it to sum
so that is why you see few megabytes allocated.
The latter one is just iterating data that already are in the memory, so no significant allocations are seen.
Anyway, I don’t think the second implementation is better. It is much longer and harder to reason about. In most cases you should not care about those allocations too much, these days memory is cheap and garbage collectors are very sophisticated.
Also Julia is a bit different than general purpose languages with functional features (C#, Haskell, Java). For instance in Julia map
function is doing in-memory projection of the result, but in other languages you might expect some “lazy” collection that could be further processed and the actual code is executed when you try to look into the final data without any allocations for intermediate results. In Julia I observed that iterating zip
function result seems to have such laziness property, example:
sum((pair -> (pair[1]- pair[2])^2), zip(input1, input2))