Using maximum with a function as first argument (or with a generator) can be 2 or 3 times slower than an explicit for loop, where intuitively they should take more or less the same time: see this comment on discourse. Here I'm computing the maximum of the absolute value of elements in an array without allocating the intermediate abs.(v) array.
julia> v = randn(1000000);
julia> function maxabs(v)
m = abs(v[1])
for i in 2:length(v)
z = abs(v[i])
z>m && (m=z)
end
m
end
maxabs (generic function with 1 method)
julia> @benchmark maxabs($v)
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 594.965 渭s (0.00% GC)
median time: 622.898 渭s (0.00% GC)
mean time: 633.162 渭s (0.00% GC)
maximum time: 1.057 ms (0.00% GC)
--------------
samples: 7867
evals/sample: 1
julia> @benchmark maximum(abs, $v)
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 1.586 ms (0.00% GC)
median time: 1.659 ms (0.00% GC)
mean time: 1.682 ms (0.00% GC)
maximum time: 2.718 ms (0.00% GC)
--------------
samples: 2968
evals/sample: 1
julia> @benchmark maximum(abs(el) for el in $v)
BenchmarkTools.Trial:
memory estimate: 16 bytes
allocs estimate: 1
--------------
minimum time: 1.482 ms (0.00% GC)
median time: 1.504 ms (0.00% GC)
mean time: 1.538 ms (0.00% GC)
maximum time: 2.682 ms (0.00% GC)
--------------
samples: 3245
evals/sample: 1
I'm on Julia 0.7, but the same issue appeared on Julia 0.6 (I have to note that the generator version improved between 0.6 and 0.7: in 0.6 it was slower than the function version, now they are comparable).
julia> versioninfo()
Julia Version 0.7.0-DEV.3634
Commit 8d8f960faa (2018-01-29 10:32 UTC)
Platform Info:
OS: Linux (x86_64-linux-gnu)
CPU: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
WORD_SIZE: 64
LIBM: libopenlibm
LLVM: libLLVM-3.9.1 (ORCJIT, skylake)
Environment:
EDIT: the issue is about maximum, not about generators, see comments below. I've edited the title accordingly.
Could the difference be that the built-in method cares about NaN?
julia> maxabs([1.0, NaN])
1.0
julia> maximum(abs, [1.0, NaN])
NaN
This appears to be not about generators but about maximum. Tested on v0.6: maximum uses m = max(m,z). Your implementation with &&, or m = ifelse(z > m, m, z) is about two times faster, not sure why. I also get a 10% speedup by removing the (v == v) (NaN handling?) in mapreduce_impl.
Simpler MWE:
mymax(x,y) = ifelse(x > y, x, y)
@benchmark mapreduce(abs, max, v)
@benchmark mapreduce(abs, mymax, v)
I get 2.1 vs 0.8, and same behavior on NaNs.
You are right, maximum is slower even without using generators.
The difference is in the NaN handling may be because I use < rather than isless, but < is much faster as @antoine-levitt points out:
julia> function mymax(v)
m = v[1]
for i in 2:length(v)
z = v[i]
isless(z,m) || (m=z)
end
m
end
mymax (generic function with 1 method)
julia> function mymax2(v)
m = v[1]
for i in 2:length(v)
z = v[i]
z<m || (m=z)
end
m
end
mymax2 (generic function with 1 method)
julia> @benchmark mymax($v)
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 1.562 ms (0.00% GC)
median time: 1.584 ms (0.00% GC)
mean time: 1.614 ms (0.00% GC)
maximum time: 2.320 ms (0.00% GC)
--------------
samples: 3094
evals/sample: 1
julia> @benchmark mymax2($v)
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 588.142 渭s (0.00% GC)
median time: 609.928 渭s (0.00% GC)
mean time: 625.466 渭s (0.00% GC)
maximum time: 1.109 ms (0.00% GC)
--------------
samples: 7962
evals/sample: 1
julia> @benchmark maximum($v)
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 1.532 ms (0.00% GC)
median time: 1.638 ms (0.00% GC)
mean time: 1.674 ms (0.00% GC)
maximum time: 3.061 ms (0.00% GC)
--------------
samples: 2980
evals/sample: 1
OK, this turns out to be because of
max(x::T, y::T) where {T<:AbstractFloat} = ifelse((y > x) | (signbit(y) < signbit(x)),
ifelse(isnan(x), x, y), ifelse(isnan(y), y, x))
The speed difference goes away under fastmath. Since maximum already does its own NaN handling, maybe it could simply pass a faster max to mapreduce?
Seems fixed
julia> @btime maxabs($v)
800.098 渭s (0 allocations: 0 bytes)
4.734731485436737
julia> @btime maximum(abs, $v)
415.926 渭s (0 allocations: 0 bytes)
4.734731485436737
Most helpful comment
OK, this turns out to be because of
The speed difference goes away under fastmath. Since maximum already does its own NaN handling, maybe it could simply pass a faster max to mapreduce?