diff --git a/.vscode/launch.json b/.vscode/launch.json index 595999edb..e96aea323 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build_benchmark", - "program": "${workspaceFolder}/playground/Playground/bin/Debug/net6/Playground.dll", + "program": "${workspaceFolder}/playground/Playground/bin/Debug/net7.0/Playground.dll", "args": [], "cwd": "${workspaceFolder}/playground/Playground", "stopAtEntry": false, diff --git a/playground/Playground/MatchCaseOrder.cs b/playground/Playground/MatchCaseOrder.cs deleted file mode 100644 index eaa7d75ec..000000000 --- a/playground/Playground/MatchCaseOrder.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Order; -using DryIoc.ImTools; - -namespace Playground -{ - [MemoryDiagnoser, Orderer(SummaryOrderPolicy.FastestToSlowest)] - public class MatchCaseOrder - { - private readonly int[] _items = { 42, 43, 44, 45 }; - - [Benchmark(Baseline = true)] - public object Match0to3() => _items.Match(x => x % 2 == 0); - - [Benchmark] - public object Match3to0() => _items.Match2(x => x % 2 == 0); - } - - public static class Arr - { - public static T[] Match2(this T[] source, Func condition) - { - if (source == null || source.Length == 0) - return source; - - if (source.Length > 2) - { - var matchStart = 0; - T[] matches = null; - var matchFound = false; - - var i = 0; - while (i < source.Length) - { - matchFound = condition(source[i]); - if (!matchFound) - { - // for accumulated matched items - if (i != 0 && i > matchStart) - matches = ArrayTools.AppendTo(source, matchStart, i - matchStart, matches); - matchStart = i + 1; // guess the next match start will be after the non-matched item - } - - ++i; - } - - // when last match was found but not all items are matched (hence matchStart != 0) - if (matchFound && matchStart != 0) - return ArrayTools.AppendTo(source, matchStart, i - matchStart, matches); - - if (matches != null) - return matches; - - if (matchStart != 0) // no matches - return ArrayTools.Empty(); - - return source; - } - - if (source.Length == 2) - { - var condition0 = condition(source[0]); - var condition1 = condition(source[1]); - return condition0 && condition1 ? new[] { source[0], source[1] } - : condition0 ? new[] { source[0] } - : condition1 ? new[] { source[1] } - : ArrayTools.Empty(); - } - - return condition(source[0]) ? source : ArrayTools.Empty(); - } - } -} diff --git a/playground/Playground/MatchExperiments.cs b/playground/Playground/MatchExperiments.cs new file mode 100644 index 000000000..3a9703f5d --- /dev/null +++ b/playground/Playground/MatchExperiments.cs @@ -0,0 +1,224 @@ +using System; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using DryIoc.ImTools; + +namespace Playground +{ + [MemoryDiagnoser] + public class MatchExperiments + { +/* + +## Match3 + +BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19042.928/20H2/October2020Update) +Intel Core i5-8350U CPU 1.70GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores +.NET SDK=7.0.100 + [Host] : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 + DefaultJob : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 + +## 2 slices + +```cs +private readonly int[] _items = { 42, 43, 44, 45 }; +``` + +| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|------- |---------:|---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| +| Match1 | 39.10 ns | 0.443 ns | 0.414 ns | 39.08 ns | 1.00 | 0.00 | 0.0204 | 64 B | 1.00 | +| Match2 | 44.66 ns | 0.988 ns | 1.138 ns | 44.56 ns | 1.14 | 0.03 | 0.0204 | 64 B | 1.00 | +| Match3 | 31.20 ns | 0.710 ns | 1.243 ns | 30.75 ns | 0.80 | 0.03 | 0.0229 | 72 B | 1.12 | +| Match4 | 29.19 ns | 0.632 ns | 1.539 ns | 28.67 ns | 0.74 | 0.03 | 0.0102 | 32 B | 0.50 | + +## 5 slices + +```cs +private readonly int[] _items = { 4, 1, 42, 44, 45, 46, 47, 48, 49, 52 }; +``` + +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|------- |----------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| +| Match1 | 111.13 ns | 2.275 ns | 3.862 ns | 1.00 | 0.00 | 0.0663 | 208 B | 1.00 | +| Match2 | 106.21 ns | 1.918 ns | 1.700 ns | 0.96 | 0.03 | 0.0663 | 208 B | 1.00 | +| Match3 | 54.01 ns | 1.053 ns | 1.126 ns | 0.49 | 0.02 | 0.0356 | 112 B | 0.54 | +| Match4 | 110.05 ns | 1.568 ns | 1.225 ns | 1.00 | 0.03 | 0.0433 | 136 B | 0.65 | +*/ + + private readonly int[] _items = { 42, 43, 44, 45 }; + // private readonly int[] _items = { 4, 1, 42, 44, 45, 46, 47, 48, 49, 52 }; + + [Benchmark(Baseline = true)] + public object Match1() => _items.Match(x => (x & 1) == 0); + + [Benchmark] + public object Match2() => _items.Match2(x => (x & 1) == 0); + + [Benchmark] + public object Match3() => _items.Match3(x => (x & 1) == 0); + + [Benchmark] + public object Match4() => _items.Match4(x => (x & 1) == 0); + } + + public static class Arr + { + public static T[] Match4(this T[] source, Func condition) + { + var inMatch = false; + var matchesCount = 0; + + var firstMatchStart = 0; + var firstMatchCount = 0; + var secondMatchStart = 0; + var secondMatchCount = 0; + var nextMatchStart = 0; + + T[] result = null; + for (var i = 0; (uint)i < source.Length; i++) + { + if (condition(source[i])) + { + if (!inMatch) + { + inMatch = true; + ++matchesCount; + if (matchesCount == 1) + firstMatchStart = i; + else if (matchesCount == 2) + secondMatchStart = i; + else + nextMatchStart = i; + } + } + else if (inMatch) + { + inMatch = false; + if (matchesCount == 1) + firstMatchCount = i - firstMatchStart; + else if (matchesCount == 2) + secondMatchCount = i - secondMatchStart; + else if (result == null) + { + result = new T[firstMatchCount + secondMatchCount + i - nextMatchStart]; + Array.Copy(source, firstMatchStart, result, 0, firstMatchCount); + Array.Copy(source, secondMatchStart, result, firstMatchCount, secondMatchCount); + Array.Copy(source, nextMatchStart, result, firstMatchCount + secondMatchCount, i - nextMatchStart); + } + else + result = source.AppendTo(result, nextMatchStart, i - nextMatchStart); + } + } + + if (inMatch) + { + if (matchesCount == 1) + firstMatchCount = source.Length - firstMatchStart; + else if (matchesCount == 2) + secondMatchCount = source.Length - secondMatchStart; + else if (result == null) + { + result = new T[firstMatchCount + secondMatchCount + source.Length - nextMatchStart]; + Array.Copy(source, firstMatchStart, result, 0, firstMatchCount); + Array.Copy(source, secondMatchStart, result, firstMatchCount, secondMatchCount); + Array.Copy(source, nextMatchStart, result, firstMatchCount + secondMatchCount, source.Length - nextMatchStart); + } + else + result = source.AppendTo(result, nextMatchStart, source.Length - nextMatchStart); + } + + if (matchesCount < 3) + { + result = new T[firstMatchCount + secondMatchCount]; + + if (firstMatchCount == 1) + result[0] = source[firstMatchStart]; + else + Array.Copy(source, firstMatchStart, result, 0, firstMatchCount); + + if (matchesCount != 1) + { + if (secondMatchCount == 1) + result[firstMatchCount] = source[secondMatchStart]; + else + Array.Copy(source, secondMatchStart, result, firstMatchCount, secondMatchCount); + } + + return result; + } + + return result ?? ArrayTools.Empty(); + } + + public static T[] Match3(this T[] source, Func condition) + { + var results = new T[source.Length]; + + int matched = 0; + for (var i = 0; (uint)i < source.Length; i++) + if (condition(source[i])) + results[matched++] = source[i]; + + Array.Resize(ref results, matched); + return results; + } + + public static T[] Match2(this T[] source, Func condition) + { + if (source == null || source.Length == 0) + return source; + + if (source.Length > 2) + { + var matchStart = 0; + T[] matches = null; + var matchFound = false; + + var i = 0; + while (i < source.Length) + { + matchFound = condition(source[i]); + if (!matchFound) + { + // for accumulated matched items + if (i != 0 && i > matchStart) + // matches = ArrayTools.AppendTo(source, matchStart, i - matchStart, matches); + matches = matches == null + ? source.Copy(matchStart, i - matchStart) + : source.AppendTo(matches, matchStart, i - matchStart); + matchStart = i + 1; // guess the next match start will be after the non-matched item + } + + ++i; + } + + // when last match was found but not all items are matched (hence matchStart != 0) + if (matchFound && matchStart != 0) + // return ArrayTools.AppendTo(source, matchStart, i - matchStart, matches); + return matches == null + ? source.Copy(matchStart, i - matchStart) + : source.AppendTo(matches, matchStart, i - matchStart); + + if (matches != null) + return matches; + + if (matchStart != 0) // no matches + return ArrayTools.Empty(); + + return source; + } + + if (source.Length == 2) + { + var condition0 = condition(source[0]); + var condition1 = condition(source[1]); + return condition0 && condition1 ? new[] { source[0], source[1] } + : condition0 ? new[] { source[0] } + : condition1 ? new[] { source[1] } + : ArrayTools.Empty(); + } + + return condition(source[0]) ? source : ArrayTools.Empty(); + } + } +} diff --git a/playground/Playground/Playground.csproj b/playground/Playground/Playground.csproj index 92c7d2f08..17b447337 100644 --- a/playground/Playground/Playground.csproj +++ b/playground/Playground/Playground.csproj @@ -1,7 +1,7 @@ Exe - net6 + net7.0 Benchmarks, sandbox for experiments. 1701;1702;AD0001;NU1608 false @@ -14,17 +14,17 @@ - - - + + + - + - - - - + + + + diff --git a/playground/Playground/Program.cs b/playground/Playground/Program.cs index 863a4f6f8..706efc7f5 100644 --- a/playground/Playground/Program.cs +++ b/playground/Playground/Program.cs @@ -19,7 +19,7 @@ public static void Main() //bm.SortViaInsertion(); //BenchmarkRunner.Run(); - //BenchmarkRunner.Run(); + BenchmarkRunner.Run(); //BenchmarkRunner.Run(); //BenchmarkRunner.Run(); @@ -40,7 +40,7 @@ public static void Main() //BenchmarkRunner.Run(); //BenchmarkRunner.Run(); //BenchmarkRunner.Run(); - BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); diff --git a/src/DryIoc/ImTools.cs b/src/DryIoc/ImTools.cs index 37ab1d6ab..e5ee40b56 100644 --- a/src/DryIoc/ImTools.cs +++ b/src/DryIoc/ImTools.cs @@ -517,74 +517,59 @@ public static void ForEach(this T[] source, Action action) action(source[i]); } - /// Appends source to results - public static T[] AppendTo(T[] source, int sourcePos, int count, T[] results = null) + /// Copies the slice to the new array, just a sugar extension method + public static T[] Copy(this T[] source, int sourcePos, int count) { - if (results == null) - { - var newResults = new T[count]; - if (count == 1) - newResults[0] = source[sourcePos]; - else - for (int i = 0, j = sourcePos; i < count; ++i, ++j) - newResults[i] = source[j]; - return newResults; - } - - var matchCount = results.Length; - var appendedResults = new T[matchCount + count]; - if (matchCount == 1) - appendedResults[0] = results[0]; - else - Array.Copy(results, 0, appendedResults, 0, matchCount); - + var results = new T[count]; if (count == 1) - appendedResults[matchCount] = source[sourcePos]; + results[0] = source[sourcePos]; else - Array.Copy(source, sourcePos, appendedResults, matchCount, count); - - return appendedResults; + Array.Copy(source, sourcePos, results, 0, count); + return results; } - private static R[] AppendTo(T[] source, int sourcePos, int count, Func map, R[] results = null) + private static R[] Copy(this T[] source, int sourcePos, int count, Func map) { - if (results == null || results.Length == 0) - { - var newResults = new R[count]; - if (count == 1) - newResults[0] = map(source[sourcePos]); - else - for (int i = 0, j = sourcePos; i < count; ++i, ++j) - newResults[i] = map(source[j]); - return newResults; - } - - var oldResultsCount = results.Length; - var appendedResults = new R[oldResultsCount + count]; - if (oldResultsCount == 1) - appendedResults[0] = results[0]; + var results = new R[count]; + if (count == 1) + results[0] = map(source[sourcePos]); else - Array.Copy(results, 0, appendedResults, 0, oldResultsCount); + for (int i = 0, j = sourcePos; i < count; ++i, ++j) + results[i] = map(source[j]); + return results; + } + private static R[] Copy(this T[] source, S state, int sourcePos, int count, Func map) + { + var results = new R[count]; if (count == 1) - appendedResults[oldResultsCount] = map(source[sourcePos]); + results[0] = map(state, source[sourcePos]); else - { - for (int i = oldResultsCount, j = sourcePos; i < appendedResults.Length; ++i, ++j) - appendedResults[i] = map(source[j]); - } - - return appendedResults; + for (int i = 0, j = sourcePos; i < count; ++i, ++j) + results[i] = map(state, source[j]); + return results; } - private static R[] Copy(this T[] source, int sourcePos, int count, Func map) + private static R[] Copy(this T[] source, A a, B b, int sourcePos, int count, Func map) { var results = new R[count]; if (count == 1) - results[0] = map(source[sourcePos]); + results[0] = map(a, b, source[sourcePos]); else for (int i = 0, j = sourcePos; i < count; ++i, ++j) - results[i] = map(source[j]); + results[i] = map(a, b, source[j]); + return results; + } + + /// Appends the slice from the source to the end of the results array possibly the resizing the results to accomodate the slice + public static T[] AppendTo(this T[] source, T[] results, int sourcePos, int count) + { + var oldResultsCount = results.Length; + Array.Resize(ref results, oldResultsCount + count); + if (count == 1) + results[oldResultsCount] = source[sourcePos]; + else + Array.Copy(source, sourcePos, results, oldResultsCount, count); return results; } @@ -600,68 +585,31 @@ private static R[] AppendTo(this T[] source, R[] results, int sourcePos, i return results; } - private static R[] AppendTo(T[] source, S state, int sourcePos, int count, Func map, R[] results = null) + private static R[] AppendTo(this T[] source, S state, R[] results, int sourcePos, int count, Func map) { - if (results == null || results.Length == 0) - { - var newResults = new R[count]; - if (count == 1) - newResults[0] = map(state, source[sourcePos]); - else - for (int i = 0, j = sourcePos; i < count; ++i, ++j) - newResults[i] = map(state, source[j]); - return newResults; - } - var oldResultsCount = results.Length; - var appendedResults = new R[oldResultsCount + count]; - if (oldResultsCount == 1) - appendedResults[0] = results[0]; - else - Array.Copy(results, 0, appendedResults, 0, oldResultsCount); - + Array.Resize(ref results, oldResultsCount + count); if (count == 1) - appendedResults[oldResultsCount] = map(state, source[sourcePos]); + results[oldResultsCount] = map(state, source[sourcePos]); else - { - for (int i = oldResultsCount, j = sourcePos; i < appendedResults.Length; ++i, ++j) - appendedResults[i] = map(state, source[j]); - } - - return appendedResults; + for (int i = oldResultsCount, j = sourcePos; i < results.Length; ++i, ++j) + results[i] = map(state, source[j]); + return results; } - private static R[] AppendTo(T[] source, A a, B b, int sourcePos, int count, Func map, R[] results = null) + private static R[] AppendTo(this T[] source, A a, B b, R[] results, int sourcePos, int count, Func map) { - if (results == null || results.Length == 0) - { - var newResults = new R[count]; - if (count == 1) - newResults[0] = map(a, b, source[sourcePos]); - else - for (int i = 0, j = sourcePos; i < count; ++i, ++j) - newResults[i] = map(a, b, source[j]); - return newResults; - } - var oldResultsCount = results.Length; - var appendedResults = new R[oldResultsCount + count]; - if (oldResultsCount == 1) - appendedResults[0] = results[0]; - else - Array.Copy(results, 0, appendedResults, 0, oldResultsCount); - + Array.Resize(ref results, oldResultsCount + count); if (count == 1) - appendedResults[oldResultsCount] = map(a, b, source[sourcePos]); + results[oldResultsCount] = map(a, b, source[sourcePos]); else - { - for (int i = oldResultsCount, j = sourcePos; i < appendedResults.Length; ++i, ++j) - appendedResults[i] = map(a, b, source[j]); - } - - return appendedResults; + for (int i = oldResultsCount, j = sourcePos; i < results.Length; ++i, ++j) + results[i] = map(a, b, source[j]); + return results; } + // todo: @perf look into MatchExperiments.cs and the benchmarks there /// Where method similar to Enumerable.Where but more performant and non necessary allocating. /// It returns source array and does Not create new one if all items match the condition. public static T[] Match(this T[] source, Func condition) @@ -700,13 +648,17 @@ public static T[] Match(this T[] source, Func condition) { // for accumulated matched items if (i != 0 && i > matchStart) - matches = AppendTo(source, matchStart, i - matchStart, matches); + matches = matches == null + ? source.Copy(matchStart, i - matchStart) + : source.AppendTo(matches, matchStart, i - matchStart); matchStart = i + 1; // guess the next match start will be after the non-matched item } // when last match was found but not all items are matched (hence matchStart != 0) if (matchFound && matchStart != 0) - return AppendTo(source, matchStart, i - matchStart, matches); + return matches == null + ? source.Copy(matchStart, i - matchStart) + : source.AppendTo(matches, matchStart, i - matchStart); return matches ?? (matchStart != 0 ? Empty() : source); } @@ -749,13 +701,17 @@ public static T[] Match(this T[] source, S state, Func conditi { // for accumulated matched items if (i != 0 && i > matchStart) - matches = AppendTo(source, matchStart, i - matchStart, matches); + matches = matches == null + ? source.Copy(matchStart, i - matchStart) + : source.AppendTo(matches, matchStart, i - matchStart); matchStart = i + 1; // guess the next match start will be after the non-matched item } // when last match was found but not all items are matched (hence matchStart != 0) if (matchFound && matchStart != 0) - return AppendTo(source, matchStart, i - matchStart, matches); + return matches == null + ? source.Copy(matchStart, i - matchStart) + : source.AppendTo(matches, matchStart, i - matchStart); return matches ?? (matchStart != 0 ? Empty() : source); } @@ -786,13 +742,17 @@ public static T[] Match(this T[] source, A a, B b, Func { // for accumulated matched items if (i != 0 && i > matchStart) - matches = AppendTo(source, matchStart, i - matchStart, matches); + matches = matches == null + ? source.Copy(matchStart, i - matchStart) + : source.AppendTo(matches, matchStart, i - matchStart); matchStart = i + 1; // guess the next match start will be after the non-matched item } // when last match was found but not all items are matched (hence matchStart != 0) if (matchFound && matchStart != 0) - return AppendTo(source, matchStart, i - matchStart, matches); + return matches == null + ? source.Copy(matchStart, i - matchStart) + : source.AppendTo(matches, matchStart, i - matchStart); return matches ?? (matchStart != 0 ? Empty() : source); } @@ -837,6 +797,7 @@ public static R[] Match(this T[] source, Func condition, Func(this T[] source, S state, Func cond { // for accumulated matched items if (i != 0 && i > matchStart) - matches = AppendTo(source, state, matchStart, i - matchStart, map, matches); + matches = matches == null + ? source.Copy(state, matchStart, i - matchStart, map) + : source.AppendTo(state, matches, matchStart, i - matchStart, map); matchStart = i + 1; // guess the next match start will be after the non-matched item } // when last match was found but not all items are matched (hence matchStart != 0) if (matchFound && matchStart != 0) - return AppendTo(source, state, matchStart, i - matchStart, map, matches); + return matches == null + ? source.Copy(state, matchStart, i - matchStart, map) + : source.AppendTo(state, matches, matchStart, i - matchStart, map); - return matches ?? (matchStart == 0 ? AppendTo(source, state, 0, source.Length, map) : Empty()); + return matches ?? (matchStart == 0 ? source.Copy(state, 0, source.Length, map) : Empty()); } /// Match with the additional state to use in and @@ -951,15 +916,20 @@ public static R[] Match(this T[] source, A a, B b, Func matchStart) - matches = AppendTo(source, a, b, matchStart, i - matchStart, map, matches); + matches = matches == null + ? source.Copy(a, b, matchStart, i - matchStart, map) + : source.AppendTo(a, b, matches, matchStart, i - matchStart, map); + matchStart = i + 1; // guess the next match start will be after the non-matched item } // when last match was found but not all items are matched (hence matchStart != 0) if (matchFound && matchStart != 0) - return AppendTo(source, a, b, matchStart, i - matchStart, map, matches); + return matches == null + ? source.Copy(a, b, matchStart, i - matchStart, map) + : source.AppendTo(a, b, matches, matchStart, i - matchStart, map); - return matches ?? (matchStart == 0 ? AppendTo(source, a, b, 0, source.Length, map) : Empty()); + return matches ?? (matchStart == 0 ? source.Copy(a, b, 0, source.Length, map) : Empty()); } /// Maps all items from source to result array.