Julia: maximum is 2/3 times slower than explicit for loop

Created on 12 Feb 2018  路  6Comments  路  Source: JuliaLang/julia

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.

performance

Most helpful comment

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?

All 6 comments

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
Was this page helpful?
0 / 5 - 0 ratings