Find positions of minimum and maximum values at once

The minimum and maximum values of an iterable can be found either with minimum and maximum, and more conveniently(*) with extrema.

Also, the positions of those extrema can be found with findmin and findmax (or argmin, argmax) separately. But I don’t know of any function in Base to obtain both at once. Did I miss it, or is there any reason why findextrema or argextrema are not desirable?

(*) I also expected that extrema would be more performant than minimum and maximum separately, but it isn’t.

2 Likes

Are you sure about that?

julia> @btime (minimum(x), maximum(x)) setup=(x=randn(10_000))
  62.222 μs (0 allocations: 0 bytes)
(-3.6004358584957203, 4.050586645172776)

julia> @btime extrema(x) setup=(x=randn(10_000))
  30.402 μs (0 allocations: 0 bytes)
(-4.16019176790883, 3.907089251771992)

Surprisingly, though, findmin/max is faster than minimum/maximum:

julia> @btime (findmin(x), findmax(x)) setup=(x=randn(10_000))
  33.522 μs (0 allocations: 0 bytes)
((-3.451315161397838, 4075), (3.534236845843399, 8767))

See the linked issue — extrema fails to SIMD-vectorize on some systems, apparently. So it depends on your CPU.

Yep, exactly. I still see that issue on my Mac’s ARM processor. The 4x difference is quite telling.

julia> @btime (minimum(x), maximum(x)) setup=(x=randn(10_000))
  12.416 μs (0 allocations: 0 bytes)
(-3.989876350573738, 3.444422298891971)

julia> @btime (extrema(x)) setup=(x=randn(10_000))
  49.042 μs (0 allocations: 0 bytes)
(-3.639776550439404, 3.7251211876149286)

julia> @btime (minimum(x), maximum(x)) setup=(x=rand(-100:100, 10_000))
  6.125 μs (0 allocations: 0 bytes)
(-100, 100)

julia> @btime (extrema(x)) setup=(x=rand(-100:100, 10_000))
  3.073 μs (0 allocations: 0 bytes)
(-100, 100)

Here’s a quick-and-dirty implementation of findextrema which is much faster on my laptop at least:

function findextrema(x)
    isempty(x) && throw(ArgumentError("reducing over an empty collection is not allowed."))
    minind = maxind = firstindex(x)
    minval = maxval = first(x)
    for (i, val) in pairs(x)
        (minind, minval) = ifelse(val < minval, (i, val), (minind, minval))
        (maxind, maxval) = ifelse(val > maxval, (i, val), (maxind, maxval))
    end
    return (minind, minval), (maxind, maxval)
end

Benchmark:

julia> @btime extrema(x) setup=(x=randn(10_000))
  30.468 μs (0 allocations: 0 bytes)
(-4.460544193815779, 3.614705321217675)

julia> @btime (findmin(x), findmax(x)) setup=(x=randn(10_000))
  33.541 μs (0 allocations: 0 bytes)
((-3.9324850320327984, 4062), (3.7604910904523896, 502))

julia> @btime findextrema(x) setup=(x=randn(10_000))
  8.370 μs (0 allocations: 0 bytes)
((9567, -3.7857378107436417), (4247, 3.8199475029280956))

This is an Intel processor, though.

2 Likes