From 169c82c938b982064d0b8e4c862cf68b4890fd9c Mon Sep 17 00:00:00 2001 From: Sebastian Wilzbach Date: Fri, 3 Mar 2017 05:00:49 +0100 Subject: [PATCH] std.algorithm.searching: no mapping-specialization for extremum (#5001) std.algorithm.searching: no mapping-specialization for extremum merged-on-behalf-of: Sebastian Wilzbach --- changelog/std-algorithm-searching-extremum.dd | 22 +++ std/algorithm/searching.d | 187 ++++++++++++++++-- 2 files changed, 188 insertions(+), 21 deletions(-) create mode 100644 changelog/std-algorithm-searching-extremum.dd diff --git a/changelog/std-algorithm-searching-extremum.dd b/changelog/std-algorithm-searching-extremum.dd new file mode 100644 index 000000000..21027820c --- /dev/null +++ b/changelog/std-algorithm-searching-extremum.dd @@ -0,0 +1,22 @@ +Performance improvements for `std.algorithm.searching.{min,max}Element` + +$(REF minElement, std, algorithm, searching) and $(REF maxElement, std, algorithm, searching) +are now considerably faster (almost 2x in microbenchmarks) +as a special path for the identity case is provided. This allows the compiler +to make use of SSE instructions. + +The exact improvements are: + +$(CONSOLE +% dmd -release -O test.d && ./test +extremum.before = 54 secs, 12 ms, 347 μs, and 9 hnsecs +extremum.after = 29 secs, 521 ms, 896 μs, and 5 hnsecs +) + +$(CONSOLE +% ldc -release -O3 test.d && ./test +extremum.before = 13 secs, 186 ms, 176 μs, and 4 hnsecs +extremum.after = 2 secs, 241 ms, 454 μs, and 9 hnsecs +) + +$(LINK2 https://gist.github.com/wilzbach/9f757fa76200956aadb97059d614df34, See the benchmark code). diff --git a/std/algorithm/searching.d b/std/algorithm/searching.d index 05e6067af..8cc70f92f 100644 --- a/std/algorithm/searching.d +++ b/std/algorithm/searching.d @@ -1242,38 +1242,40 @@ Params: Returns: The extreme value according to `map` and `selector` of the passed-in values. */ -private auto extremum(alias map = "a", alias selector = "a < b", Range)(Range r) -if (isInputRange!Range && !isInfinite!Range) +private auto extremum(alias map, alias selector = "a < b", Range)(Range r) +if (isInputRange!Range && !isInfinite!Range && + is(typeof(unaryFun!map(ElementType!(Range).init)))) in { assert(!r.empty, "r is an empty range"); } body { - alias mapFun = unaryFun!map; alias Element = ElementType!Range; Unqual!Element seed = r.front; r.popFront(); return extremum!(map, selector)(r, seed); } -private auto extremum(alias map = "a", alias selector = "a < b", Range, +private auto extremum(alias map, alias selector = "a < b", Range, RangeElementType = ElementType!Range) (Range r, RangeElementType seedElement) if (isInputRange!Range && !isInfinite!Range && - !is(CommonType!(ElementType!Range, RangeElementType) == void)) + !is(CommonType!(ElementType!Range, RangeElementType) == void) && + is(typeof(unaryFun!map(ElementType!(Range).init)))) { alias mapFun = unaryFun!map; alias selectorFun = binaryFun!selector; alias Element = ElementType!Range; alias CommonElement = CommonType!(Element, RangeElementType); - alias MapType = Unqual!(typeof(mapFun(CommonElement.init))); - Unqual!CommonElement extremeElement = seedElement; + + alias MapType = Unqual!(typeof(mapFun(CommonElement.init))); MapType extremeElementMapped = mapFun(extremeElement); - static if (isRandomAccessRange!Range && hasLength!Range) + // direct access via a random access range is faster + static if (isRandomAccessRange!Range) { foreach (const i; 0 .. r.length) { @@ -1301,7 +1303,55 @@ if (isInputRange!Range && !isInfinite!Range && return extremeElement; } -@safe pure nothrow unittest +private auto extremum(alias selector = "a < b", Range)(Range r) + if (isInputRange!Range && !isInfinite!Range && + !is(typeof(unaryFun!selector(ElementType!(Range).init)))) +{ + alias Element = ElementType!Range; + Unqual!Element seed = r.front; + r.popFront(); + return extremum!selector(r, seed); +} + +// if we only have one statement in the loop it can be optimized a lot better +private auto extremum(alias selector = "a < b", Range, + RangeElementType = ElementType!Range) + (Range r, RangeElementType seedElement) + if (isInputRange!Range && !isInfinite!Range && + !is(CommonType!(ElementType!Range, RangeElementType) == void) && + !is(typeof(unaryFun!selector(ElementType!(Range).init)))) +{ + alias Element = ElementType!Range; + alias CommonElement = CommonType!(Element, RangeElementType); + Unqual!CommonElement extremeElement = seedElement; + alias selectorFun = binaryFun!selector; + + // direct access via a random access range is faster + static if (isRandomAccessRange!Range) + { + foreach (const i; 0 .. r.length) + { + if (selectorFun(r[i], extremeElement)) + { + extremeElement = r[i]; + } + } + } + else + { + while (!r.empty) + { + if (selectorFun(r.front, extremeElement)) + { + extremeElement = r.front; + } + r.popFront(); + } + } + return extremeElement; +} + +@safe pure unittest { // allows a custom map to select the extremum assert([[0, 4], [1, 2]].extremum!"a[0]" == [0, 4]); @@ -1310,14 +1360,25 @@ if (isInputRange!Range && !isInfinite!Range && // allows a custom selector for comparison assert([[0, 4], [1, 2]].extremum!("a[0]", "a > b") == [1, 2]); assert([[0, 4], [1, 2]].extremum!("a[1]", "a > b") == [0, 4]); + + // use a custom comparator + import std.math : cmp; + assert([-2., 0, 5].extremum!cmp == 5.0); + assert([-2., 0, 2].extremum!`cmp(a, b) < 0` == -2.0); + + // combine with map + import std.range : enumerate; + assert([-3., 0, 5].enumerate.extremum!(`a.value`, cmp) == tuple(2, 5.0)); + assert([-2., 0, 2].enumerate.extremum!(`a.value`, `cmp(a, b) < 0`) == tuple(0, -2.0)); + + // seed with a custom value + int[] arr; + assert(arr.extremum(1) == 1); } @safe pure nothrow unittest { - // allow seeds - int[] arr; - assert(arr.extremum(1) == 1); - + // 2d seeds int[][] arr2d; assert(arr2d.extremum([1]) == [1]); @@ -1325,6 +1386,46 @@ if (isInputRange!Range && !isInfinite!Range && assert(extremum([2, 3, 4], 1.5) == 1.5); } +@safe pure unittest +{ + import std.range : enumerate, iota; + + // forward ranges + assert(iota(1, 5).extremum() == 1); + assert(iota(2, 5).enumerate.extremum!"a.value" == tuple(0, 2)); + + // should work with const + const(int)[] immArr = [2, 1, 3]; + assert(immArr.extremum == 1); + + // should work with immutable + immutable(int)[] immArr2 = [2, 1, 3]; + assert(immArr2.extremum == 1); + + // with strings + assert(["b", "a", "c"].extremum == "a"); + + // with all dummy ranges + import std.internal.test.dummyrange; + foreach (DummyType; AllDummyRanges) + { + DummyType d; + assert(d.extremum == 1); + assert(d.extremum!(a => a) == 1); + assert(d.extremum!`a > b` == 10); + assert(d.extremum!(a => a, `a > b`) == 10); + } +} + +@nogc @safe nothrow pure unittest +{ + static immutable arr = [7, 3, 4, 2, 1, 8]; + assert(arr.extremum == 1); + + static immutable arr2d = [[1, 9], [3, 1], [4, 2]]; + assert(arr2d.extremum!"a[1]" == arr2d[1]); +} + // find /** Finds an individual element in an input range. Elements of $(D @@ -3237,14 +3338,21 @@ Returns: The minimal element of the passed-in range. See_Also: $(REF min, std,algorithm,comparison) */ -auto minElement(alias map = "a", Range)(Range r) +auto minElement(alias map, Range)(Range r) if (isInputRange!Range && !isInfinite!Range) { return extremum!map(r); } /// ditto -auto minElement(alias map = "a", Range, RangeElementType = ElementType!Range) +auto minElement(Range)(Range r) + if (isInputRange!Range && !isInfinite!Range) +{ + return extremum(r); +} + +/// ditto +auto minElement(alias map, Range, RangeElementType = ElementType!Range) (Range r, RangeElementType seed) if (isInputRange!Range && !isInfinite!Range && !is(CommonType!(ElementType!Range, RangeElementType) == void)) @@ -3252,6 +3360,15 @@ if (isInputRange!Range && !isInfinite!Range && return extremum!map(r, seed); } +/// ditto +auto minElement(Range, RangeElementType = ElementType!Range) + (Range r, RangeElementType seed) + if (isInputRange!Range && !isInfinite!Range && + !is(CommonType!(ElementType!Range, RangeElementType) == void)) +{ + return extremum(r, seed); +} + /// @safe pure unittest { @@ -3299,7 +3416,13 @@ if (isInputRange!Range && !isInfinite!Range && { DummyType d; assert(d.minElement == 1); + assert(d.minElement!(a => a) == 1); } + + // with empty, but seeded ranges + int[] arr; + assert(arr.minElement(42) == 42); + assert(arr.minElement!(a => a)(42) == 42); } @nogc @safe nothrow pure unittest @@ -3329,21 +3452,37 @@ Returns: The maximal element of the passed-in range. See_Also: $(REF max, std,algorithm,comparison) */ -auto maxElement(alias map = "a", Range)(Range r) -if (isInputRange!Range && !isInfinite!Range && - !is(CommonType!(ElementType!Range, RangeElementType) == void)) +auto maxElement(alias map, Range)(Range r) +if (isInputRange!Range && !isInfinite!Range) { return extremum!(map, "a > b")(r); } /// ditto -auto maxElement(alias map = "a", Range, RangeElementType = ElementType!Range) - (Range r, RangeElementType seed) +auto maxElement(Range)(Range r) if (isInputRange!Range && !isInfinite!Range) +{ + return extremum!`a > b`(r); +} + +/// ditto +auto maxElement(alias map, Range, RangeElementType = ElementType!Range) + (Range r, RangeElementType seed) +if (isInputRange!Range && !isInfinite!Range && + !is(CommonType!(ElementType!Range, RangeElementType) == void)) { return extremum!(map, "a > b")(r, seed); } +/// ditto +auto maxElement(Range, RangeElementType = ElementType!Range) + (Range r, RangeElementType seed) +if (isInputRange!Range && !isInfinite!Range && + !is(CommonType!(ElementType!Range, RangeElementType) == void)) +{ + return extremum!`a > b`(r, seed); +} + /// @safe pure unittest { @@ -3392,7 +3531,14 @@ if (isInputRange!Range && !isInfinite!Range) { DummyType d; assert(d.maxElement == 10); + assert(d.maxElement!(a => a) == 10); } + + // with empty, but seeded ranges + int[] arr; + assert(arr.maxElement(42) == 42); + assert(arr.maxElement!(a => a)(42) == 42); + } @nogc @safe nothrow pure unittest @@ -3404,7 +3550,6 @@ if (isInputRange!Range && !isInfinite!Range) assert(arr2d.maxElement!"a[1]" == arr2d[1]); } - // minPos /** Computes a subrange of `range` starting at the first occurrence of `range`'s