diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..c0f35c9e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [ master, main ] + pull_request: + +jobs: + build-test: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 10.0.x + 11.0.x + dotnet-quality: preview + - name: Build + run: dotnet build ImTools.slnx -c Release -p:DevMode=false + - name: Test + run: dotnet test --no-build -c Release -p:DevMode=false test/ImTools.UnitTests/ImTools.UnitTests.csproj + - name: Pack + run: BuildScripts\NugetPack.bat + - uses: actions/upload-artifact@v4 + with: + name: packages + path: .dist/packages/*.nupkg diff --git a/.vscode/settings.json b/.vscode/settings.json index 051e436f..a91c68fc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,9 @@ { "cSpell.words": [ + "Aval", + "Bval", "cref", + "Cval", "Hasher", "langword", "mentries", @@ -11,5 +14,6 @@ ], "files.watcherExclude": { "**/target": true - } + }, + "dotnet.defaultSolution": "ImTools.slnx" } \ No newline at end of file diff --git a/ImTools.sln b/ImTools.sln deleted file mode 100644 index 73d333c3..00000000 --- a/ImTools.sln +++ /dev/null @@ -1,107 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29424.173 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImTools", "src\ImTools\ImTools.csproj", "{6B4B1E6B-6C7B-4FE4-8620-D4ABEE67C592}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A6F5B847-FF68-4EA1-98D4-B7517296AC16}" - ProjectSection(SolutionItems) = preProject - src\Directory.Build.props = src\Directory.Build.props - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F779D683-7F9C-4B65-95C5-ED8E75642362}" - ProjectSection(SolutionItems) = preProject - test\Directory.Build.props = test\Directory.Build.props - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "playground", "playground", "{D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3}" - ProjectSection(SolutionItems) = preProject - playground\Directory.Build.props = playground\Directory.Build.props - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F6D087C9-3B9A-413A-86DD-14BD1F6BF013}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - .travis.yml = .travis.yml - appveyor.yml = appveyor.yml - build.bat = build.bat - build_no_legacy.bat = build_no_legacy.bat - Directory.Build.props = Directory.Build.props - README.md = README.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImTools.UnitTests", "test\ImTools.UnitTests\ImTools.UnitTests.csproj", "{D95E6E09-2052-4F08-A4D4-C73244720920}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImTools.Benchmarks", "playground\ImTools.Benchmarks\ImTools.Benchmarks.csproj", "{113D7A58-BA48-4C82-9E3C-0862BE1186A5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{CD913EEB-7AF4-47E3-8B7C-02235E5DCDE4}" - ProjectSection(SolutionItems) = preProject - build\NuGetPack.bat = build\NuGetPack.bat - build\NuGetPublish.bat = build\NuGetPublish.bat - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nuspecs", "nuspecs", "{29BC7B76-D817-425F-98F1-A92454D23857}" - ProjectSection(SolutionItems) = preProject - nuspecs\ImTools.nuspec = nuspecs\ImTools.nuspec - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImTools.UnionPlayground", "playground\ImTools.UnionPlayground\ImTools.UnionPlayground.csproj", "{EFBD055A-3C8A-45C7-8718-D9C53A66521D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImTools.SimpleDIPlayground", "playground\ImTools.SimpleDIPlayground\ImTools.SimpleDIPlayground.csproj", "{60B58862-31B3-48DC-BFF3-42A9B0A194A5}" -EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "ImTools.UnionCsVsFsBenchmarks", "playground\ImTools.UnionCsVsFsBenchmarks\ImTools.UnionCsVsFsBenchmarks.fsproj", "{239B6CA0-2860-4A67-859F-FD4EF6DAA547}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Experiments", "playground\Experiments\Experiments.csproj", "{88D8A585-1936-41DC-9A6E-74589EB6EA97}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6B4B1E6B-6C7B-4FE4-8620-D4ABEE67C592}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6B4B1E6B-6C7B-4FE4-8620-D4ABEE67C592}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6B4B1E6B-6C7B-4FE4-8620-D4ABEE67C592}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6B4B1E6B-6C7B-4FE4-8620-D4ABEE67C592}.Release|Any CPU.Build.0 = Release|Any CPU - {D95E6E09-2052-4F08-A4D4-C73244720920}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D95E6E09-2052-4F08-A4D4-C73244720920}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D95E6E09-2052-4F08-A4D4-C73244720920}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D95E6E09-2052-4F08-A4D4-C73244720920}.Release|Any CPU.Build.0 = Release|Any CPU - {113D7A58-BA48-4C82-9E3C-0862BE1186A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {113D7A58-BA48-4C82-9E3C-0862BE1186A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {113D7A58-BA48-4C82-9E3C-0862BE1186A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {113D7A58-BA48-4C82-9E3C-0862BE1186A5}.Release|Any CPU.Build.0 = Release|Any CPU - {EFBD055A-3C8A-45C7-8718-D9C53A66521D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EFBD055A-3C8A-45C7-8718-D9C53A66521D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EFBD055A-3C8A-45C7-8718-D9C53A66521D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EFBD055A-3C8A-45C7-8718-D9C53A66521D}.Release|Any CPU.Build.0 = Release|Any CPU - {60B58862-31B3-48DC-BFF3-42A9B0A194A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {60B58862-31B3-48DC-BFF3-42A9B0A194A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {60B58862-31B3-48DC-BFF3-42A9B0A194A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {60B58862-31B3-48DC-BFF3-42A9B0A194A5}.Release|Any CPU.Build.0 = Release|Any CPU - {239B6CA0-2860-4A67-859F-FD4EF6DAA547}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {239B6CA0-2860-4A67-859F-FD4EF6DAA547}.Debug|Any CPU.Build.0 = Debug|Any CPU - {239B6CA0-2860-4A67-859F-FD4EF6DAA547}.Release|Any CPU.ActiveCfg = Release|Any CPU - {239B6CA0-2860-4A67-859F-FD4EF6DAA547}.Release|Any CPU.Build.0 = Release|Any CPU - {88D8A585-1936-41DC-9A6E-74589EB6EA97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {88D8A585-1936-41DC-9A6E-74589EB6EA97}.Debug|Any CPU.Build.0 = Debug|Any CPU - {88D8A585-1936-41DC-9A6E-74589EB6EA97}.Release|Any CPU.ActiveCfg = Release|Any CPU - {88D8A585-1936-41DC-9A6E-74589EB6EA97}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {6B4B1E6B-6C7B-4FE4-8620-D4ABEE67C592} = {A6F5B847-FF68-4EA1-98D4-B7517296AC16} - {D95E6E09-2052-4F08-A4D4-C73244720920} = {F779D683-7F9C-4B65-95C5-ED8E75642362} - {113D7A58-BA48-4C82-9E3C-0862BE1186A5} = {D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3} - {EFBD055A-3C8A-45C7-8718-D9C53A66521D} = {D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3} - {60B58862-31B3-48DC-BFF3-42A9B0A194A5} = {D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3} - {239B6CA0-2860-4A67-859F-FD4EF6DAA547} = {D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3} - {88D8A585-1936-41DC-9A6E-74589EB6EA97} = {D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {B8B8E41A-6F08-46C3-828D-C74B91428793} - EndGlobalSection -EndGlobal diff --git a/ImTools.slnx b/ImTools.slnx new file mode 100644 index 00000000..3424ff77 --- /dev/null +++ b/ImTools.slnx @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 32097dd6..d1b7e5b9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Initially split from the [DryIoc](https://github.com/dadhi/dryioc), and now used [![license](https://img.shields.io/github/license/dadhi/ImTools.svg)](http://opensource.org/licenses/MIT) -- Windows, Linux, MacOS [![CI build](https://ci.appveyor.com/api/projects/status/el9echuqfnl86u53?svg=true)](https://ci.appveyor.com/project/MaksimVolkau/imtools/branch/master) +- Windows, Linux, MacOS [![CI build](https://github.com/dadhi/ImTools/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/dadhi/ImTools/actions/workflows/ci.yml) - Lib package [![NuGet Version](https://img.shields.io/nuget/v/ImTools.dll)](https://www.nuget.org/packages/ImTools.dll)![NuGet Downloads](https://img.shields.io/nuget/dt/ImTools.dll) - Code package [![NuGet Version](https://img.shields.io/nuget/v/ImTools)](https://www.nuget.org/packages/ImTools)![NuGet Downloads](https://img.shields.io/nuget/dt/ImTools) - Latest release [![latest release](https://img.shields.io/badge/latest%20release-v4.0.0-green)](https://github.com/dadhi/ImTools/releases/tag/v4.0.0) diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 4d690fac..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,25 +0,0 @@ -image: - - Visual Studio 2022 - - Ubuntu - - macOS - -version: 5.0.0-preview-01-b{build} - -test: off - -skip_commits: - message: /noci .*/ - -build_script: - - dotnet test -p:CILinux=true -c:Release test/ImTools.UnitTests/ImTools.UnitTests.csproj - -for: - - matrix: - only: - - image: Visual Studio 2022 - - build_script: - - build_with_packaging.bat - - artifacts: - - path: .\.dist\packages\*.nupkg diff --git a/b.bat b/b.bat index 5b85acb5..5dad065e 100644 --- a/b.bat +++ b/b.bat @@ -5,7 +5,7 @@ echo:## Starting: RESTORE and BUILD... echo: dotnet clean -v:m -dotnet build -c:Release -v:m +dotnet build ImTools.slnx -c:Release -v:m if %ERRORLEVEL% neq 0 goto :error echo: diff --git a/bd.bat b/bd.bat index 5b85acb5..5ed2022e 100644 --- a/bd.bat +++ b/bd.bat @@ -5,7 +5,7 @@ echo:## Starting: RESTORE and BUILD... echo: dotnet clean -v:m -dotnet build -c:Release -v:m +dotnet build ImTools.slnx -c:Release -v:m -p:DevMode=true if %ERRORLEVEL% neq 0 goto :error echo: @@ -15,7 +15,7 @@ echo: echo:## Starting: TESTS... echo: -dotnet test --no-build -c Release test/ImTools.UnitTests/ImTools.UnitTests.csproj +dotnet test --no-build -c Release -p:DevMode=true test/ImTools.UnitTests/ImTools.UnitTests.csproj if %ERRORLEVEL% neq 0 goto :error echo: diff --git a/bm.bat b/bm.bat index f3da08a7..8f9bcc99 100644 --- a/bm.bat +++ b/bm.bat @@ -1 +1 @@ -dotnet run -f net8.0 -c Release --project playground\ImTools.Benchmarks \ No newline at end of file +dotnet run -f net10.0 -c Release --project playground\ImTools.Benchmarks \ No newline at end of file diff --git a/build.bat b/build.bat index 896c2de9..84b1a802 100644 --- a/build.bat +++ b/build.bat @@ -5,7 +5,7 @@ echo:## Starting: RESTORE and BUILD... echo: dotnet clean -v:m -dotnet build -c:Release -v:m -p:DevMode=false +dotnet build ImTools.slnx -c:Release -v:m -p:DevMode=false if %ERRORLEVEL% neq 0 goto :error echo: diff --git a/build_with_packaging.bat b/build_with_packaging.bat index 2d44a746..27a2457f 100644 --- a/build_with_packaging.bat +++ b/build_with_packaging.bat @@ -5,7 +5,7 @@ echo:## Starting: RESTORE and BUILD... echo: dotnet clean -v:m -dotnet build -c:Release -p:DevMode=false -v:m +dotnet build ImTools.slnx -c:Release -p:DevMode=false -v:m if %ERRORLEVEL% neq 0 goto :error echo: diff --git a/playground/Experiments/Experiments.csproj b/playground/Experiments/Experiments.csproj index f78b5eb9..1f2e8fb7 100644 --- a/playground/Experiments/Experiments.csproj +++ b/playground/Experiments/Experiments.csproj @@ -1,9 +1,8 @@ - net8.0 - net8.0;net6.0;net472 - net7.0 + net11.0;net10.0;net8.0 + net11.0;net10.0;net8.0;net472 1701;1702;AD0001;NU1608 diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 21bd1402..d8b1d8b8 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -1,17 +1,79 @@ +// +/* +The MIT License (MIT) + +Copyright (c) 2016-2025 Maksim Volkau + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// ReSharper disable once InconsistentNaming +#nullable disable + +#if DEBUG +#define VERIFY_MAP +#endif + +#if !NETSTANDARD2_0_OR_GREATER && !NET472 +#define SUPPORTS_UNSAFE +#define SUPPORTS_CREATE_SPAN +#endif + +#if LIGHT_EXPRESSION +namespace FastExpressionCompiler.LightExpression.ImTools; +#else +namespace FastExpressionCompiler.ImTools; +#endif + using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Text; +using System.Linq; using System.Runtime.CompilerServices; - -#if NET7_0_OR_GREATER using System.Runtime.InteropServices; +using System.Diagnostics.CodeAnalysis; + +#if NET8_0_OR_GREATER +using System.Runtime.Intrinsics; +using System.Numerics; #endif -namespace FastExpressionCompiler.ImTools; +using static SmallMap; -using static FHashMap; +/// Helpers and polyfills for the missing things in the old .NET versions +public static class RefTools +{ + /// Polyfill for the missing returning the `ref` in the failed search scenario. + /// Note that the result is the `null` even for the struct `T`, so avoid the accessing its members without the check + [MethodImpl((MethodImplOptions)256)] + public static ref T GetNullRef() => +#if SUPPORTS_UNSAFE + ref Unsafe.NullRef(); +#else + ref _missing; + internal static T _missing = default; +#endif +} -/// Wrapper for the array and count +/// Wrapper for the array of the specific capacity and a separate count less or equal to this capacity public struct SmallList { /// Array of items @@ -37,13 +99,17 @@ public SmallList(T[] items) : this(items, items.Length) { } public static class SmallList { internal const int ForLoopCopyCount = 4; - internal const int InitialCapacity = 4; + internal const int DefaultInitialCapacity = 4; + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static ref T ThrowIndexOutOfBounds(int index, int count) => + throw new IndexOutOfRangeException($"Index {index} is out of range of count {count} for SmallList<{typeof(T)},..>."); [MethodImpl((MethodImplOptions)256)] - internal static void Expand(ref TItem[] items) + internal static void Expand(ref T[] items) { // `| 1` is for the case when the length is 0 - var newItems = new TItem[(items.Length << 1) | 1]; // have fun to guess the new length, haha ;-P + var newItems = new T[(items.Length << 1) | 1]; // have fun to guess the new length, ha-ha ;-P if (items.Length > ForLoopCopyCount) Array.Copy(items, newItems, items.Length); else @@ -55,7 +121,7 @@ internal static void Expand(ref TItem[] items) /// Appends the new default item at the end of the items. Assumes that `index lte items.Length`! /// `items` should be not null [MethodImpl((MethodImplOptions)256)] - public static ref TItem AppendDefaultToNotNullItemsAndGetRef(ref TItem[] items, int index) + public static ref T AddDefaultToNotNullItemsAndGetRef(ref T[] items, int index) { Debug.Assert(index <= items.Length); if (index == items.Length) @@ -65,12 +131,12 @@ public static ref TItem AppendDefaultToNotNullItemsAndGetRef(ref TItem[] /// Appends the new default item at the end of the items. Assumes that `index lte items.Length`, `items` may be null [MethodImpl((MethodImplOptions)256)] - public static ref TItem AppendDefaultAndGetRef(ref TItem[] items, int index, int initialCapacity = InitialCapacity) + public static ref T AddDefaultAndGetRef(ref T[] items, int index, int initialCapacity = DefaultInitialCapacity) { if (items == null) { Debug.Assert(index == 0); - items = new TItem[initialCapacity]; + items = new T[initialCapacity]; return ref items[index]; } @@ -82,624 +148,1272 @@ public static ref TItem AppendDefaultAndGetRef(ref TItem[] items, int ind /// Returns surely present item ref by its index [MethodImpl((MethodImplOptions)256)] - public static ref TItem GetSurePresentItemRef(this ref SmallList source, int index) => + public static ref T GetSurePresentItemRef(this ref SmallList source, int index) => ref source.Items[index]; + /// Returns surely present item ref by its index without boundary checks + [MethodImpl((MethodImplOptions)256)] + public static ref T GetSurePresentRef(this T[] source, int index) => +#if SUPPORTS_UNSAFE + ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(source), index); +#else + ref source[index]; +#endif + +#if NET7_0_OR_GREATER + /// Get the item by-ref without bounds check + [MethodImpl((MethodImplOptions)256)] + public static ref T GetSurePresentItemRef(this ref T source, int index) where T : struct => + ref Unsafe.Add(ref source, index); +#endif + + /// Get the item without bounds check + [MethodImpl((MethodImplOptions)256)] +#if NET7_0_OR_GREATER + internal static T GetSurePresentItem(this ref T source, int index) where T : struct => Unsafe.Add(ref source, index); +#else + internal static T GetSurePresentItem(this T[] source, int index) => source[index]; +#endif + // todo: @perf add the not null variant /// Appends the new default item to the list and returns ref to it for write or read [MethodImpl((MethodImplOptions)256)] - public static ref TItem Append(ref this SmallList source, int initialCapacity = InitialCapacity) => - ref AppendDefaultAndGetRef(ref source.Items, source.Count++, initialCapacity); + public static ref T Add(this ref SmallList source, int initialCapacity = DefaultInitialCapacity) => + ref AddDefaultAndGetRef(ref source.Items, source.Count++, initialCapacity); - /// Appends the new item to the list // todo: @perf add the not null variant + /// Appends the new item to the list [MethodImpl((MethodImplOptions)256)] - public static void Append(ref this SmallList source, in TItem item, int initialCapacity = InitialCapacity) => - AppendDefaultAndGetRef(ref source.Items, source.Count++, initialCapacity) = item; + public static void Add(this ref SmallList source, in T item, int initialCapacity = DefaultInitialCapacity) => + AddDefaultAndGetRef(ref source.Items, source.Count++, initialCapacity) = item; /// Looks for the item in the list and return its index if found or -1 for the absent item [MethodImpl((MethodImplOptions)256)] - public static int TryGetIndex(this ref SmallList source, TItem it, TEq eq = default) - where TEq : struct, IEq + public static int TryGetIndex(this T[] items, in T it, int startIndex, int count, TEq eq = default, + int notFoundResult = -1) + where TEq : struct, IEq { - var count = source.Count; - var items = source.Items; - for (var i = 0; i < count; ++i) + Debug.Assert(items != null); + for (var i = startIndex; i < count; ++i) { ref var di = ref items[i]; // todo: @perf Marshall? if (eq.Equals(it, di)) return i; } - return -1; + return notFoundResult; } - /// Returns the ref of the found item or appends the item to the end of the list, and returns ref to it + /// Looks for the item in the list and return its index if found or -1 for the absent item [MethodImpl((MethodImplOptions)256)] - public static int GetIndexOrAppend(this ref SmallList source, in TItem item, TEq eq) - where TEq : struct, IEq - { - var count = source.Count; - var items = source.Items; - for (var i = 0; i < count; ++i) - { - ref var di = ref items[i]; // todo: @perf Marshall? - if (eq.Equals(item, di)) - return i; - } - source.Append() = item; - return -1; - } + public static int TryGetIndex(this ref SmallList source, T it, TEq eq = default) + where TEq : struct, IEq + => source.Items.TryGetIndex(it, 0, source.Count, eq); - /// Returns surely present item ref by its index + /// Returns the index of the found item or appends the item to the end of the list, and returns its index [MethodImpl((MethodImplOptions)256)] - public static ref TItem GetSurePresentItemRef(this ref SmallList4 source, int index) + public static int GetIndexOrAdd(this ref SmallList source, in T item, TEq eq = default) + where TEq : struct, IEq { - Debug.Assert(source.Count != 0); - Debug.Assert(index < source.Count); - switch (index) + var count = source.Count; + if (count != 0) { - case 0: return ref source._it0; - case 1: return ref source._it1; - case 2: return ref source._it2; - case 3: return ref source._it3; - default: - Debug.Assert(source._rest != null, $"Expecting deeper items are already existing on stack at index: {index}"); - return ref source._rest[index - SmallList4.StackItemCount]; + var index = TryGetIndex(source.Items, in item, 0, count, eq); + if (index != -1) + return index; } + source.Add() = item; + return count; } +} - /// Returns last present item ref, assumes that the list is not empty! - [MethodImpl((MethodImplOptions)256)] - public static ref TItem GetLastSurePresentItem(this ref SmallList4 source) => - ref source.GetSurePresentItemRef(source._count - 1); - - /// Returns the ref to tombstone indicating the missing item. - [MethodImpl((MethodImplOptions)256)] - public static ref TItem NotFound(this ref SmallList4 _) => ref SmallList4.Missing; +#pragma warning disable CS9101 // UnscopedRef goes wrong on Ubuntu - /// Appends the default item to the end of the list and returns the reference to it. - [MethodImpl((MethodImplOptions)256)] - public static ref TItem AppendDefaultAndGetRef(this ref SmallList4 source) - { - var index = source._count++; - switch (index) - { - case 0: return ref source._it0; - case 1: return ref source._it1; - case 2: return ref source._it2; - case 3: return ref source._it3; - default: - return ref AppendDefaultAndGetRef(ref source._rest, index - SmallList4.StackItemCount, SmallList4.StackItemCount); - } - } +/// Utilities for Stack4, Stack8, etc. +public static class Stack +{ + [MethodImpl(MethodImplOptions.NoInlining)] + internal static ref T ThrowIndexOutOfBounds(int index, int capacity) => + throw new IndexOutOfRangeException($"Index {index} is out of range for Stack{capacity}<{typeof(T)},..>."); +} - /// Looks for the item in the list and return its index if found or -1 for the absent item - [MethodImpl((MethodImplOptions)256)] - public static int TryGetIndex(this ref SmallList4 source, TItem it, TEq eq = default) - where TEq : struct, IEq - { - switch (source._count) - { - case 1: - if (eq.Equals(it, source._it0)) return 0; - break; +/// Stack with the Size information to check it at compile time +public interface IStack : IStack + where TSize : struct, ISize + where TStack : struct, IStack +{ +} - case 2: - if (eq.Equals(it, source._it0)) return 0; - if (eq.Equals(it, source._it1)) return 1; - break; +// todo: @wip possibly separate introduce IStackCap interface for the StackCap, because nothing in this interface requires the stack. +/// Abstracts over collection of the items on stack of the fixed Capacity, +/// to be used as a part of the hybrid data structures which grow from stack to heap +public interface IStack + where TStack : struct, IStack +{ + /// Maximum count of items hold on stack. Should be Power of Two + int Capacity { get; } - case 3: - if (eq.Equals(it, source._it0)) return 0; - if (eq.Equals(it, source._it1)) return 1; - if (eq.Equals(it, source._it2)) return 2; - break; + /// Returns the item by ref to read and write the item value, + /// but does not check the index bounds comparing to the `this[index]` + [UnscopedRef] + ref T GetSurePresentRef(int index); - default: - if (eq.Equals(it, source._it0)) return 0; - if (eq.Equals(it, source._it1)) return 1; - if (eq.Equals(it, source._it2)) return 2; - if (eq.Equals(it, source._it3)) return 3; - if (source._rest != null) - { - var count = source._count - SmallList4.StackItemCount; - var items = source._rest; - for (var i = 0; i < count; ++i) - { - ref var di = ref items[i]; // todo: @perf Marshall? - if (eq.Equals(it, di)) - return i + SmallList4.StackItemCount; - } - } - break; - } - return -1; - } + /// Indexer returning the item by ref to read and write the item value + [UnscopedRef] + ref T this[int index] { get; } - /// Returns the ref of the found item or appends the item to the end of the list, and returns ref to it - [MethodImpl((MethodImplOptions)256)] - public static int GetIndexOrAppend(this ref SmallList4 source, in TItem item, TEq eq) - where TEq : struct, IEq - { - switch (source._count) - { - case 0: - source._count = 1; - source._it0 = item; - return -1; +#if SUPPORTS_CREATE_SPAN + /// Creates a span over the stack items + public Span AsSpan(); +#endif +} - case 1: - if (eq.Equals(item, source._it0)) return 0; - source._count = 2; - source._it1 = item; - return -1; +/// Base marker for collection or container holding some number of items +public interface ISize +{ + /// Returns the size of the collection or container + int Size { get; } +} +/// Marker for collection or container holding 2 or items +public interface ISize2Plus : ISize { } +/// Marker for collection or container holding 4 or more items +public interface ISize4Plus : ISize2Plus { } +/// Marker for collection or container holding 8 or more items +public interface ISize8Plus : ISize4Plus { } +/// Marker for collection or container holding 16 or more items +public interface ISize16Plus : ISize8Plus { } + +/// Marker for collection or container holding 0 items +public struct Size0 : ISize +{ + /// + public int Size => 0; +} - case 2: - if (eq.Equals(item, source._it0)) return 0; - if (eq.Equals(item, source._it1)) return 1; - source._count = 3; - source._it2 = item; - return -1; +/// Marker for collection or container holding 4 items +public struct Size2 : ISize2Plus +{ + /// + public int Size => 2; +} +/// Marker for collection or container holding 4 items +public struct Size4 : ISize4Plus +{ + /// + public int Size => 4; +} +/// Marker for collection or container holding 8 items +public struct Size8 : ISize8Plus +{ + /// + public int Size => 8; +} +/// Marker for collection or container holding 16 items +public struct Size16 : ISize16Plus +{ + /// + public int Size => 16; +} - case 3: - if (eq.Equals(item, source._it0)) return 0; - if (eq.Equals(item, source._it1)) return 1; - if (eq.Equals(item, source._it2)) return 2; - source._count = 4; - source._it3 = item; - return -1; +/// Implementation of `IStack` for 2 items on stack +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct Stack2 : IStack> +{ + /// + public int Capacity => 2; - default: - if (eq.Equals(item, source._it0)) return 0; - if (eq.Equals(item, source._it1)) return 1; - if (eq.Equals(item, source._it2)) return 2; - if (eq.Equals(item, source._it3)) return 3; + internal T _it0, _it1; - var restCount = source._count - SmallList4.StackItemCount; - if (restCount != 0) - { - var items = source._rest; - for (var i = 0; i < restCount; ++i) - { - ref var di = ref items[i]; // todo: @perf Marshall? - if (eq.Equals(item, di)) - return i + SmallList4.StackItemCount; - } - } - AppendDefaultAndGetRef(ref source._rest, restCount, SmallList4.StackItemCount) = item; - ++source._count; - return -1; + /// + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] + public ref T GetSurePresentRef(int index) + { +#if SUPPORTS_UNSAFE + return ref Unsafe.Add(ref _it0, index); +#else + switch (index) + { + case 0: return ref _it0; + default: return ref _it1; } +#endif } - /// Returns surely present item ref by its index - [MethodImpl((MethodImplOptions)256)] - public static ref TItem GetSurePresentItemRef(this ref SmallList2 source, int index) + /// + [UnscopedRef] + public ref T this[int index] { - Debug.Assert(source.Count != 0); - Debug.Assert(index < source.Count); - switch (index) + [MethodImpl((MethodImplOptions)256)] + get { - case 0: return ref source._it0; - case 1: return ref source._it1; - default: - Debug.Assert(source._rest != null, $"Expecting deeper items are already existing on stack at index: {index}"); - return ref source._rest[index - SmallList2.StackItemCount]; + if (index >= 0 & index < Capacity) + return ref GetSurePresentRef(index); + return ref Stack.ThrowIndexOutOfBounds(index, Capacity); } } - /// Returns last present item ref, assumes that the list is not empty! +#if SUPPORTS_CREATE_SPAN + /// [MethodImpl((MethodImplOptions)256)] - public static ref TItem GetLastSurePresentItem(this ref SmallList2 source) => - ref source.GetSurePresentItemRef(source._count - 1); + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _it0, Capacity); +#endif +} - /// Returns the ref to tombstone indicating the missing item. - [MethodImpl((MethodImplOptions)256)] - public static ref TItem NotFound(this ref SmallList2 _) => ref SmallList2.Missing; +/// Implementation of `IStack` for 4 items on stack +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct Stack4 : IStack> +{ + /// + public int Capacity => 4; - /// Appends the default item to the end of the list and returns the reference to it. + internal T _it0, _it1, _it2, _it3; + + /// + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public static ref TItem AppendDefaultAndGetRef(this ref SmallList2 source) + public ref T GetSurePresentRef(int index) { - var index = source._count++; +#if SUPPORTS_UNSAFE + return ref Unsafe.Add(ref _it0, index); +#else switch (index) { - case 0: return ref source._it0; - case 1: return ref source._it1; - default: - return ref AppendDefaultAndGetRef(ref source._rest, index - SmallList2.StackItemCount, SmallList2.StackItemCount); + case 0: return ref _it0; + case 1: return ref _it1; + case 2: return ref _it2; + default: return ref _it3; } +#endif } - /// Looks for the item in the list and return its index if found or -1 for the absent item - [MethodImpl((MethodImplOptions)256)] - public static int TryGetIndex(this ref SmallList2 source, TItem it, TEq eq = default) - where TEq : struct, IEq + /// + [UnscopedRef] + public ref T this[int index] { - switch (source._count) + [MethodImpl((MethodImplOptions)256)] + get { - case 1: - if (eq.Equals(it, source._it0)) return 0; - break; - - default: - if (eq.Equals(it, source._it0)) return 0; - if (eq.Equals(it, source._it1)) return 1; - if (source._rest != null) - { - var count = source._count - SmallList2.StackItemCount; - var items = source._rest; - for (var i = 0; i < count; ++i) - { - ref var di = ref items[i]; // todo: @perf Marshall? - if (eq.Equals(it, di)) - return i + SmallList2.StackItemCount; - } - } - break; + if (index >= 0 & index < Capacity) + return ref GetSurePresentRef(index); + return ref Stack.ThrowIndexOutOfBounds(index, Capacity); } - return -1; } - /// Returns the ref of the found item or appends the item to the end of the list, and returns ref to it +#if SUPPORTS_CREATE_SPAN + /// [MethodImpl((MethodImplOptions)256)] - public static int GetIndexOrAppend(this ref SmallList2 source, TItem item, TEq eq) - where TEq : struct, IEq - { - switch (source._count) - { - case 0: - source._count = 1; - source._it0 = item; - return -1; - - case 1: - if (eq.Equals(item, source._it0)) return 0; - source._count = 2; - source._it1 = item; - return -1; - - default: - if (eq.Equals(item, source._it0)) return 0; - if (eq.Equals(item, source._it1)) return 1; - - var restCount = source._count - SmallList2.StackItemCount; - if (restCount != 0) - { - var items = source._rest; - for (var i = 0; i < restCount; ++i) - { - ref var di = ref items[i]; // todo: @perf Marshall? - if (eq.Equals(item, di)) - return i + SmallList2.StackItemCount; - } - } - AppendDefaultAndGetRef(ref source._rest, restCount, SmallList2.StackItemCount) = item; - ++source._count; - return -1; - } - } + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _it0, Capacity); +#endif } -/// List with the number of first items (4) stored inside its struct and the rest in the growable array. -/// Supports addition and removal (removel is without resize) only at the end of the list, aka Stack behavior -public struct SmallList4 +/// Implementation of `IStack` for 8 items on stack +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct Stack8 : IStack> { - /// The number of entries stored inside the map itself without moving them to array on heap - public const int StackItemCount = 4; - - // todo: @check what if someone stores something in it, it would be a memory leak, but isn't it the same as using `out var` in the returning`false` Try...methods? - internal static TItem Missing; // return the ref to Tombstone when nothing found + /// + public int Capacity => 8; - internal int _count; - internal TItem _it0, _it1, _it2, _it3; - internal TItem[] _rest; + internal T _it0, _it1, _it2, _it3, _it4, _it5, _it6, _it7; - /// Gets the number of items in the list - public int Count + /// + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] + public ref T GetSurePresentRef(int index) { - [MethodImpl((MethodImplOptions)256)] - get => _count; +#if SUPPORTS_UNSAFE + return ref Unsafe.Add(ref _it0, index); +#else + switch (index) + { + case 0: return ref _it0; + case 1: return ref _it1; + case 2: return ref _it2; + case 3: return ref _it3; + case 4: return ref _it4; + case 5: return ref _it5; + case 6: return ref _it6; + default: return ref _it7; + } +#endif } - /// Returns surely present item by its index - public TItem this[int index] + /// + [UnscopedRef] + public ref T this[int index] { [MethodImpl((MethodImplOptions)256)] get { - Debug.Assert(_count != 0); - Debug.Assert(index < _count); - switch (index) - { - case 0: return _it0; - case 1: return _it1; - case 2: return _it2; - case 3: return _it3; - default: - Debug.Assert(_rest != null, $"Expecting deeper items are already existing on stack at index: {index}"); - return _rest[index - StackItemCount]; - } + if (index >= 0 & index < Capacity) + return ref GetSurePresentRef(index); + return ref Stack.ThrowIndexOutOfBounds(index, Capacity); } } - /// Adds the item to the end of the list aka the Stack.Push +#if SUPPORTS_CREATE_SPAN + /// [MethodImpl((MethodImplOptions)256)] - public void Append(in TItem item) - { - var index = _count++; - switch (index) - { - case 0: _it0 = item; break; - case 1: _it1 = item; break; - case 2: _it2 = item; break; - case 3: _it3 = item; break; - default: - SmallList.AppendDefaultAndGetRef(ref _rest, index - StackItemCount, StackItemCount) = item; - break; - } - } + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _it0, Capacity); +#endif +} - /// Bridge to go from the List.Add - [MethodImpl((MethodImplOptions)256)] - public void Add(in TItem item) => Append(in item); +/// Implementation of `IStack` for 16 items on stack +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct Stack16 : IStack> +{ + /// + public int Capacity => 16; - /// Adds the default item to the end of the list aka the Stack.Push default + internal T _it0, _it1, _it2, _it3, _it4, _it5, _it6, _it7; + internal T _it8, _it9, _it10, _it11, _it12, _it13, _it14, _it15; + + /// + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public void AppendDefault() + public ref T GetSurePresentRef(int index) { - if (++_count >= StackItemCount) - SmallList.AppendDefaultAndGetRef(ref _rest, _count - StackItemCount, StackItemCount); +#if SUPPORTS_UNSAFE + return ref Unsafe.Add(ref _it0, index); +#else + switch (index) + { + case 0: return ref _it0; + case 1: return ref _it1; + case 2: return ref _it2; + case 3: return ref _it3; + case 4: return ref _it4; + case 5: return ref _it5; + case 6: return ref _it6; + case 7: return ref _it7; + case 8: return ref _it8; + case 9: return ref _it9; + case 10: return ref _it10; + case 11: return ref _it11; + case 12: return ref _it12; + case 13: return ref _it13; + case 14: return ref _it14; + default: return ref _it15; + } +#endif } - /// Removes the last item from the list aka the Stack Pop. Assumes that the list is not empty! - [MethodImpl((MethodImplOptions)256)] - public void RemoveLastSurePresentItem() + /// + [UnscopedRef] + public ref T this[int index] { - Debug.Assert(Count != 0); - var index = --_count; - switch (index) + [MethodImpl((MethodImplOptions)256)] + get { - case 0: _it0 = default; break; - case 1: _it1 = default; break; - case 2: _it2 = default; break; - case 3: _it3 = default; break; - default: - Debug.Assert(_rest != null, $"Expecting a deeper parent stack created before accessing it here at level {index}"); - _rest[index - StackItemCount] = default; - break; + if (index >= 0 & index < Capacity) + return ref GetSurePresentRef(index); + return ref Stack.ThrowIndexOutOfBounds(index, Capacity); } } + +#if SUPPORTS_CREATE_SPAN + /// + [MethodImpl((MethodImplOptions)256)] + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _it0, Capacity); +#endif } -/// List with the number of first items (2) stored inside its struct and the rest in the growable array. -/// Supports addition and removal (removel is without resize) only at the end of the list, aka Stack behavior -public struct SmallList2 +/// Generic version of SmallList abstracted for how much items are on the stack +public struct SmallList : IEnumerable + where TStack : struct, IStack { - /// The number of entries stored inside the map itself without moving them to array on heap - public const int StackItemCount = 2; + /// Let's enable access to the Count, so you can Pop the item by --list.Count. Just don't forget to nullify the popped item if needed + public int Count; - // todo: @check what if someone stores something in it, it would be a memory leak, but isn't it the same as using `out var` in the returning`false` Try...methods? - internal static TItem Missing; // return the ref to Tombstone when nothing found +#pragma warning disable CS0649 // it is fine `Stack` is never assigned to, and will always have its default value + /// Let's enable access to the stack, just know what's you doing + public TStack Stack; +#pragma warning restore CS0649 - internal int _count; - internal TItem _it0, _it1; - internal TItem[] _rest; + /// Exposes the rest on the heap + public T[] Rest; - /// Good stuff + /// Ensures that the list has allocated space to hold `count` of items [MethodImpl((MethodImplOptions)256)] public void InitCount(int count) { - _count = count; - if (count > StackItemCount) - _rest = new TItem[count - StackItemCount]; + Debug.Assert(count > 0, "Count should be more than 0"); + Debug.Assert(Count == 0, "Initial the count should be 0"); + + // Add the StackCapacity empty space at the end, we may use it later for BuildToArray. + // The actual source Capacity will be StackCapacity + count. + if (count > Stack.Capacity) + Rest = new T[count]; + Count = count; } - /// Good stiff - [MethodImpl((MethodImplOptions)256)] - public void Init(TItem it0) + /// Returns surely present item by ref + [UnscopedRef] + public ref T this[int index] { - _count = 1; - _it0 = it0; + [MethodImpl((MethodImplOptions)256)] + get + { + if (index < 0 | index >= Count) + return ref SmallList.ThrowIndexOutOfBounds(index, Count); + + var stackCap = Stack.Capacity; + if (index < stackCap) + return ref Stack.GetSurePresentRef(index); + + Debug.Assert(Rest != null, "Expecting deeper items are already existing on heap"); + return ref Rest.GetSurePresentRef(index - stackCap); + } } - /// Good steff + /// Returns a surely present item ref by its index + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public void Init(TItem it0, TItem it1) + public ref T GetSurePresentRef(int index) { - _count = 2; - _it0 = it0; - _it1 = it1; + Debug.Assert(Count != 0, "SmallList.GetSurePresentItemRef: list should not be empty"); + Debug.Assert(index >= 0 & index < Count, $"SmallList.GetSurePresentItemRef: index {index} should be less than Count {Count}"); + + var stackCap = Stack.Capacity; + if (index < stackCap) + return ref Stack.GetSurePresentRef(index); + + Debug.Assert(Rest != null); + return ref Rest[index - stackCap]; } - /// Good staff + /// Appends the default item to the end of the list and returns the reference to it. + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public void Init(TItem it0, TItem it1, params TItem[] rest) + public ref T AddDefaultAndGetRef() { - _count = 2 + rest.Length; - _it0 = it0; - _it1 = it1; - _rest = rest; + var index = Count++; + var stackCap = Stack.Capacity; + if (index < stackCap) + return ref Stack.GetSurePresentRef(index); + return ref SmallList.AddDefaultAndGetRef(ref Rest, index - stackCap); } - /// Good styff - public void Init(params TItem[] items) + /// Adds the item to the end of the list aka the Stack.Push. Returns the index of the added item. + [MethodImpl((MethodImplOptions)256)] + public int Add(in T item) { - switch (items.Length) - { - case 0: - break; - case 1: - Init(items[0]); - break; - case 2: - Init(items[0], items[1]); - break; - default: - _count = items.Length; - _it0 = items[0]; - _it1 = items[1]; - for (var i = StackItemCount; i < items.Length; ++i) - items[i - StackItemCount] = items[i]; - _rest = items; - break; - } + var index = Count++; + var stackCap = Stack.Capacity; + if (index < stackCap) + Stack.GetSurePresentRef(index) = item; + else + SmallList.AddDefaultAndGetRef(ref Rest, index - stackCap) = item; + return index; } - /// Good staff - public void Init(TList items) where TList : IReadOnlyList + /// Looks for the item in the list and return its index if found or -1 for the absent item + [MethodImpl((MethodImplOptions)256)] + public int TryGetIndex(in T item, TEq eq = default) where TEq : struct, IEq { - if (items is TItem[] arr) + if (Count != 0) { - Init(arr); - return; - } - switch (items.Count) - { - case 0: - break; - case 1: - Init(items[0]); - break; - case 2: - Init(items[0], items[1]); - break; - default: - var count = items.Count; - var rest = new TItem[count - StackItemCount]; - _count = count; - _it0 = items[0]; - _it1 = items[1]; - for (var i = StackItemCount; i < count; ++i) - rest[i - StackItemCount] = items[i]; - _rest = rest; - break; + var index = 0; + foreach (var it in this) + { + if (eq.Equals(item, it)) + return index; + ++index; + } } + return -1; } - /// Gets the number of items in the list - public int Count + /// Returns the index of the found item or appends the item to the end of the list, and returns its index + [MethodImpl((MethodImplOptions)256)] + public int GetIndexOrAdd(in T item, TEq eq = default) where TEq : struct, IEq { - [MethodImpl((MethodImplOptions)256)] - get => _count; + var i = TryGetIndex(in item, eq); + return i != -1 ? i : Add(in item); } - /// Returns surely present item by its index - public TItem this[int index] + ///Clears the list, but keeps the already allocated array on heap to reuse in the future + [MethodImpl((MethodImplOptions)256)] + public void Clear() { - [MethodImpl((MethodImplOptions)256)] - get + Stack = default; // todo: @perf is there way to faster clear items on stack? + var restCount = Count - Stack.Capacity; + if (restCount > 0) { - Debug.Assert(_count != 0); - Debug.Assert(index < _count); - switch (index) - { - case 0: return _it0; - case 1: return _it1; - default: - Debug.Assert(_rest != null, $"Expecting deeper items are already existing on stack at index: {index}"); - return _rest[index - StackItemCount]; - } + Debug.Assert(Rest != null, "Expecting deeper items are already existing on heap"); + Array.Clear(Rest, 0, restCount); } + Count = 0; } - /// Adds the item to the end of the list aka the Stack.Push + /// Returns last present item ref, assumes that the list is not empty! + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public void Append(in TItem item) + public ref T GetLastSurePresentItem() { - var index = _count++; - switch (index) - { - case 0: _it0 = item; break; - case 1: _it1 = item; break; - default: - SmallList.AppendDefaultAndGetRef(ref _rest, index - StackItemCount, StackItemCount) = item; - break; - } + Debug.Assert(Count != 0, "Expecting that the list is not empty"); + return ref GetSurePresentRef(Count - 1); } - /// Bridge to go from the List.Add + /// Removes the last item from the list aka the Stack Pop. Assumes that the list is not empty! [MethodImpl((MethodImplOptions)256)] - public void Add(in TItem item) => Append(in item); + public void RemoveLastSurePresentItem() + { + Debug.Assert(Count != 0, "SmallList.RemoveLastSurePresentItem: Expecting that the list is not empty"); + GetSurePresentRef(Count - 1) = default; + --Count; + } - /// Adds the default item to the end of the list aka the Stack.Push default + /// Returns an enumerator struct [MethodImpl((MethodImplOptions)256)] - public void AppendDefault() + public SmallListEnumerator GetEnumerator() => new SmallListEnumerator(this); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} + +/// Enumerator on stack, without allocations +public struct SmallListEnumerator : IEnumerator, IEnumerator + where TStack : struct, IStack +{ + private readonly SmallList _list; + private int _index; + internal SmallListEnumerator(SmallList list) { - if (++_count >= StackItemCount) - SmallList.AppendDefaultAndGetRef(ref _rest, _count - StackItemCount, StackItemCount); + _list = list; + _index = -1; } - - /// Removes the last item from the list aka the Stack Pop. Assumes that the list is not empty! + private T _current; + /// + public T Current => _current; + object IEnumerator.Current => _current; + /// [MethodImpl((MethodImplOptions)256)] - public void RemoveLastSurePresentItem() + public bool MoveNext() { - Debug.Assert(Count != 0); - var index = --_count; - switch (index) + var index = ++_index; + if (index < _list.Count) { - case 0: _it0 = default; break; - case 1: _it1 = default; break; - default: - Debug.Assert(_rest != null, $"Expecting a deeper parent stack created before accessing it here at level {index}"); - _rest[index - StackItemCount] = default; - break; + _current = index < _list.Stack.Capacity + ? _list.Stack.GetSurePresentRef(index) + : _list.Rest[index - _list.Stack.Capacity]; + return true; } + return false; } + /// + public void Reset() => _index = -1; + /// + public void Dispose() { } +} - /// Copy items to new the array +internal static class BitOps +{ + /// Gets the next power of two greater than or equal to the specified capacity. [MethodImpl((MethodImplOptions)256)] - public TItem[] CopyToArray() + public static uint NextPowerOfTwo(uint value) { - switch (Count) - { - case 0: return Array.Empty(); - case 1: return new[] { _it0 }; - case 2: return new[] { _it0, _it1 }; - default: - var items = new TItem[Count]; - items[0] = _it0; - items[1] = _it1; - Array.Copy(_rest, 0, items, 2, Count - StackItemCount); - return items; - } +#if NET8_0_OR_GREATER + return BitOperations.RoundUpToPowerOf2(value); +#else + --value; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + return value + 1; +#endif } - /// Exposing as list + /// Counts the number of leading zero bits in the specified value. + /// Useful to get Log2(value) and the number of bits needed to represent the value. [MethodImpl((MethodImplOptions)256)] - public IList AsList() => CopyToArray(); + public static byte LeadingZeroCount(uint value) + { +#if NET8_0_OR_GREATER + return (byte)BitOperations.LeadingZeroCount(value); +#else + if (value == 0) return 32; + var count = 0; + if (value <= 0x0000FFFF) { count += 16; value <<= 16; } + if (value <= 0x00FFFFFF) { count += 8; value <<= 8; } + if (value <= 0x0FFFFFFF) { count += 4; value <<= 4; } + if (value <= 0x3FFFFFFF) { count += 2; value <<= 2; } + if (value <= 0x7FFFFFFF) { count += 1; } + return (byte)count; +#endif + } } -/// Configiration and the tools for the FHashMap map data structure -public static class FHashMap +public interface ICapIndex { - /// 2^32 / phi for the Fibonacci hashing, where phi is the golden ratio ~1.61803 - public const uint GoldenRatio32 = 2654435769; + /// The capacity + public int Capacity { get; } - internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) - internal const byte MinHashesCapacityBitShift = 4; // 1 << 3 == 8 + /// Returns the item by ref to read and write the item value, + /// but does not check the index bounds comparing to the `this[index]` + [UnscopedRef] + ref T GetSurePresentRef(int index); +} - /// Upper hash bits spent on storing the probes, e.g. 5 bits mean 31 probes max. - public const byte MaxProbeBits = 5; - internal const byte MaxProbeCount = (1 << MaxProbeBits) - 1; - internal const byte ProbeCountShift = 32 - MaxProbeBits; - internal const int HashAndIndexMask = ~(MaxProbeCount << ProbeCountShift); +/// Abstraction to configure your own entries data structure. Check the derived types for the examples +public interface IGrowingArray : ICapIndex +{ + /// Initializes the entries storage to the specified capacity + void Init(uint capacity); - /// The number of entries stored inside the map itself without moving them to array on heap - public const int StackEntriesCount = 4; + /// Initializes the entries storage to the specified capacity without checking the capacity. + void InitDoNotCheckCapacity(uint capacityPowerOfTwo); - /// Creates the map with the storage - [MethodImpl((MethodImplOptions)256)] - public static FHashMap> New(byte capacityBitShift = 0) - where TEq : struct, IEq => new(capacityBitShift); + /// Doubles the current capacity of the array preserving the existing elements and count. + void DoubleCapacity(); - /// Holds a single entry consisting of key and value. - /// Value may be set or changed but the key is set in stone (by construction). - [DebuggerDisplay("{Key?.ToString()}->{Value}")] - public struct Entry + /// Doubles the capacity until the specified value is inside + void GrowCapacity(uint capacity); +} + +/// Stores the entries in a single dynamically reallocated growing array +public struct SingleBackingArray : IGrowingArray +{ + internal int _capacityPowerOfTwo; + internal T[] _entries; + + /// + public int Capacity => _capacityPowerOfTwo; + + /// + [MethodImpl((MethodImplOptions)256)] + public void Init(uint capacity) + { + _capacityPowerOfTwo = capacity == 0 ? 1 : (int)BitOps.NextPowerOfTwo(capacity); + _entries = new T[_capacityPowerOfTwo]; + } + + /// + [MethodImpl((MethodImplOptions)256)] + public void InitDoNotCheckCapacity(uint capacityPowerOfTwo) + { + _capacityPowerOfTwo = (int)capacityPowerOfTwo; + _entries = new T[capacityPowerOfTwo]; + } + + /// + [MethodImpl((MethodImplOptions)256)] + public void DoubleCapacity() + { + Debug.Assert(_entries != null, "Entries should be initialized before doubling the capacity"); + Debug.Assert(_capacityPowerOfTwo != 0, "Capacity should be not 0"); + Array.Resize(ref _entries, _capacityPowerOfTwo <<= 1); + } + + /// + public void GrowCapacity(uint capacity) + { + if (_capacityPowerOfTwo == 0) + { + Init(capacity); + return; + } + while (_capacityPowerOfTwo < capacity) + DoubleCapacity(); + } + + /// + [MethodImpl((MethodImplOptions)256)] + public ref T GetSurePresentRef(int index) + { + Debug.Assert(index >= 0 && index < _capacityPowerOfTwo, $"Index {index} should be in the range of 0 to capacity:{_capacityPowerOfTwo}"); + return ref _entries.GetSurePresentRef(index); + } +} + +// todo: @perf do we actually need the power of 2 size? For example, it is not used for the SmallMap meta/hashes anyway because it is slow +/// A growing array that grows by appending a new segment doubling the total capacity. +/// Existing segments are never resized/copied, so item references stay valid (stable). +/// Random access is still O(1) via power-of-two segment sizes. +/// Segment sizes starting from first segment F: 0:F, 1:F, 2:2F, 3:4F, 4:8F, ... (first two equal so total doubles each add). +/// +public struct GrrPowerOf2 : IGrowingArray, IEnumerable + where TSegmentsStack : struct, IStack +{ + public const int MinCapacityAndFirstSegmentSize = 16; + + internal int _totalCapacityPowerOfTwo; + /// Cached first segment length (power of two). Avoids recomputing 1 << bitIndex on the hot path. + internal int _firstSegmentCapacity; + /// helps to calculate the segment index by subtracting the leading zero count of the global index from this value. + internal byte _firstSegmentCapacityLeadingZerosPlusOne; + + internal SmallList _segments; + + /// The number of segments. + public int SegmentCount => _segments.Count; + + /// + public int Capacity => _totalCapacityPowerOfTwo; + + /// Creates the first segment with capacity rounded up to the next power-of-two (min 16). + /// Safe to call again to re-initialize (discards previous segments). + [MethodImpl((MethodImplOptions)256)] + public void Init(uint capacity) + { + var firstSegmentCap = capacity <= MinCapacityAndFirstSegmentSize + ? MinCapacityAndFirstSegmentSize + : (int)BitOps.NextPowerOfTwo(capacity); + _totalCapacityPowerOfTwo = firstSegmentCap; + _firstSegmentCapacity = firstSegmentCap; + _firstSegmentCapacityLeadingZerosPlusOne = (byte)(1 + BitOps.LeadingZeroCount((uint)firstSegmentCap)); + // Reset so re-Init works (SmallList.InitCount asserts Count == 0) + _segments = default; + _segments.InitCount(1); + _segments.GetSurePresentRef(0) = new T[firstSegmentCap]; + } + + /// + [MethodImpl((MethodImplOptions)256)] + public void InitDoNotCheckCapacity(uint capacityPowerOfTwo) + { + _totalCapacityPowerOfTwo = (int)capacityPowerOfTwo; // default is 16 == 0x0...010000 + _firstSegmentCapacity = (int)capacityPowerOfTwo; + _firstSegmentCapacityLeadingZerosPlusOne = (byte)(1 + BitOps.LeadingZeroCount(capacityPowerOfTwo)); // 1 + (32 - 5) == 28 for capacityPowerOfTwo == 16 + + // Reset so re-Init works + _segments = default; + _segments.Count = 1; + _segments.GetSurePresentRef(0) = new T[capacityPowerOfTwo]; + } + + /// + [MethodImpl((MethodImplOptions)256)] + public void DoubleCapacity() + { + Debug.Assert(_segments.Count != 0, + $"Segments should be initialized before doubling the capacity but found {_segments.Count} segments"); + Debug.Assert(_totalCapacityPowerOfTwo != 0, "Capacity should be non-zero before doubling"); + // New segment size == current total capacity, so total doubles and old segments stay untouched. + _segments.AddDefaultAndGetRef() = new T[_totalCapacityPowerOfTwo]; + _totalCapacityPowerOfTwo <<= 1; + } + + /// + [MethodImpl((MethodImplOptions)256)] + public void GrowCapacity(uint capacity) + { + if (_totalCapacityPowerOfTwo == 0) + { + Init(capacity); + return; + } + while ((uint)_totalCapacityPowerOfTwo < capacity) + DoubleCapacity(); + } + + /// + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] + public ref T GetSurePresentRef(int index) + { + Debug.Assert(_totalCapacityPowerOfTwo != 0, "Grr should be initialized before accessing items"); + Debug.Assert(index >= 0 && index < _totalCapacityPowerOfTwo, + $"Index {index} should be in the range of 0 to capacity:{_totalCapacityPowerOfTwo}"); + + if (index < _firstSegmentCapacity) + return ref _segments.GetSurePresentRef(0).GetSurePresentRef(index); + + // Example A: input index = 17, first segment capacity = 16 + // 28(_firstSegmentCapacityLeadingZerosPlusOne) - 27(LeadingZeroCount(17)) == 1 + // Example B: input index = 33, first segment capacity = 16 + // 28(_firstSegmentCapacityLeadingZerosPlusOne) - 26(LeadingZeroCount(31)) == 2 + var segmentIndex = _firstSegmentCapacityLeadingZerosPlusOne - BitOps.LeadingZeroCount((uint)index); + + Debug.Assert(segmentIndex >= 1 && segmentIndex < _segments.Count, + $"segmentIndex {segmentIndex} out of range for index:{index}, segmentCount:{_segments.Count}"); + + // Prefix capacity before segment S (S >= 1) is firstCap << (S - 1) == total capacity of segments 0..S-1 + var insideIndex = index - (_firstSegmentCapacity << (segmentIndex - 1)); + return ref _segments.GetSurePresentRef(segmentIndex).GetSurePresentRef(insideIndex); + } + + /// Returns an enumerator over all allocated slots (Capacity items, not a separate Count). + [MethodImpl((MethodImplOptions)256)] + public GrrPowerOf2Enumerator GetEnumerator() => new GrrPowerOf2Enumerator(_segments); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} + + +/// Enumerator over Grr segment slots (full segment arrays, including unused capacity tails). +public struct GrrPowerOf2Enumerator : IEnumerator, IEnumerator + where TStack : struct, IStack +{ + private T _current; + private T[] _currentSegment; + private int _segmentIndex; + private int _insideIndex; + private SmallList _segments; + + internal GrrPowerOf2Enumerator(SmallList segments) + { + _current = default; + _segmentIndex = 0; + _insideIndex = -1; + _segments = segments; + _currentSegment = segments.Count == 0 ? null : segments.GetSurePresentRef(0); + } + + /// + public T Current => _current; + object IEnumerator.Current => _current; + + /// + [MethodImpl((MethodImplOptions)256)] + public bool MoveNext() + { + if (_currentSegment == null) + return false; + + if (++_insideIndex >= _currentSegment.Length) + { + if (++_segmentIndex >= _segments.Count) + { + _currentSegment = null; + return false; + } + _currentSegment = _segments.GetSurePresentRef(_segmentIndex); + Debug.Assert(_currentSegment != null, + $"New current segment should not be null for segmentIndex:{_segmentIndex} < _segments.Count:{_segments.Count}"); + _insideIndex = 0; + } + + _current = _currentSegment.GetSurePresentRef(_insideIndex); + return true; + } + + /// + public void Reset() + { + _current = default; + _segmentIndex = 0; + _insideIndex = -1; + // Keep _segments so Reset+MoveNext can restart over the same data + _currentSegment = _segments.Count == 0 ? null : _segments.GetSurePresentRef(0); + } + + /// + public void Dispose() { } +} + +/// +/// Hybrid growable list: first items live on a fixed , +/// overflow lives in a stable segmented (no resize/copy of old heap items). +/// API mirrors but heap growth keeps references into already-added heap items stable. +/// +public struct SmallGrrPowerOf2 : IEnumerable + where TStack : struct, IStack + where TSegStack : struct, IStack +{ + /// Number of used items. May decrement to pop if you clear the slot yourself when needed. + public int Count; + +#pragma warning disable CS0649 + /// Inline/stack portion of the list. + public TStack Stack; +#pragma warning restore CS0649 + + /// Heap overflow stored as stable segments. Valid indexes are 0 .. Count - Stack.Capacity - 1. + public GrrPowerOf2 Rest; + + /// Indexer with bounds checks. + [UnscopedRef] + public ref T this[int index] + { + [MethodImpl((MethodImplOptions)256)] + get + { + if (index < 0 | index >= Count) + return ref SmallList.ThrowIndexOutOfBounds(index, Count); + return ref GetSurePresentRef(index); + } + } + + /// Returns a surely present item by index (no bounds check beyond asserts). + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] + public ref T GetSurePresentRef(int index) + { + Debug.Assert(index >= 0 & index < Count, $"SmallGrr index {index} should be in [0, {Count})"); + var stackCap = Stack.Capacity; + if (index < stackCap) + return ref Stack.GetSurePresentRef(index); + return ref Rest.GetSurePresentRef(index - stackCap); + } + + /// Appends a default item and returns a ref to it. + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] + public ref T AddDefaultAndGetRef() + { + var index = Count++; + var stackCap = Stack.Capacity; + if (index < stackCap) + return ref Stack.GetSurePresentRef(index); + + var restIndex = index - stackCap; + if (restIndex >= Rest.Capacity) + Rest.GrowCapacity((uint)restIndex + 1); + return ref Rest.GetSurePresentRef(restIndex); + } + + /// Appends an item. Returns the index of the added item. + [MethodImpl((MethodImplOptions)256)] + public int Add(in T item) + { + AddDefaultAndGetRef() = item; + return Count - 1; + } + + /// Ensures capacity for at least items (stack + rest). + [MethodImpl((MethodImplOptions)256)] + public void EnsureCapacity(int count) + { + Debug.Assert(count >= 0); + var stackCap = Stack.Capacity; + if (count <= stackCap) + return; + Rest.GrowCapacity((uint)(count - stackCap)); + } + + /// Pre-sizes Count and backing storage (like ). + [MethodImpl((MethodImplOptions)256)] + public void InitCount(int count) + { + Debug.Assert(count > 0, "Count should be more than 0"); + Debug.Assert(Count == 0, "Initial the count should be 0"); + EnsureCapacity(count); + Count = count; + } + + /// Looks for the item and returns its index, or -1. + [MethodImpl((MethodImplOptions)256)] + public int TryGetIndex(in T item, TEq eq = default) where TEq : struct, IEq + { + for (var i = 0; i < Count; ++i) + { + if (eq.Equals(item, GetSurePresentRef(i))) + return i; + } + return -1; + } + + /// Returns index of existing item or appends and returns the new index. + [MethodImpl((MethodImplOptions)256)] + public int GetIndexOrAdd(in T item, TEq eq = default) where TEq : struct, IEq + { + var i = TryGetIndex(in item, eq); + return i != -1 ? i : Add(in item); + } + + /// Clears count; does not free heap segments (they can be reused). + [MethodImpl((MethodImplOptions)256)] + public void Clear() + { + var stackCap = Stack.Capacity; + var n = Count < stackCap ? Count : stackCap; + for (var i = 0; i < n; ++i) + Stack.GetSurePresentRef(i) = default; + // Leave Rest segments allocated for reuse; clear used rest slots + var restCount = Count - stackCap; + if (restCount > 0) + { + for (var i = 0; i < restCount; ++i) + Rest.GetSurePresentRef(i) = default; + } + Count = 0; + } + + /// Last item ref; list must be non-empty. + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] + public ref T GetLastSurePresentItem() + { + Debug.Assert(Count != 0, "Expecting that the list is not empty"); + return ref GetSurePresentRef(Count - 1); + } + + /// Removes the last item. List must be non-empty. + [MethodImpl((MethodImplOptions)256)] + public void RemoveLastSurePresentItem() + { + Debug.Assert(Count != 0, "SmallGrr.RemoveLastSurePresentItem: list should not be empty"); + GetSurePresentRef(Count - 1) = default; + --Count; + } + + /// Struct enumerator over used items only (Count). + [MethodImpl((MethodImplOptions)256)] + public SmallGrrEnumerator GetEnumerator() => + new SmallGrrEnumerator(this); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} + +/// Enumerator for over used items. +public struct SmallGrrEnumerator : IEnumerator, IEnumerator + where TStack : struct, IStack + where TSegStack : struct, IStack +{ + private SmallGrrPowerOf2 _list; + private int _index; + private T _current; + + internal SmallGrrEnumerator(SmallGrrPowerOf2 list) + { + _list = list; + _index = -1; + _current = default; + } + + /// + public T Current => _current; + object IEnumerator.Current => _current; + + /// + [MethodImpl((MethodImplOptions)256)] + public bool MoveNext() + { + var index = ++_index; + if (index < _list.Count) + { + _current = _list.GetSurePresentRef(index); + return true; + } + return false; + } + + /// + public void Reset() => _index = -1; + + /// + public void Dispose() { } +} + +/// Printable thing via provided printer +public interface IPrintable +{ + /// Print to the provided string builder via the provided printer. + StringBuilder Print(StringBuilder s, Func printer); +} + +/// Produces good enough hash codes for the fields +public static class Hasher +{ + /// Combines hashes of two fields + public static int Combine(T1 a, T2 b) + { + var bh = b?.GetHashCode() ?? 0; + if (ReferenceEquals(a, null)) + return bh; + var ah = a.GetHashCode(); + if (ah == 0) + return bh; + return Combine(ah, bh); + } + + /// Inspired by System.Tuple.CombineHashCodes + [MethodImpl((MethodImplOptions)256)] + public static int Combine(int h1, int h2) + { + unchecked + { + return (h1 << 5) + h1 ^ h2; + } + } +} + +/// Configures removed key tombstone, equality and hash function for the SmallMap and friends +public interface IEq : IEqualityComparer +{ + /// Defines the value of the key indicating the removed entry + K GetTombstone(); +} + +/// Default comparer using the `object.GetHashCode` and `object.Equals` overloads +public struct DefaultEq : IEq +{ + /// + [MethodImpl((MethodImplOptions)256)] + public K GetTombstone() => default; + + /// + [MethodImpl((MethodImplOptions)256)] + public bool Equals(K x, K y) => ReferenceEquals(x, y) || x.Equals(y); + + /// + [MethodImpl((MethodImplOptions)256)] + public int GetHashCode(K key) => key.GetHashCode(); +} + +/// Uses the integer itself as hash code and `==` for equality +public struct IntEq : IEq +{ + /// + [MethodImpl((MethodImplOptions)256)] + public int GetTombstone() => int.MinValue; // todo: @improve separate the tombstone from the hash + + /// + [MethodImpl((MethodImplOptions)256)] + public bool Equals(int x, int y) => x == y; + + /// + [MethodImpl((MethodImplOptions)256)] + public int GetHashCode(int key) => key; +} + +/// General tools for IEq and its implementations +public static class Eq +{ + /// Factor to multiple by, to achieve the Fibonacci hashing (2^32 / phi, where phi is the golden ratio ~1.61803). + public const uint GoldenRatioFactor32 = 2654435769; +} + +/// Instances of the RefEq for the often used K +public static class RefEq +{ + /// RefEq for object, possibly the only thing you need + public static readonly RefEq OfObject = default; +} + +/// Uses the `RuntimeHelpers.GetHashCode` and `object.ReferenceEquals` +public struct RefEq : IEq where K : class +{ + /// + [MethodImpl((MethodImplOptions)256)] + public K GetTombstone() => null; + + /// + [MethodImpl((MethodImplOptions)256)] + public bool Equals(K x, K y) => ReferenceEquals(x, y); + + /// + [MethodImpl((MethodImplOptions)256)] + public int GetHashCode(K key) => RuntimeHelpers.GetHashCode(key); +} + +/// Uses the `RuntimeHelpers.GetHashCode` and hashes the result with Fibonacci hashing via * GoldenRatio +public struct GoldenRatioRefEq : IEq where K : class +{ + /// + [MethodImpl((MethodImplOptions)256)] + public K GetTombstone() => null; + + /// + [MethodImpl((MethodImplOptions)256)] + public bool Equals(K x, K y) => ReferenceEquals(x, y); + + /// + [MethodImpl((MethodImplOptions)256)] + public int GetHashCode(K key) => (int)(RuntimeHelpers.GetHashCode(key) * Eq.GoldenRatioFactor32); +} + +/// Compares via `ReferenceEquals` and gets the hash faster via `RuntimeHelpers.GetHashCode` +public struct RefEq : IEq<(A, B)> + where A : class + where B : class +{ + /// + [MethodImpl((MethodImplOptions)256)] + public (A, B) GetTombstone() => (null, null); + + /// + [MethodImpl((MethodImplOptions)256)] + public bool Equals((A, B) x, (A, B) y) => + ReferenceEquals(x.Item1, y.Item1) && ReferenceEquals(x.Item2, y.Item2); + + /// + [MethodImpl((MethodImplOptions)256)] + public int GetHashCode((A, B) key) => + Hasher.Combine(RuntimeHelpers.GetHashCode(key.Item1), RuntimeHelpers.GetHashCode(key.Item2)); +} + +/// Compares via `ReferenceEquals` and gets the hash faster via `RuntimeHelpers.GetHashCode` +public struct RefEq : IEq<(A, B, C)> + where A : class + where B : class + where C : class +{ + /// + [MethodImpl((MethodImplOptions)256)] + public (A, B, C) GetTombstone() => (null, null, null); + + /// + [MethodImpl((MethodImplOptions)256)] + public bool Equals((A, B, C) x, (A, B, C) y) => + ReferenceEquals(x.Item1, y.Item1) && ReferenceEquals(x.Item2, y.Item2) && ReferenceEquals(x.Item3, y.Item3); + + /// + [MethodImpl((MethodImplOptions)256)] + public int GetHashCode((A, B, C) key) => + Hasher.Combine(RuntimeHelpers.GetHashCode(key.Item1), Hasher.Combine(RuntimeHelpers.GetHashCode(key.Item2), RuntimeHelpers.GetHashCode(key.Item3))); +} + +/// Add the Use parameter to `Method{T}(..., Use{T} _)` to enable type inference for T, +/// by calling it as `var t = Method(..., Use{T}.It)` +public readonly struct Pass +{ + public static readonly Pass It = new(); + public readonly A Aval; +} + +/// Add the Pass parameter in type inference for A, B +public readonly struct Pass +{ + public static readonly Pass It = new(); + public readonly A Aval; + public readonly B Bval; +} + +/// Add the Pass parameter in type inference for A, B, C +public readonly struct Pass +{ + public static readonly Pass It = new(); + public readonly A Aval; + public readonly B Bval; + public readonly C Cval; +} + +/// Configuration and the tools for the SmallMap and friends +public static class SmallMap +{ + internal const uint MinCapacity = 16; + internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) + internal const int EntryIndexStartAtBit = 32; // higher bits for the entry index + internal const long HashAndProbeMask = (1L << EntryIndexStartAtBit) - 1; // lower 32 bits + + /// Represent a keyed entry stored in the SmallMap. + /// Its implementation struct may include the additional Value for the Map or just the Key for the Set. + /// The implementation may also decide to make Value readonly or writable for the in-place update + public interface IEntry + { + /// Returns the key of the payload + K Key { get; set; } + } + + /// The entry with just a key. + /// When used in the SmallMap it may represent a Set without wasting the space for the absent value + [DebuggerDisplay("{Key?.ToString()}")] + public struct Entry : IEntry { /// The readonly key - public K Key; + public K Key { get; set; } + /// Construct with the key and default value + public Entry(K key) => Key = key; + } + + /// Holds an entry consisting of key and value. + [DebuggerDisplay("{Key?.ToString()}:{Value}")] + public struct Entry : IEntry + { + /// The readonly key + public K Key { get; set; } /// The mutable value public V Value; /// Construct with the key and default value @@ -712,646 +1426,834 @@ public Entry(K key, V value) } } - /// binary reprsentation of the `int` - public static string ToB(int x) => Convert.ToString(x, 2).PadLeft(32, '0'); - + /// Lookup for the K in the TStackEntries, first by calculating it hash with TEq and searching the hash in the TStackHashes [MethodImpl((MethodImplOptions)256)] -#if NET7_0_OR_GREATER - internal static ref int GetHashRef(ref int start, int distance) => ref Unsafe.Add(ref start, distance); -#else - internal static ref int GetHashRef(ref int[] start, int distance) => ref start[distance]; -#endif + public static int TryGetStackEntryIndex( + this ref TStackEntries entries, ref TStackHashes hashes, int count, K key, int hash, + Pass it = default) + where TEntry : struct, IEntry + where TEq : struct, IEq + where TStackHashes : struct, IStack + where TStackEntries : struct, IStack + where TCap : struct, ISize2Plus + { + var cap = default(TCap).Size; + Debug.Assert(count <= cap, $"SmallMap.TryGetStackEntryIndex: count {count} should be <= stack capacity {cap}"); - [MethodImpl((MethodImplOptions)256)] -#if NET7_0_OR_GREATER - internal static int GetHash(ref int start, int distance) => Unsafe.Add(ref start, distance); -#else - internal static int GetHash(ref int[] start, int distance) => start[distance]; +#if NET8_0_OR_GREATER + if (cap >= 8 & Vector256.IsHardwareAccelerated) + { + if (count == 0) + return -1; + + var vHash = Vector256.Create(hash); + var vHashes = MemoryMarshal.Cast>(hashes.AsSpan()); + var i = 0; + foreach (var vCurr in vHashes) + { + var vMatches = Vector256.Equals(vCurr, vHash); + var matches = Vector256.ExtractMostSignificantBits(vMatches); + while (matches != 0) + { + var matchIndex = BitOperations.TrailingZeroCount(matches) + i; + if (matchIndex >= count) // todo: @perf can I remove this branch? + return -1; + + if (default(TEq).Equals(entries.GetSurePresentRef(matchIndex).Key, key)) + return matchIndex; + + // Clear lower bits up to and including the first set bit, afaik it can be hw accelerated + // 0b0001_1000 & (0b0001_1000 - 1) -> & 0b0001_1000 & 0b0001_0111 -> 0b0001_0000 + matches &= matches - 1; + } + if ((i += Vector256.Count) >= count) // todo: @perf can I remove this branch, what if the hash would be not null? + return -1; + } + return -1; + } #endif + for (var i = 0; i < count; ++i) + { + var h = hashes.GetSurePresentRef(i); + if (hashes.GetSurePresentRef(i) == hash && + default(TEq).Equals(entries.GetSurePresentRef(i).Key, key)) + return i; + } + return -1; + } - /// Configures removed key tombstone, equality and hash function for the FHashMap - public interface IEq + /// Adds or updates the entry with the provided value. + /// Returns `true` if it is updated the existing value. + [MethodImpl((MethodImplOptions)256)] + public static bool AddOrUpdate(this ref TMap map, K key, in V value) + where TMap : struct, IMap> { - /// Defines the value of the key indicating the removed entry - K GetTombstone(); - - /// Equals keys - bool Equals(K x, K y); + map.AddOrGetEntryRef(key, out var found).Value = value; + return found; + } - /// Calculates and returns the hash of the key - int GetHashCode(K key); + /// Lookups for the stored entry by key. Returns the found value or provided default + [MethodImpl((MethodImplOptions)256)] + public static V GetValueOrDefault(this ref TMap map, K key, V defaultValue = default) + where TMap : struct, IMap> + { + ref var e = ref map.TryGetEntryRef(key, out var found); + if (found) return e.Value; + return defaultValue; } +} - /// Default comparer using the `object.GetHashCode` and `object.Equals` overloads - public struct DefaultEq : IEq +/// Methods to verify the integrity and invariants of SmallMap +public static class SmallMapDiagnostics +{ + /// Verifies the integrity and invariants of the SmallMap + public static void Verify(ref this TMap map, + Action assertCond, IEnumerable expectedKeys = null, Pass pass = default) + where TMap : struct, IMap, IMapImpl { - /// - [MethodImpl((MethodImplOptions)256)] - public K GetTombstone() => default; + map.VerifyHashesAndKeysEq(assertCond, pass); - /// - [MethodImpl((MethodImplOptions)256)] - public bool Equals(K x, K y) => x.Equals(y); + map.VerifyProbesRobinHoodInvariants(assertCond, pass); - /// - [MethodImpl((MethodImplOptions)256)] - public int GetHashCode(K key) => key.GetHashCode(); + map.VerifyKeyHasMetaInfo(assertCond, pass); + + map.VerifyNoDuplicateKeys(assertCond, pass); + + if (expectedKeys != null) + map.VerifyContainAllKeys(expectedKeys, assertCond, pass); } - /// Uses the `object.GetHashCode` and `object.ReferenceEquals` - public struct RefEq : IEq where K : class + /// Verifies that the hashes correspond to the keys stored in the entries. May be called tests. + public static void VerifyHashesAndKeysEq(ref this TMap map, Action assertCond, Pass _ = default) + where TMap : struct, IMap, IMapImpl { - /// - [MethodImpl((MethodImplOptions)256)] - public K GetTombstone() => null; - - /// - [MethodImpl((MethodImplOptions)256)] - public bool Equals(K x, K y) => ReferenceEquals(x, y); + if (map.Capacity <= 0) return; + var metas = map.PackedEntryIndexesHashesProbes; + var probeMask = map.Capacity - 1; + for (var i = 0; i < metas.Capacity; i++) + { + var m = metas.GetSurePresentRef(i); + if (m != 0) + { + var metaHashPart = (int)(m & ~probeMask); + var entryIndex = (int)(m >>> EntryIndexStartAtBit); - /// - [MethodImpl((MethodImplOptions)256)] - public int GetHashCode(K key) => RuntimeHelpers.GetHashCode(key); + var key = map.GetSurePresentKey(entryIndex); + var keyHash = map.GetHashCode(key); + var keyHashPart = keyHash & ~probeMask; + assertCond(metaHashPart == keyHashPart, $"Hash mismatch for i:{i}, entryIndex:{entryIndex}, key:{key}, expected:{keyHashPart}, actual:{metaHashPart}"); + } + } } - /// Compares via `ReferenceEquals` and gets the hash faster via `RuntimeHelpers.GetHashCode` - public struct RefEq : IEq<(A, B)> - where A : class - where B : class + /// Verifies that there is no duplicate keys stored in hashes -> entries. May be called from the tests. + public static void VerifyNoDuplicateKeys(this TMap map, Action assertCond, Pass _ = default) + where TMap : struct, IMap, IMapImpl { - /// - [MethodImpl((MethodImplOptions)256)] - public (A, B) GetTombstone() => (null, null); + if (map.Capacity <= 0) return; + var uniq = new Dictionary(map.Count); - /// - [MethodImpl((MethodImplOptions)256)] - public bool Equals((A, B) x, (A, B) y) => - ReferenceEquals(x.Item1, y.Item1) && ReferenceEquals(x.Item2, y.Item2); + var metas = map.PackedEntryIndexesHashesProbes; + var probeMask = map.Capacity - 1; + for (var i = 0; i < metas.Capacity; i++) + { + var m = metas.GetSurePresentRef(i); + if (m == 0) + continue; - /// - [MethodImpl((MethodImplOptions)256)] - public int GetHashCode((A, B) key) => - Hasher.Combine(RuntimeHelpers.GetHashCode(key.Item1), RuntimeHelpers.GetHashCode(key.Item2)); + var entryIndex = (int)(m >>> EntryIndexStartAtBit); + var key = map.GetSurePresentKey(entryIndex); + + if (!uniq.ContainsKey(key)) + uniq.Add(key, 1); + else + assertCond(false, $"Duplicate key: {key}"); + } } - /// Compares via `ReferenceEquals` and gets the hash faster via `RuntimeHelpers.GetHashCode` - public struct RefEq : IEq<(A, B, C)> - where A : class - where B : class - where C : class + /// Verifies that the probes are consistently increasing + public static void VerifyProbesRobinHoodInvariants(this TMap map, Action assertCond, Pass _ = default, + int startIndex = 0, int endBefore = -1) + where TMap : struct, IMapImpl { - /// - [MethodImpl((MethodImplOptions)256)] - public (A, B, C) GetTombstone() => (null, null, null); - - /// - [MethodImpl((MethodImplOptions)256)] - public bool Equals((A, B, C) x, (A, B, C) y) => - ReferenceEquals(x.Item1, y.Item1) && ReferenceEquals(x.Item2, y.Item2) && ReferenceEquals(x.Item3, y.Item3); - - /// - [MethodImpl((MethodImplOptions)256)] - public int GetHashCode((A, B, C) key) => - Hasher.Combine(RuntimeHelpers.GetHashCode(key.Item1), Hasher.Combine(RuntimeHelpers.GetHashCode(key.Item2), RuntimeHelpers.GetHashCode(key.Item3))); + if (map.Capacity <= 0) + return; // default or stack-only map: no metadata table yet + var metas = map.PackedEntryIndexesHashesProbes; + var probeMask = map.Capacity - 1; + var prevIndex = startIndex == 0 ? probeMask : startIndex - 1; + var prevProbe = metas.GetSurePresentRef(prevIndex) & probeMask; + + endBefore = endBefore == -1 ? map.Capacity : endBefore; + for (var i = startIndex; i < endBefore; i++) + { + var probe = metas.GetSurePresentRef(i) & probeMask; + var invariant = prevProbe >= probe || prevProbe + 1 == probe; + assertCond(invariant, $"Probe invariant failed for probe:{probe} at {i}, prevProbe:{prevProbe} at {prevIndex}"); + prevIndex = i; + prevProbe = probe; + } } - /// Combines the hashes of 2 keys - internal static class Hasher + /// Verifies that each key has a corresponding hash entry which is correctly references this key + public static void VerifyKeyHasMetaInfo(ref this TMap map, + Action assertCond, Pass _ = default) + where TMap : struct, IMap { - /// Combines the hashes of 2 keys - [MethodImpl((MethodImplOptions)256)] - public static int Combine(int h1, int h2) => unchecked((h1 * (int)0xA5555529) + h2); + for (var i = 0; i < map.Count; ++i) + { + var key = map.GetSurePresentKey(i); + var foundIndex = map.TryGetIndex(key); + assertCond(foundIndex != -1, $"Not found (foundIndex:-1) for entryIndex:{i}, key:{key}"); + assertCond(foundIndex == i, $"Does not correspond foundIndex:{foundIndex}, i:{i}, key:{key}"); + } } - // todo: @improve can we move the Entry into the type parameter to configure and possibly save the memory e.g. for the sets? - /// Abstraction to configure your own entries data structure. Check the derivitives for the examples - public interface IEntries where TEq : IEq + /// Verifies that the map contains all passed keys. May be called from the tests. + public static void VerifyContainAllKeys(ref this TMap map, + IEnumerable expectedKeys, Action assertContains, Pass _ = default) + where TMap : struct, IMap { - /// Initializes the entries storage to the specified capacity via the number of bits in the capacity - void Init(byte capacityBitShift); + foreach (var key in expectedKeys) + assertContains(map.ContainsKey(key), $"Key not found: {key}"); + } +} - /// Returns the reference to entry by its index, index should map to the present/non-removed entry - ref Entry GetSurePresentEntryRef(int index); +public interface IMap +{ + /// Number of entries in the map + int Count { get; } + /// Lookups for the stored key. Returns its index in the entries or -1 + int TryGetIndex(K key); + /// Lookups for the stored key. If found true, otherwise false + bool ContainsKey(K key); + /// Lookups for the stored key by the index in the entries data structure + K GetSurePresentKey(int entryIndex); +} - /// Adds the key at the "end" of entriesc- so the order of addition is preserved. - ref V AddKeyAndGetValueRef(K key, int index); - } +public interface IMap : IMap + where TEntry : struct, IEntry +{ + /// Returns the ref to the found entry or the null ref + [UnscopedRef] + ref TEntry TryGetEntryRef(K key, out bool found); - internal const int MinEntriesCapacity = 2; + /// Returns the ref to the found entry or Adds the entry if not found + [UnscopedRef] + ref TEntry AddOrGetEntryRef(K key, out bool found); - /// Stores the entries in a single dynamically reallocated array - public struct SingleArrayEntries : IEntries where TEq : struct, IEq - { - internal Entry[] _entries; + /// Adds an entry for sure absent key. Provides the performance in scenarios where you know the key is not yet in the map + [UnscopedRef] + ref TEntry AddSureAbsentDefaultEntryAndGetRef(K key); +} - /// - public void Init(byte capacityBitShift) => - _entries = new Entry[1 << capacityBitShift]; +/// Implementation details +public interface IMapImpl +{ + /// The Capacity + int Capacity { get; } + /// The metadata + IGrowingArray PackedEntryIndexesHashesProbes { get; } + /// Returns the hash code for the provided key using the IEq implementation used by this map + int GetHashCode(K key); +} - /// - [MethodImpl((MethodImplOptions)256)] - public ref Entry GetSurePresentEntryRef(int index) - { -#if NET7_0_OR_GREATER - return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_entries), index); -#else - return ref _entries[index]; +#if DEBUG +/// Represents unpacked metadata for debugging - matches long layout +[DebuggerDisplay("{ToString(),nq}")] +[StructLayout(LayoutKind.Explicit, Size = 8)] +public readonly struct DebugMetaUnpacked +{ + [FieldOffset(0)] + public readonly uint HashAndProbe; + + [FieldOffset(4)] + public readonly uint EntryIndex; + + public override string ToString() => + HashAndProbe == 0 ? "empty" : $"ei:{EntryIndex}, hp:0b{Convert.ToString(HashAndProbe, 2).PadLeft(32, '0')}"; +} #endif - } - /// - [MethodImpl((MethodImplOptions)256)] - public ref V AddKeyAndGetValueRef(K key, int index) - { - if (index == _entries.Length) - Array.Resize(ref _entries, index << 1); -#if NET7_0_OR_GREATER - ref var e = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_entries), index); -#else - ref var e = ref _entries[index]; +/// +/// Fast and less-allocating hash map without thread safety nets. Please measure it in your own use case before use. +/// Configurable via (hash/equality) and (entry storage). +/// +/// Details: +/// - Struct so the empty/default map allocates nothing on the heap +/// - First N entries live on stack (); overflow goes to +/// - Metadata (packed entry-index | hash | probe) is stored in a single array (nothing references it, so no need for stable segments) and grows independently +/// - Robin-Hood open addressing on the metadata table +/// - Entry indexes preserve insertion order (stable enumeration by index 0..Count-1) +/// - Prefer for heap entries when you need stable refs across growth; +/// use when raw random-access speed matters more than ref stability +/// +/// Not implemented yet: removal / tombstones ( is reserved for that). +/// +[DebuggerDisplay("Count = {Count}, Capacity = {Capacity}")] +public struct SmallMap : IMap, IMapImpl + where TEntry : struct, IEntry + where TEq : struct, IEq + where TStackCap : struct, ISize2Plus + where TStackHashes : struct, IStack + where TStackEntries : struct, IStack + where THeapEntries : struct, IGrowingArray +{ +#if DEBUG + // Diagnostic counters to measure the performance of the map operations + public int PutMetadataWithoutResizing_Count = 0; + public int PutHashAndIndexWithoutResizing_ProbeGreaterOrEqualCheckCount = 0; + public int AddOrGetRefInEntries_Count = 0; + public int ResizeProbesAndHashes_Count = 0; #endif - e.Key = key; - return ref e.Value; + + internal int _capacityPowerOfTwo; + internal int _count; + + // Packing hash and probe together enables single load to examine probe and hash = + // and enables the fast check even without the unpacking both values. + // Packing entry index in the high bits saves on the array load and possible cache eviction plus simplifies the resize, because the index part can be copied as-is + // If the whole long is 0 then the slot is free. + // 0..01|00010000...110|00010 + // | | |- The probe, occupies index mask of bits always starting with 1, so the occupied item should have non 0 value + // | |- The remaining part of the hash without the index ask bits used for the item index. + // |- The entry index stored in the high bits + internal SingleBackingArray _packedEntryIndexesHashesProbes; + +#if DEBUG && SUPPORTS_UNSAFE + /// Debug view of the first metadata segment only (not full multi-segment capacity). + public Span DebugMetaUnpacked + { + get + { + var packed = _packedEntryIndexesHashesProbes; + if (packed._entries == null) + return Span.Empty; + return MemoryMarshal.Cast(packed._entries.AsSpan()); } } +#endif + +#pragma warning disable IDE0044 // it tries to make entries readonly but they should stay modify-able to prevent its defensive struct copying +#pragma warning disable CS0649 // field is never assigned to, and will always have its default value + internal TStackHashes _stackHashes; + internal TStackEntries _stackEntries; + internal THeapEntries _heapEntries; +#pragma warning restore CS0649 +#pragma warning restore IDE0044 + + /// Capacity + public int Capacity => _capacityPowerOfTwo; + + /// Access to the entry indexes, hashes and corresponding probes. + /// Note: the result value may be boxed + public IGrowingArray PackedEntryIndexesHashesProbes => _packedEntryIndexesHashesProbes; + + /// + public int Count => _count; + + /// Access to the key-value entries + public THeapEntries HeapEntries => _heapEntries; + + /// Capacity calculates as `1 leftShift capacityBitShift` + public SmallMap(uint capacityPowerOfTwoPlease) + { + var capacityPowerOfTwo = capacityPowerOfTwoPlease < MinCapacity + ? MinCapacity + : BitOps.NextPowerOfTwo(capacityPowerOfTwoPlease); + + _capacityPowerOfTwo = (int)capacityPowerOfTwo; + + // capacity was checked above + _packedEntryIndexesHashesProbes.InitDoNotCheckCapacity(capacityPowerOfTwo); + _heapEntries.InitDoNotCheckCapacity(capacityPowerOfTwo); + } - /// Finds the stored value by key. If found returns ref to the value it can be modified in place. + /// [MethodImpl((MethodImplOptions)256)] - public static ref V TryGetValueRef(this ref FHashMap map, K key, out bool found) - where TEq : struct, IEq - where TEntries : struct, IEntries + public int GetHashCode(K key) => default(TEq).GetHashCode(key); + + /// + [MethodImpl((MethodImplOptions)256)] + public K GetSurePresentKey(int entryIndex) { - if (map._count > StackEntriesCount) - return ref map.TryGetValueRefByHash(key, out found); - switch (map._count) - { - case 1: - if (found = default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - break; - case 2: - if (found = default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - if (found = default(TEq).Equals(key, map._e1.Key)) return ref map._e1.Value; - break; - case 3: - if (found = default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - if (found = default(TEq).Equals(key, map._e1.Key)) return ref map._e1.Value; - if (found = default(TEq).Equals(key, map._e2.Key)) return ref map._e2.Value; - break; - case 4: - if (found = default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - if (found = default(TEq).Equals(key, map._e1.Key)) return ref map._e1.Value; - if (found = default(TEq).Equals(key, map._e2.Key)) return ref map._e2.Value; - if (found = default(TEq).Equals(key, map._e3.Key)) return ref map._e3.Value; - break; - } - found = false; - return ref FHashMap._missing.Value; + Debug.Assert(entryIndex >= 0 & entryIndex < _count, $"Entry index {entryIndex} should be in the range 0..{_count - 1}"); + return (entryIndex >= _stackEntries.Capacity + ? _heapEntries.GetSurePresentRef(entryIndex - _stackEntries.Capacity) + : _stackEntries.GetSurePresentRef(entryIndex)) + .Key; } - /// Gets the reference to the existing value of the provided key, or the default value to set for the newly added key. + ///Get the value ref by the entry index. Also the index corresponds to entry adding order. + /// Important: it does not check the index bounds, so you need to check that the index is from 0 to map.Count-1 + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public static ref V GetOrAddValueRef(this ref FHashMap map, K key, out bool found) - where TEq : struct, IEq - where TEntries : struct, IEntries + public ref TEntry GetSurePresentEntryRef(int entryIndex) { - if (map._count > StackEntriesCount) - return ref map.GetOrAddValueRefByHash(key, out found); - found = true; - switch (map._count) - { - case 0: - found = false; - map._count = 1; - map._e0.Key = key; - return ref map._e0.Value; - - case 1: - if (default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - found = false; - map._count = 2; - map._e1.Key = key; - return ref map._e1.Value; - - case 2: - if (default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - if (default(TEq).Equals(key, map._e1.Key)) return ref map._e1.Value; - found = false; - map._count = 3; - map._e2.Key = key; - return ref map._e2.Value; - - case 3: - if (default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - if (default(TEq).Equals(key, map._e1.Key)) return ref map._e1.Value; - if (default(TEq).Equals(key, map._e2.Key)) return ref map._e2.Value; - found = false; - map._count = 4; - map._e3.Key = key; - return ref map._e3.Value; - - default: - if (default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - if (default(TEq).Equals(key, map._e1.Key)) return ref map._e1.Value; - if (default(TEq).Equals(key, map._e2.Key)) return ref map._e2.Value; - if (default(TEq).Equals(key, map._e3.Key)) return ref map._e3.Value; - found = false; - - map._capacityBitShift = MinHashesCapacityBitShift; - map._packedHashesAndIndexes = new int[1 << MinHashesCapacityBitShift]; - - var indexMask = (1 << MinHashesCapacityBitShift) - 1; - - // todo: @perf optimize by calculating the keys hashes and putting them into the span and iterating over them inside a single method - map.AddInitialHashWithoutResizing(map._e0.Key, 0, indexMask); - map.AddInitialHashWithoutResizing(map._e1.Key, 1, indexMask); - map.AddInitialHashWithoutResizing(map._e2.Key, 2, indexMask); - map.AddInitialHashWithoutResizing(map._e3.Key, 3, indexMask); - map.AddInitialHashWithoutResizing(key, StackEntriesCount, indexMask); - - map._count = 5; - map._entries.Init(2); - return ref map._entries.AddKeyAndGetValueRef(key, 0); - } + Debug.Assert(entryIndex >= 0 && entryIndex < _count, $"Index {entryIndex} should be in the range 0..{_count - 1}"); + if (entryIndex >= _stackEntries.Capacity) + return ref _heapEntries.GetSurePresentRef(entryIndex - _stackEntries.Capacity); + return ref _stackEntries.GetSurePresentRef(entryIndex); } - private static void AddInitialHashWithoutResizing(this ref FHashMap map, K key, int index, int indexMask) - where TEq : struct, IEq - where TEntries : struct, IEntries + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] + private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) { -#if NET7_0_OR_GREATER - ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(map._packedHashesAndIndexes); -#else - var hashesAndIndexes = map._packedHashesAndIndexes; +#if DEBUG + ++AddOrGetRefInEntries_Count; #endif - var hash = default(TEq).GetHashCode(key); - var hashIndex = hash & indexMask; - - // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - ref var h = ref GetHashRef(ref hashesAndIndexes, hashIndex); - var probes = 1; - while ((h >>> ProbeCountShift) >= probes) + found = true; + var probeMask = _capacityPowerOfTwo - 1; + var meta = (long)_count << EntryIndexStartAtBit | (long)(hash & ~probeMask) | 1L; + var metaIndex = hash & probeMask; + while (true) { - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); - ++probes; - } - - // 3. We did not find the hash and therefore the key, so insert the new entry - var hRobinHooded = h; - h = (probes << ProbeCountShift) | (hash & HashAndIndexMask & ~indexMask) | index; + ref var mRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(metaIndex & probeMask); + if ((mRef & probeMask) < (meta & probeMask)) + { + var mRobinHooded = mRef; + mRef = meta; + if (mRobinHooded == 0) // finish on the empty slot + break; + meta = mRobinHooded; + } - // 4. If the robin hooded hash is empty then we stop - // 5. Otherwise we steal the slot with the smaller probes - probes = hRobinHooded >>> ProbeCountShift; - while (hRobinHooded != 0) - { - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); - if ((h >>> ProbeCountShift) < ++probes) + if ((mRef & HashAndProbeMask) == (meta & HashAndProbeMask)) { - var tmp = h; - h = (probes << ProbeCountShift) | (hRobinHooded & HashAndIndexMask); - hRobinHooded = tmp; - probes = hRobinHooded >>> ProbeCountShift; + var entryIndex = (int)(mRef >>> EntryIndexStartAtBit); + ref var eRef = ref GetSurePresentEntryRef(entryIndex); + if (default(TEq).Equals(eRef.Key, key)) + return ref eRef; } + + ++meta; + ++metaIndex; } + + found = false; + var newEntryIndex = _count - _stackEntries.Capacity; + if (newEntryIndex >= _heapEntries.Capacity) + _heapEntries.DoubleCapacity(); + + ref var newEntry = ref _heapEntries.GetSurePresentRef(newEntryIndex); + newEntry.Key = key; + ++_count; + + // If free metadata slots are less than ~1/8 of capacity (~12.5%), grow the table. + if (_capacityPowerOfTwo - _count <= (_capacityPowerOfTwo >>> MinFreeCapacityShift)) + ResizeMetadata(); + + return ref newEntry; } - /// Adds the sure absent key entry. - /// Provides the performance in scenarios where you look for present key, and using it, and if ABSENT then add the new one. - /// So this method optimized NOT to look for the present item for the second time in SEQUENCE [MethodImpl((MethodImplOptions)256)] - public static ref V AddSureAbsentDefaultAndGetRef(this ref FHashMap map, K key) - where TEq : struct, IEq - where TEntries : struct, IEntries + private void PutMetadataWithoutResizing(int probeMask, int hash, int entryIndex) { - if (map._count > StackEntriesCount) - return ref map.AddSureAbsentDefaultAndGetRefByHash(key); - switch (map._count) +#if DEBUG + ++PutMetadataWithoutResizing_Count; +#endif + var meta = (long)entryIndex << EntryIndexStartAtBit | (long)(hash & ~probeMask) | 1L; + var metaIndex = hash & probeMask; + while (true) { - case 0: - map._count = 1; - map._e0.Key = key; - return ref map._e0.Value; - - case 1: - map._count = 2; - map._e1.Key = key; - return ref map._e1.Value; - - case 2: - map._count = 3; - map._e2.Key = key; - return ref map._e2.Value; - - case 3: - map._count = 4; - map._e3.Key = key; - return ref map._e3.Value; - - default: - map._capacityBitShift = MinHashesCapacityBitShift; - map._packedHashesAndIndexes = new int[1 << MinHashesCapacityBitShift]; - - var indexMask = (1 << MinHashesCapacityBitShift) - 1; - - map.AddInitialHashWithoutResizing(map._e0.Key, 0, indexMask); - map.AddInitialHashWithoutResizing(map._e1.Key, 1, indexMask); - map.AddInitialHashWithoutResizing(map._e2.Key, 2, indexMask); - map.AddInitialHashWithoutResizing(map._e3.Key, 3, indexMask); - map.AddInitialHashWithoutResizing(key, StackEntriesCount, indexMask); - - map._count = 5; - map._entries.Init(2); - return ref map._entries.AddKeyAndGetValueRef(key, 0); + ref var mRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(metaIndex & probeMask); + if ((mRef & probeMask) < (meta & probeMask)) + { + var mRobinHooded = mRef; + mRef = meta; + if (mRobinHooded == 0) // finish on the empty slot + return; + meta = mRobinHooded; + } + + ++metaIndex; + ++meta; } } + /// + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - private static ref V AddSureAbsentDefaultAndGetRefByHash(this ref FHashMap map, K key) - where TEq : struct, IEq - where TEntries : struct, IEntries + public ref TEntry AddOrGetEntryRef(K key, out bool found) { - // if the free space is less than 1/8 of capacity (12.5%) then Resize - var indexMask = (1 << map._capacityBitShift) - 1; - if (indexMask - map._count <= (indexMask >>> MinFreeCapacityShift)) - indexMask = map.ResizeHashes(indexMask); - var hash = default(TEq).GetHashCode(key); - var hashIndex = hash & indexMask; -#if NET7_0_OR_GREATER - ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(map._packedHashesAndIndexes); -#else - var hashesAndIndexes = map._packedHashesAndIndexes; -#endif - ref var h = ref GetHashRef(ref hashesAndIndexes, hashIndex); + if (_count > _stackEntries.Capacity) + return ref AddOrGetRefInEntries(key, hash, out found); + + var i = _stackEntries.TryGetStackEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); + if (found = i != -1) + return ref _stackEntries.GetSurePresentRef(i); - // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - var probes = 1; - while ((h >>> ProbeCountShift) >= probes) + // Add the new entry to the stack if there is still space in the stack + if (_count < _stackEntries.Capacity) { - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); - ++probes; + var addedIndex = _count++; + _stackHashes.GetSurePresentRef(addedIndex) = hash; + ref var newStackEntry = ref _stackEntries.GetSurePresentRef(addedIndex); + newStackEntry.Key = key; + return ref newStackEntry; } - // 3. We did not find the hash and therefore the key, so insert the new entry - var hRobinHooded = h; - h = (probes << ProbeCountShift) | (hash & HashAndIndexMask & ~indexMask) | map._count; + return ref MigrateToHeapAndAddEntry(key, hash); + } - // 4. If the robin hooded hash is empty then we stop - // 5. Otherwise we steal the slot with the smaller probes - probes = hRobinHooded >>> ProbeCountShift; - while (hRobinHooded != 0) + [UnscopedRef] + private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) + { + // Stack is full. Switch lookups from linear stack scan to the Robin-Hood metadata table. + // Stack entries themselves stay put (never copied). Heap overflow uses THeapEntries; + // with Grr those heap entry refs also stay stable across later growth. + var stackCapacity = _stackEntries.Capacity; + var newCapacity = Math.Max(_capacityPowerOfTwo, stackCapacity << 1); + + // Keep the larger of constructor-provided capacity vs. stack-driven capacity. + if (_packedEntryIndexesHashesProbes.Capacity != newCapacity) + _packedEntryIndexesHashesProbes.GrowCapacity((uint)newCapacity); + + _capacityPowerOfTwo = newCapacity = _packedEntryIndexesHashesProbes.Capacity; + var probeMask = newCapacity - 1; + for (var i = 0; i < stackCapacity; ++i) { - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); - if ((h >>> ProbeCountShift) < ++probes) - { - var tmp = h; - h = (probes << ProbeCountShift) | (hRobinHooded & HashAndIndexMask); - hRobinHooded = tmp; - probes = hRobinHooded >>> ProbeCountShift; - } + var h = _stackHashes.GetSurePresentRef(i); + PutMetadataWithoutResizing(probeMask, h, i); } - return ref map._entries.AddKeyAndGetValueRef(key, (map._count++) - StackEntriesCount); + PutMetadataWithoutResizing(probeMask, hash, stackCapacity); + + // First heap entry slot is index 0 in THeapEntries (global entry index == stackCapacity). + _heapEntries.GrowCapacity((uint)stackCapacity); + ref var newEntry = ref _heapEntries.GetSurePresentRef(0); + newEntry.Key = key; + ++_count; + +#if VERIFY_MAP + this.VerifyProbesRobinHoodInvariants(static (x, msg) => Debug.Assert(x, msg), Pass.It); + this.VerifyKeyHasMetaInfo(static (x, msg) => Debug.Assert(x, msg), Pass.It); +#endif + + return ref newEntry; } - ///Get the value ref by the entry index. Also the index corresponds to entry adding order. - ///Improtant: it does not checks the index bounds, so you need to check that the index is from 0 to map.Count-1 + /* + Robin Hood open addressing example (capacity mask 7): + + 1. Empty map + Index: 0 1 2 3 4 5 6 7 + Hash: [0] [0] [0] [0] [0] [0] [0] [0] + + 2. Insert A hash 13 -> index 5, probe 1 + 3. Insert B hash 5 -> index 6, probe 2 (displaced from 5) + 4. Insert C hash 7 -> index 7, probe 1 + 5. Insert D hash 21 -> index 7 probe 3, C wraps to index 0 probe 2 + */ + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public static ref Entry GetSurePresentEntryRef(this ref FHashMap map, int index) - where TEq : struct, IEq - where TEntries : struct, IEntries + private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key, int hash) { - Debug.Assert(index >= 0); - Debug.Assert(index < map._count); - if (index >= StackEntriesCount) - return ref map._entries.GetSurePresentEntryRef(index - StackEntriesCount); - switch (index) - { - case 0: return ref map._e0; - case 1: return ref map._e1; - case 2: return ref map._e2; - case 3: return ref map._e3; - } - return ref FHashMap._missing; + if (_capacityPowerOfTwo - _count <= (_capacityPowerOfTwo >>> MinFreeCapacityShift)) + ResizeMetadata(); + + PutMetadataWithoutResizing(_capacityPowerOfTwo - 1, hash, _count); + + var newEntryIndex = _count - _stackEntries.Capacity; + if (newEntryIndex >= _heapEntries.Capacity) + _heapEntries.DoubleCapacity(); + + ref var entry = ref _heapEntries.GetSurePresentRef(newEntryIndex); + entry.Key = key; + + ++_count; + + return ref entry; } + /// + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - internal static ref V TryGetValueRefByHash(this ref FHashMap map, K key, out bool found) - where TEq : struct, IEq - where TEntries : struct, IEntries + public ref TEntry AddSureAbsentDefaultEntryAndGetRef(K key) { var hash = default(TEq).GetHashCode(key); - var indexMask = (1 << map._capacityBitShift) - 1; - var hashMiddleMask = HashAndIndexMask & ~indexMask; - var hashMiddle = hash & hashMiddleMask; - var hashIndex = hash & indexMask; - -#if NET7_0_OR_GREATER - ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(map._packedHashesAndIndexes); -#else - var hashesAndIndexes = map._packedHashesAndIndexes; -#endif - - var h = GetHash(ref hashesAndIndexes, hashIndex); + if (_count > _stackEntries.Capacity) + return ref AddSureAbsentDefaultAndGetRefInEntries(key, hash); - // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - var probes = 1; - while ((h >>> ProbeCountShift) >= probes) + // Add the new entry to the stack if there is still space in stack + if (_count < _stackEntries.Capacity) { - // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) - { - ref var e = ref map.GetSurePresentEntryRef(h & indexMask); - if (default(TEq).Equals(e.Key, key)) - { - found = true; - return ref e.Value; - } - } - - h = GetHash(ref hashesAndIndexes, ++hashIndex & indexMask); - ++probes; + var newIndex = _count++; + _stackHashes.GetSurePresentRef(newIndex) = hash; + ref var newStackEntry = ref _stackEntries.GetSurePresentRef(newIndex); + newStackEntry.Key = key; + return ref newStackEntry; } - found = false; - return ref FHashMap._missing.Value; + return ref MigrateToHeapAndAddEntry(key, hash); } + /// [MethodImpl((MethodImplOptions)256)] - private static ref V GetOrAddValueRefByHash(this ref FHashMap map, K key, out bool found) - where TEq : struct, IEq - where TEntries : struct, IEntries + public int TryGetIndex(K key) { - // if the free space is less than 1/8 of capacity (12.5%) then Resize - var indexMask = (1 << map._capacityBitShift) - 1; - if (indexMask - map._count <= (indexMask >>> MinFreeCapacityShift)) - indexMask = map.ResizeHashes(indexMask); - var hash = default(TEq).GetHashCode(key); - var hashMiddleMask = HashAndIndexMask & ~indexMask; - var hashMiddle = hash & hashMiddleMask; - var hashIndex = hash & indexMask; + return _count > _stackEntries.Capacity + ? TryGetIndexInEntries(key, hash) + : _stackEntries.TryGetStackEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); + } -#if NET7_0_OR_GREATER - ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(map._packedHashesAndIndexes); -#else - var hashesAndIndexes = map._packedHashesAndIndexes; -#endif - ref var h = ref GetHashRef(ref hashesAndIndexes, hashIndex); + /// + public bool ContainsKey(K key) => TryGetIndex(key) != -1; - // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - var probes = 1; - while ((h >>> ProbeCountShift) >= probes) + [MethodImpl((MethodImplOptions)256)] + internal int TryGetIndexInEntries(K key, int hash) + { + var probeMask = _capacityPowerOfTwo - 1; + var meta = (long)(hash & ~probeMask) | 1L; + var metaIndex = hash & probeMask; + while (true) { - // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) + ref var mRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(metaIndex & probeMask); + if ((mRef & probeMask) < (meta & probeMask)) + return -1; + + if ((mRef & HashAndProbeMask) == meta) { - ref var e = ref map.GetSurePresentEntryRef(h & indexMask); - if (default(TEq).Equals(e.Key, key)) - { - found = true; - return ref e.Value; - } + var entryIndex = (int)(mRef >>> EntryIndexStartAtBit); + ref var entry = ref GetSurePresentEntryRef(entryIndex); + if (default(TEq).Equals(entry.Key, key)) + return entryIndex; } - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); - ++probes; - } - // 3. We did not find the hash and therefore the key, so insert the new entry - var hRobinHooded = h; - h = (probes << ProbeCountShift) | hashMiddle | map._count; + ++meta; + ++metaIndex; + } + } - // 4. If the robin hooded hash is empty then we stop - // 5. Otherwise we steal the slot with the smaller probes - probes = hRobinHooded >>> ProbeCountShift; - while (hRobinHooded != 0) + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] + internal ref TEntry TryGetEntryRef(K key, int hash, out bool found) + { + found = false; + var probeMask = _capacityPowerOfTwo - 1; + var meta = (long)(hash & ~probeMask) | 1L; + var metaIndex = hash & probeMask; + while (true) { - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); - if ((h >>> ProbeCountShift) < ++probes) + ref var mRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(metaIndex & probeMask); + if ((mRef & probeMask) < (meta & probeMask)) + return ref RefTools.GetNullRef(); + + if ((mRef & HashAndProbeMask) == meta) { - var tmp = h; - h = (probes << ProbeCountShift) | (hRobinHooded & HashAndIndexMask); - hRobinHooded = tmp; - probes = hRobinHooded >>> ProbeCountShift; + var entryIndex = (int)(mRef >>> EntryIndexStartAtBit); + ref var entry = ref GetSurePresentEntryRef(entryIndex); + if (default(TEq).Equals(entry.Key, key)) + { + found = true; + return ref entry; + } } + + ++meta; + ++metaIndex; } - found = false; - return ref map._entries.AddKeyAndGetValueRef(key, (map._count++) - StackEntriesCount); } -} - -// todo: @improve ? how/where to add SIMD to improve CPU utilization but not losing perf for smaller sizes -/// -/// Fast and less-allocating hash map without thread safety nets. Please measure it in your own use case before use. -/// It is configurable in regard of hash calculation/equality via `TEq` type paremeter and -/// in regard of key-value storage via `TEntries` type parameter. -/// -/// Details: -/// - Implemented as a struct so that the empty/default map does not allocate on heap -/// - Hashes and key-values are the separate collections enabling better cash locality and faster performance (data-oriented design) -/// - No SIMD for now to avoid complexity and costs for the smaller maps, so the map is more fit for the smaller sizes. -/// - Provides the "stable" enumeration of the entries in the added order -/// - The TryRemove method removes the hash but replaces the key-value entry with the tombstone key and the default value. -/// For instance, for the `RefEq` the tombstone is . You may redefine it in the `IEq{K}.GetTombstone()` implementation. -/// -/// -public struct FHashMap - where TEq : struct, IEq - where TEntries : struct, IEntries -{ - internal static Entry _missing; - internal byte _capacityBitShift; - internal int _count; + /// + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] + public ref TEntry TryGetEntryRef(K key, out bool found) + { + var hash = default(TEq).GetHashCode(key); + if (_count > _stackEntries.Capacity) + return ref TryGetEntryRef(key, hash, out found); - // The _packedHashesAndIndexes elements are of `Int32` with the bits split as following: - // 00010|000...110|01101 - // | | |- The index into the _entries structure, 0-based. The index bit count (indexMask) is the hashes capacity - 1. - // | | | This part of the erased hash is used to get the ideal index into the hashes array, so later this part of hash may be restored from the hash index and its probes. - // | |- The remaining middle bits of the original hash - // |- 5 (MaxProbeBits) high bits of the Probe count, with the minimal value of b00001 indicating the non-empty slot. - internal int[] _packedHashesAndIndexes; + var i = _stackEntries.TryGetStackEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); + if (found = i != -1) + return ref _stackEntries.GetSurePresentRef(i); -#pragma warning disable IDE0044 // it tries to make entries readonly but they should stay modifyable to prevent its defensive struct copying - internal TEntries _entries; -#pragma warning restore IDE0044 + return ref RefTools.GetNullRef(); + } - // todo: @improve how to configure how much we store on stack - internal Entry _e0, _e1, _e2, _e3; + internal void ResizeMetadataWithoutWrapArounds(int startAt, int oldCapacity, int probeMask) + { + var newCapacity = _capacityPowerOfTwo << 1; + var newProbeMask = newCapacity - 1; - /// Capacity bits - public int CapacityBitShift => _capacityBitShift; + _capacityPowerOfTwo = newCapacity; + _packedEntryIndexesHashesProbes.DoubleCapacity(); - /// Access to the hashes and indexes - public int[] PackedHashesAndIndexes => _packedHashesAndIndexes; + // From the current index i reinsert the metas into the grown metadata array. + // The meta either stays if it is already in ideal position (probe == 1) + // or depending on the next recovered hash bit either tries to go closer to its ideal position or move to the initially empty second half of the doubled meta list. + Debug.Assert(startAt >= 0 && startAt < oldCapacity, $"Start index should be within the bounds of the old capacity."); + for (var i = startAt; i < oldCapacity; ++i) + { + ref var metaRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(i); + if (metaRef == 0) // empty slot, nothing to reinsert + continue; - /// Number of entries in the map - public int Count => _count; + var probe = (int)(metaRef & probeMask); + var nextHashBit = metaRef & oldCapacity; // recover the next hash bit + if (probe == 1 & nextHashBit == 0) // already in ideal position, skip + continue; - /// Access to the key-value entries - public TEntries Entries => _entries; + var newMeta = metaRef & ~newProbeMask; + var newProbe = 1; - /// Capacity calculates as `1 leftShift capacityBitShift` - public FHashMap(byte capacityBitShift) - { - _capacityBitShift = capacityBitShift; + metaRef = 0; // erase the current meta slot, so we starting from ideal position we may find better/closer to ideal position slot or return to the previous erased slot. - // the overflow tail to the hashes is the size of log2N where N==capacityBitShift, - // it is probably fine to have the check for the overlow of capacity because it will be mispredicted only once at the end of loop (it even rarely for the lookup) - _packedHashesAndIndexes = new int[1 << capacityBitShift]; - _entries = default; - _entries.Init(capacityBitShift); + var idealIndex = i - (probe - 1) + (int)nextHashBit; + Debug.Assert(idealIndex >= 0, $"After skipping all wrapped-around metas, the new ideal index should be non-negative."); + while (idealIndex < newCapacity) // defensive check, should be prevented by wrapping with newProbeMask + { + ref var newMetaRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(idealIndex); + if (newMetaRef == 0) // empty -> place the new meta here + { + newMetaRef = newMeta | (long)newProbe; + break; + } + ++newProbe; + idealIndex = (idealIndex + 1) & newProbeMask; // defensive wrap on the new doubled capacity boundary + } + } } - internal int ResizeHashes(int indexMask) + internal void ResizeMetadata() { - var oldCapacity = indexMask + 1; - var newHashAndIndexMask = HashAndIndexMask & ~oldCapacity; - var newIndexMask = (indexMask << 1) | 1; +#if DEBUG + ++ResizeProbesAndHashes_Count; +#endif + var oldCapacity = _capacityPowerOfTwo; + var probeMask = oldCapacity - 1; + + // If no wrapped-around entries exist, we can resize without worrying about them. + ref var metaRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(0); + var probe = (int)(metaRef & probeMask); + if (probe <= 1) + { + ResizeMetadataWithoutWrapArounds(0, oldCapacity, probeMask); + return; + } - var newHashesAndIndexes = new int[oldCapacity << 1]; + var newCapacity = _capacityPowerOfTwo << 1; + var newProbeMask = newCapacity - 1; -#if NET7_0_OR_GREATER - ref var newHashes = ref MemoryMarshal.GetArrayDataReference(newHashesAndIndexes); - ref var oldHashes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); - var oldHash = oldHashes; -#else - var newHashes = newHashesAndIndexes; - var oldHashes = _packedHashesAndIndexes; - var oldHash = oldHashes[0]; -#endif - // Overflow segment is wrapped-around hashes and! the hashes at the beginning robin hooded by the wrapped-around hashes + // Move the wrapped-around metadata to the aside small-list hoping that the small amount of the items will keep them on stack. + // We need this to avoid wrapped-around items to be in a ways of simplistic linear probing re-insertions of the rest of the metadata. + SmallList> asideWrappedAroundMetas = default; var i = 0; - while ((oldHash >>> ProbeCountShift) > 1) - oldHash = GetHash(ref oldHashes, ++i); + while (i < oldCapacity) // defensive check - in reality metas should not contain only wrapped-around entries, there is no such thing without normal entries. + { + metaRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(i); + probe = (int)(metaRef & probeMask); + if (probe - 1 <= i) // default probe is 1, when at the index i:0 the probe is 2 and more means the item ideal index was before the 0 - so it goes from the end + break; + asideWrappedAroundMetas.Add(in metaRef); + metaRef = 0; // don't forget to clear the original slot + ++i; + } + Debug.Assert(i < oldCapacity, $"All entries are wrapped-around, which should not happen."); - var oldCapacityWithOverflowSegment = i + oldCapacity; - while (true) + // Go reinsert the non-wrapped-around entries into the resized metadata list. + ResizeMetadataWithoutWrapArounds(i, oldCapacity, probeMask); + + // Reinsert the wrapped around entries from the set aside list to the grown metas via normal robin-hood insertion. + var wrappedCount = asideWrappedAroundMetas.Count; + for (var wrappedIndex = 0; wrappedIndex < wrappedCount; ++wrappedIndex) { - if (oldHash != 0) + var wrappedMeta = asideWrappedAroundMetas.GetSurePresentRef(wrappedIndex); + probe = (int)(wrappedMeta & probeMask); // oldCapacity + wrappedIndex - (probe - 1) gives the original position before wrap around, which in turn is the hash part we used + var nextHashBit = wrappedMeta & oldCapacity; + var newMetaIndex = (oldCapacity + wrappedIndex - (probe - 1) + (int)nextHashBit) & newProbeMask; // wrappedIndex in the aside list is the same as the original in metas which is funny/useful invariant + var newMeta = wrappedMeta & ~newProbeMask; + var newProbe = 1; + + // Reinsert using Robin Hood swaps, advancing the probe in each occupied slot and when the smaller probe found, switch to this new RH candidate and advance it instead. + while (true) { - // get the new hash index from the old one with the next bit equal to the `oldCapacity` - var indexWithNextBit = (oldHash & oldCapacity) | (((i + 1) - (oldHash >>> ProbeCountShift)) & indexMask); - - // no need for robinhooding because we already did it for the old hashes and now just sparcing the hashes into the new array which are already in order - var probes = 1; - ref var newHash = ref GetHashRef(ref newHashes, indexWithNextBit); - while (newHash != 0) + ref var newMetaRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(newMetaIndex); // here we operate on the new metas already spread into the new position + if ((newMetaRef & newProbeMask) < newProbe) // iterate until we found empty slot (probe 0) or RH candidate with smaller probe { - newHash = ref GetHashRef(ref newHashes, ++indexWithNextBit & newIndexMask); - ++probes; + var candidate = newMetaRef; + newMetaRef = newMeta | (long)newProbe; + if (candidate == 0) // if candidate slot was empty we are done with this insertion + break; + newMeta = candidate & ~newProbeMask; + newProbe = (int)(candidate & newProbeMask); } - newHash = (probes << ProbeCountShift) | (oldHash & newHashAndIndexMask); - } - if (++i >= oldCapacityWithOverflowSegment) - break; - oldHash = GetHash(ref oldHashes, i & indexMask); + ++newProbe; + newMetaIndex = (newMetaIndex + 1) & newProbeMask; + } } - ++_capacityBitShift; - _packedHashesAndIndexes = newHashesAndIndexes; - return newIndexMask; + +#if VERIFY_MAP + this.VerifyProbesRobinHoodInvariants(static (x, msg) => Debug.Assert(x, msg), Pass.It); + this.VerifyKeyHasMetaInfo(static (x, msg) => Debug.Assert(x, msg), Pass.It); +#endif } } + +/// Map with 4 stack entries; heap overflow in stable . +public struct SmallMap4 where TEq : struct, IEq +{ + public SmallMap, TEq, Size4, Stack4, Stack4>, + GrrPowerOf2, Stack8[]>>> Map; + public SmallMap4() => Map = new(MinCapacity); + public SmallMap4(uint capacityPowerOfTwo) => Map = new(capacityPowerOfTwo); +} + +/// Map with 8 stack entries; heap overflow in stable . +public struct SmallMap8 where TEq : struct, IEq +{ + public SmallMap, TEq, Size8, Stack8, Stack8>, + GrrPowerOf2, Stack8[]>>> Map; + public SmallMap8() => Map = new(MinCapacity); + public SmallMap8(uint capacityPowerOfTwo) => Map = new(capacityPowerOfTwo); +} + +/// Map with 16 stack entries; heap overflow in stable (refs stay valid across growth). +public struct SmallMap16 where TEq : struct, IEq +{ + public SmallMap, TEq, Size16, Stack16, Stack16>, + GrrPowerOf2, Stack8[]>>> Map; + public SmallMap16() => Map = new(MinCapacity); + public SmallMap16(uint capacityPowerOfTwo) => Map = new(capacityPowerOfTwo); +} + +/// Map with 16 stack entries; heap overflow in a single growing array (faster access, refs invalidate on resize). +public struct SmallMap16_SingleArrEntries where TEq : struct, IEq +{ + public SmallMap, TEq, Size16, Stack16, Stack16>, + SingleBackingArray>> Map; + public SmallMap16_SingleArrEntries() => Map = new(MinCapacity); + public SmallMap16_SingleArrEntries(uint capacityPowerOfTwo) => Map = new(capacityPowerOfTwo); +} + +/// Set with 4 stack keys; heap overflow in stable . +public struct SmallSet4 where TEq : struct, IEq +{ + public SmallMap, TEq, Size4, Stack4, Stack4>, + GrrPowerOf2, Stack8[]>>> Set; + public SmallSet4() => Set = new(MinCapacity); + public SmallSet4(uint capacityPowerOfTwo) => Set = new(capacityPowerOfTwo); +} + +/// Set with 8 stack keys; heap overflow in stable . +public struct SmallSet8 where TEq : struct, IEq +{ + public SmallMap, TEq, Size8, Stack8, Stack8>, + GrrPowerOf2, Stack8[]>>> Set; + public SmallSet8() => Set = new(MinCapacity); + public SmallSet8(uint capacityPowerOfTwo) => Set = new(capacityPowerOfTwo); +} + +/// Set with 16 stack keys; heap overflow in stable . +public struct SmallSet16 where TEq : struct, IEq +{ + public SmallMap, TEq, Size16, Stack16, Stack16>, + GrrPowerOf2, Stack8[]>>> Set; + public SmallSet16() => Set = new(MinCapacity); + public SmallSet16(uint capacityPowerOfTwo) => Set = new(capacityPowerOfTwo); +} diff --git a/playground/Experiments/FHashMap11.cs b/playground/Experiments/FHashMap11.cs new file mode 100644 index 00000000..f4084d4b --- /dev/null +++ b/playground/Experiments/FHashMap11.cs @@ -0,0 +1,585 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +#if NET7_0_OR_GREATER +using System.Runtime.InteropServices; +#endif + +namespace ImTools.Experiments; + +using static FHashMap11; + +public static class FHashMap11 +{ + /// 2^32 / phi for the Fibonacci hashing, where phi is the golden ratio ~1.61803 + public const uint GoldenRatio32 = 2654435769; + + internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) + internal const byte MinCapacityBits = 3; // 1 << 3 == 8 + + /// Verifies that the hashes correspond to the keys stored in the entries. May be called from the tests. + public static void VerifyHashesAndKeysEq(this FHashMap11 map, Action assertEq) + where TEq : struct, IEq + { + for (var i = 0; i < map.Probes.Length; ++i) + if (map.Probes[i] != 0) + assertEq(default(TEq).GetHashCode(map.Keys[i]) == map.Hashes[i]); + } + + /// Verifies that there is no duplicate keys stored in hashes -> entries. May be called from the tests. + public static void VerifyNoDuplicateKeys(this FHashMap11 map, Action assertKey) + where TEq : struct, IEq + { + // Verify the indexes do no contains duplicate keys + var uniq = new Dictionary(map.Count); + var keys = map.Keys; + for (var i = 0; i < keys.Length; i++) + { + if (map.Probes[i] == 0) // skip empty + continue; + var key = keys[i]; + if (!uniq.TryGetValue(key, out _)) + uniq.Add(key, 1); + else + assertKey(key); + } + } + + public static void VerifyProbesAreFitRobinHood(this FHashMap11 map, Action reportFail) + where TEq : struct, IEq + { + var hashes = map.Hashes; + var prevProbe = -1; + for (var i = 0; i < hashes.Length; i++) + { + var probe = map.Probes[i]; + if (prevProbe != -1 & probe - prevProbe > 1) + reportFail($"Probes are not consequent: {prevProbe}, {probe} for {i}: p{probe}, {map.ValueIndexes[i]} -> {map.Keys[i]}"); + prevProbe = probe; + } + } + + /// Verifies that the map contains all passed keys. May be called from the tests. + public static void VerifyContainAllKeys(this FHashMap11 map, IEnumerable expectedKeys, Action assertContainKey) + where TEq : struct, IEq + { + foreach (var key in expectedKeys) + assertContainKey(map.TryGetValue(key, out _), key); + } + + [MethodImpl((MethodImplOptions)256)] +#if NET7_0_OR_GREATER + internal static ref T GetItemRef(ref T start, int distance) => ref Unsafe.Add(ref start, distance); +#else + internal static ref T GetItemRef(ref T[] start, int distance) => ref start[distance]; +#endif + + [MethodImpl((MethodImplOptions)256)] +#if NET7_0_OR_GREATER + internal static T GetItem(ref T start, int distance) => Unsafe.Add(ref start, distance); +#else + internal static T GetItem(ref T[] start, int distance) => start[distance]; +#endif + +#if DEBUG + internal struct ProbesTracker + { + internal int MaxProbe; + internal int[] Probes; + public ProbesTracker() + { + MaxProbe = 1; + Probes = new int[1]; + } + + // will output something like + // [Add] Probes abs max = 10, curr max = 6, all = [1: 180, 2: 103, 3: 59, 4: 23, 5: 3, 6: 1]; first 4 probes are 365 out of 369 + internal void DebugOutputProbes(string label) + { + Debug.Write($"[{label}] Probes abs max={MaxProbe}, curr max={Probes.Length}, all=["); + var first4probes = 0; + var allProbes = 0; + for (var i = 0; i < Probes.Length; i++) + { + var p = Probes[i]; + Debug.Write($"{(i == 0 ? "" : ", ")}{i + 1}: {p}"); + if (i < 4) + first4probes += p; + allProbes += p; + } + Debug.WriteLine($"]; first 4 probes are {first4probes} out of {allProbes}"); + } + + internal void DebugCollectAndOutputProbes(int probe, [CallerMemberName] string label = "") + { + Probes ??= []; + if (probe > Probes.Length) + { + if (probe > MaxProbe) + MaxProbe = probe; + Array.Resize(ref Probes, probe); + Probes[probe - 1] = 1; + DebugOutputProbes(label); + } + else + ++Probes[probe - 1]; + } + + internal void DebugReCollectAndOutputProbes(byte[] probes, [CallerMemberName] string label = "") + { + var newProbes = new int[1]; + for (var i = 0; i < probes.Length; ++i) + { + var p = probes[i]; + if (p == 0) continue; + if (p > MaxProbe) + MaxProbe = p; + if (p > newProbes.Length) + Array.Resize(ref newProbes, p); + ++newProbes[p - 1]; + } + Probes = newProbes; + DebugOutputProbes(label); + } + + internal void RemoveProbe(int probe) + { + ref var p = ref Probes[probe - 1]; + --p; + if (p == 0 && probe == Probes.Length) + { + Array.Resize(ref Probes, probe - 1); + if (MaxProbe == probe) + --MaxProbe; + } + } + } +#endif +} + +// todo: @improve ? how/where to add SIMD to improve CPU utilization but not losing perf for smaller sizes +/// +/// Fast and less-allocating hash map without thread safety nets. Please measure it in your own use case before use. +/// It is configurable in regard of hash calculation/equality via and +/// in regard of key-value storage via +/// +/// Features: +/// - Implemented as a struct so that the empty/default map does not allocate on heap +/// - Hashes and key-values are the separate collections enabling better cash locality and faster performance (data-oriented design) +/// - No SIMD for now to avoid complexity and costs for the smaller maps, so the map is more fit for the smaller sizes. +/// - Provides the "stable" enumeration of the entries in the added order +/// +/// +[DebuggerDisplay("Count={Count}")] +public struct FHashMap11 : IReadOnlyCollection> + where TEq : struct, IEq +{ +#if DEBUG + ProbesTracker _dbg = new(); +#endif + private byte _capacityBitShift; + internal int _count; + public byte[] Probes; + public int[] Hashes; + public K[] Keys; + public int[] ValueIndexes; + public V[] Values; + + /// Get the number of the key/hash entries in array. Should be more than + public int Capacity => 1 << _capacityBitShift; + + /// + public int Count => _count; + + /// Capacity calculates as `1 << capacityBitShift` + public FHashMap11(byte capacityBitShift) + { + _capacityBitShift = capacityBitShift; + + // the overflow tail to the hashes is the size of log2N where N==capacityBitShift, + // it is probably fine to have the check for the overflow of capacity because it will be mis-predicted only once at the end of loop (it even rarely for the lookup) + var cap = 1 << capacityBitShift; + Probes = new byte[cap]; + Hashes = new int[cap]; + Keys = new K[cap]; + ValueIndexes = new int[cap]; + Values = new V[cap]; + } + + /// Lookup for the key and get the associated value if the key is found + [MethodImpl((MethodImplOptions)256)] + public bool TryGetValue(K key, out V value) + { + if (Keys != null) + { + var hash = default(TEq).GetHashCode(key); + + var indexMask = (1 << _capacityBitShift) - 1; + var index = hash & indexMask; + +#if NET7_0_OR_GREATER + ref var probes = ref MemoryMarshal.GetArrayDataReference(Probes); + ref var hashes = ref MemoryMarshal.GetArrayDataReference(Hashes); + ref var keys = ref MemoryMarshal.GetArrayDataReference(Keys); + ref var valueIndexes = ref MemoryMarshal.GetArrayDataReference(ValueIndexes); +#else + var probes = Probes; + var hashes = Hashes; + var keys = Keys; + var valueIndexes = ValueIndexes; +#endif + var p = GetItem(ref probes, index); + + var probe = 1; + while (p >= probe) + { + if ((p == probe) & (GetItem(ref hashes, index) == hash) && + default(TEq).Equals(GetItem(ref keys, index), key)) + { + value = Values[GetItem(ref valueIndexes, index)]; + return true; + } + + index = (index + 1) & indexMask; + p = GetItem(ref probes, index); + ++probe; + } + } + + value = default; + return false; + } + + /// Lookup for the key and get the associated value or the default value if the key is not found + [MethodImpl((MethodImplOptions)256)] + public V GetValueOrDefault(K key, V defaultValue = default) => + TryGetValue(key, out var value) ? value : defaultValue; + + /// Gets the reference to the existing value of the provided key, or the default value to set for the newly added key. + [MethodImpl((MethodImplOptions)256)] + public ref V GetOrAddValueRef(K key) + { + var hash = default(TEq).GetHashCode(key); + + var indexMask = (1 << _capacityBitShift) - 1; + var currCount = _count; + + // if the free space is less than 1/8 of capacity (12.5%) then Resize + if (indexMask - currCount <= (indexMask >>> MinFreeCapacityShift)) + indexMask = ResizeHashes(indexMask); + + var index = hash & indexMask; + +#if NET7_0_OR_GREATER + ref var probes = ref MemoryMarshal.GetArrayDataReference(Probes); + ref var hashes = ref MemoryMarshal.GetArrayDataReference(Hashes); + ref var keys = ref MemoryMarshal.GetArrayDataReference(Keys); + ref var valueIndexes = ref MemoryMarshal.GetArrayDataReference(ValueIndexes); +#else + var probes = Probes; + var hashes = Hashes; + var keys = Keys; + var valueIndexes = ValueIndexes; +#endif + ref var pRef = ref GetItemRef(ref probes, index); + + // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions + var probe = (byte)1; + while (pRef >= probe) + { + // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too + if ((pRef == probe) & GetItem(ref hashes, index) == hash && + default(TEq).Equals(GetItem(ref keys, index), key)) + return ref Values[GetItem(ref valueIndexes, index)]; + + index = (index + 1) & indexMask; + pRef = ref GetItemRef(ref probes, index); + ++probe; + } + + // Nothing found, add a new entry and increase the count first + var valueIndex = currCount; + _count = currCount + 1; + + // 3. There is an empty slot to insert the new entry, so we can just insert it + insert: + if (pRef == 0) + { +#if DEBUG + _dbg.DebugCollectAndOutputProbes(probe, "Add in the empty slot"); +#endif + pRef = probe; + GetItemRef(ref hashes, index) = hash; + GetItemRef(ref keys, index) = key; + GetItemRef(ref valueIndexes, index) = valueIndex; + } + else // p < probe, e.g. 3, 4, 5, (5<6), (6==6), (7==7), (3<8), (0<4) + { + // 4. If the slot is not empty, then robin-hood the smaller (more valuable) probe + // and put the newly added item into the free slot. + // Then proceed with the robin-hooded key as-if it was a newly inserted key. + var rhProbe = pRef; + var rhHash = GetItem(ref hashes, index); + var rhKey = GetItem(ref keys, index); + var rhValueIndex = GetItem(ref valueIndexes, index); + +#if DEBUG + _dbg.DebugCollectAndOutputProbes(probe, "Add into the occupied slot after RobinHood it"); +#endif + // and set the new entry to the current slot + pRef = probe; + GetItemRef(ref hashes, index) = hash; + GetItemRef(ref keys, index) = key; + GetItemRef(ref valueIndexes, index) = valueIndex; + + // 5. Now treat the robin-hooded entry as a newly inserted entry + probe = rhProbe; + hash = rhHash; + key = rhKey; + valueIndex = rhValueIndex; + + while (true) + { + index = (index + 1) & indexMask; + pRef = ref GetItemRef(ref probes, index); + if (pRef < ++probe) + goto insert; + } + } + + if (currCount == Values.Length) + Array.Resize(ref Values, Values.Length << 1); + return ref Values[currCount]; + } + + /// Same as `GetOrAddValueRef` but provides the value to add or override for the existing key + [MethodImpl((MethodImplOptions)256)] + public void AddOrUpdate(K key, in V value) => + GetOrAddValueRef(key) = value; + + // /// Removes the hash and entry of the provided key or returns + // [MethodImpl((MethodImplOptions)256)] + // public bool TryRemove(K key) + // { + // var hash = default(TEq).GetHashCode(key); + + // var indexMask = (1 << _capacityBitShift) - 1; + // var hashIndex = hash & indexMask; + + // #if NET7_0_OR_GREATER + // ref var keyEntries = ref MemoryMarshal.GetArrayDataReference(KeyEntries); + // #else + // var keyEntries = KeyEntries; + // #endif + // ref var h = ref GetItemRef(ref keyEntries, hashIndex); + + // var removed = false; + + // // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions + // var probe = 1; + // while (h.Probe >= probe) + // { + // // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too + // if ((h.Probe == probe) & (h.Hash == hash) && default(TEq).Equals(h.Key, key)) + // { + // removed = true; + + // --_count; + // Values[h.Index] = default; // todo: @perf fragmentation issue, add the slot to the free list? + // h = default; + // #if DEBUG + // _dbg.RemoveProbe(probe); + // #endif + // break; + // } + // h = ref GetItemRef(ref keyEntries, ++hashIndex & indexMask); + // ++probe; + // } + + // if (!removed) + // return false; + + // ref var emptied = ref h; + // h = ref GetItemRef(ref keyEntries, ++hashIndex & indexMask); + + // // move the next hash into the emptied slot until the next hash is empty or ideally positioned (hash is 0 or probe is 1) + // while (h.Probe > 1) + // { + // emptied = h; + // emptied.Probe -= 1; // decrease the probe count by one cause we moving the hash closer to the ideal index + // h = default; + + // emptied = ref h; + // h = ref GetItemRef(ref keyEntries, ++hashIndex & indexMask); + // } + // return true; + // } + + internal int ResizeHashes(int indexMask) + { + if (indexMask == 0) + { + _capacityBitShift = MinCapacityBits; + var cap = 1 << MinCapacityBits; + Probes = new byte[cap]; + Hashes = new int[cap]; + Keys = new K[cap]; + ValueIndexes = new int[cap]; + Values = new V[cap]; +#if DEBUG + Debug.WriteLine($"[ResizeHashes] new empty hashes {1} -> {Probes.Length}"); +#endif + return cap - 1; + } + + var oldCap = indexMask + 1; + var newCap = oldCap << 1; + var newProbes = new byte[newCap]; + var newHashes = new int[newCap]; + var newKeys = new K[newCap]; + var newValueIndexes = new int[newCap]; + var newIndexMask = newCap - 1; + +#if NET7_0_OR_GREATER + ref var newProbesRef = ref MemoryMarshal.GetArrayDataReference(newProbes); + ref var oldProbesRef = ref MemoryMarshal.GetArrayDataReference(Probes); + var oldProbe = oldProbesRef; + + ref var newHashesRef = ref MemoryMarshal.GetArrayDataReference(newHashes); + ref var oldHashesRef = ref MemoryMarshal.GetArrayDataReference(Hashes); + + ref var newKeysRef = ref MemoryMarshal.GetArrayDataReference(newKeys); + ref var oldKeysRef = ref MemoryMarshal.GetArrayDataReference(Keys); + + ref var newValueIndexesRef = ref MemoryMarshal.GetArrayDataReference(newValueIndexes); + ref var oldValueIndexesRef = ref MemoryMarshal.GetArrayDataReference(ValueIndexes); +#else + var newProbesRef = newProbes; + var oldProbesRef = Probes; + var oldProbe = oldProbesRef[0]; + + var newHashesRef = newHashes; + var oldHashesRef = Hashes; + + var newKeysRef = newKeys; + var oldKeysRef = Keys; + + var newValueIndexesRef = newValueIndexes; + var oldValueIndexesRef = ValueIndexes; +#endif + // Overflow segment is wrapped-around hashes and! the hashes at the beginning robin-hooded by the wrapped-around hashes + // so we skip them to start from first ideal or empty entry + var i = 0; + while (oldProbe > 1) + oldProbe = GetItem(ref oldProbesRef, ++i); + var oldCapWithOverflowSegment = i + oldCap; + + while (true) + { + if (oldProbe != 0) + { + var hash = GetItem(ref oldHashesRef, i & indexMask); + var newIndex = hash & newIndexMask; + + // no need for robin-hooding because we already did it for the old hashes and + // now just sparse the hashes which are already in order into the new array + var newProbe = (byte)1; + ref var newProbeRef = ref GetItemRef(ref newProbesRef, newIndex); + while (newProbeRef != 0) + { + newIndex = (newIndex + 1) & newIndexMask; + newProbeRef = ref GetItemRef(ref newProbesRef, newIndex); + ++newProbe; + } + newProbeRef = newProbe; + GetItemRef(ref newHashesRef, newIndex) = hash; + GetItemRef(ref newKeysRef, newIndex) = GetItem(ref oldKeysRef, i & indexMask); + GetItemRef(ref newValueIndexesRef, newIndex) = GetItem(ref oldValueIndexesRef, i & indexMask); + } + if (++i >= oldCapWithOverflowSegment) + break; + + oldProbe = GetItem(ref oldProbesRef, i & indexMask); + } +#if DEBUG + Debug.WriteLine($"[ResizeHashes] {oldCap} -> {newProbes.Length}"); + _dbg.DebugReCollectAndOutputProbes(newProbes, "ResizeHashes"); +#endif + ++_capacityBitShift; + Probes = newProbes; + Hashes = newHashes; + Keys = newKeys; + ValueIndexes = newValueIndexes; + return newIndexMask; + } + + /// + [MethodImpl((MethodImplOptions)256)] + public Enumerator GetEnumerator() => new Enumerator(Probes, ValueIndexes, Keys, Values, _count); + + /// + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// Enumerator of the entries in the order of their addition to the map + public struct Enumerator : IEnumerator> + { + private int _probeIndex; + private int _index; + private KeyValuePair _current; + private readonly byte[] _probes; + private readonly int[] _valueIndexes; + private readonly K[] _keys; + private readonly V[] _values; + private int _count; + internal Enumerator(byte[] probes, int[] valueIndexes, K[] keys, V[] values, int count) + { + _probeIndex = 0; + _index = 0; + _current = default; + _probes = probes; + _valueIndexes = valueIndexes; + _keys = keys; + _values = values; + _count = count; + } + + /// Move to the next entry in the order of their addition to the map + [MethodImpl((MethodImplOptions)256)] + public bool MoveNext() + { + if (_index < _count) + { + // skip empty hashes + var i = _probeIndex; + while (_probes[i] == 0) + ++i; + _probeIndex = i; + _current = new KeyValuePair(_keys[i], _values[_valueIndexes[i]]); + + ++_probeIndex; + ++_index; + return true; + } + + _current = default; + return false; + } + + public KeyValuePair Current => _current; + object IEnumerator.Current => _current; + + void IEnumerator.Reset() + { + _probeIndex = 0; + _index = 0; + _current = default; + } + + public void Dispose() { } + } +} diff --git a/playground/Experiments/UnscopedRefAttribute.cs b/playground/Experiments/UnscopedRefAttribute.cs new file mode 100644 index 00000000..167d2d58 --- /dev/null +++ b/playground/Experiments/UnscopedRefAttribute.cs @@ -0,0 +1,7 @@ +#if !NET7_0_OR_GREATER +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(System.AttributeTargets.Method | System.AttributeTargets.Parameter | System.AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + public sealed class UnscopedRefAttribute : Attribute { } +} +#endif diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 48dcf60d..d1545abc 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -13,258 +13,266 @@ using System.Runtime.CompilerServices; using FHashMap91TypeString = ImTools.Experiments.FHashMap91, ImTools.Experiments.FHashMap91.SingleArrayEntries>>; -using SmallMapTypeString = ImTools.SmallMap, ImTools.SmallMap.SingleArrayEntries>>; -using FHashMapTypeString = FastExpressionCompiler.ImTools.FHashMap, FastExpressionCompiler.ImTools.FHashMap.SingleArrayEntries>>; +using SmallMapTypeString = ImTools.HSmallMap, ImTools.HSmallMap.SingleArrayEntries>>; +using FecSmallMapTypeString = FastExpressionCompiler.ImTools.SmallMap16>; +using FecSmallMapTypeString_SingleArrEntries = FastExpressionCompiler.ImTools.SmallMap16_SingleArrEntries>; +using BenchmarkDotNet.Order; +using ImTools.Experiments; #nullable disable #pragma warning disable CS0649, CS0169 -namespace Playground +namespace Playground; + +public class ImHashMapBenchmarks { - public class ImHashMapBenchmarks - { - private static readonly Type[] _keys = typeof(Dictionary<,>).Assembly.GetTypes().Take(1000).ToArray(); + private static readonly Type[] _allKeys = typeof(List<>).Assembly.GetTypes().Take(2000).ToArray(); + private static readonly Type[] _keys = _allKeys.Take(1000).ToArray(); + private static readonly Type[] _missingKeys = _allKeys.TakeLast(1000).ToArray(); - public struct TypeVal : IEquatable - { - public static implicit operator TypeVal(Type t) => new TypeVal(t); + // get half of the random _keys for the lookup, by selecting half of the random indexes from keys + private static readonly Random _seed = new Random(42); - public readonly Type Type; - public TypeVal(Type type) => Type = type; - public bool Equals(TypeVal other) => Type == other.Type; - public override bool Equals(object obj) => !ReferenceEquals(null, obj) && obj is TypeVal other && Equals(other); - public override int GetHashCode() => Type.GetHashCode(); - } + public struct TypeVal : IEquatable + { + public static implicit operator TypeVal(Type t) => new TypeVal(t); - [HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchMispredictions, HardwareCounter.BranchInstructions)] - [MemoryDiagnoser] - public class Populate - { - /* - ## 15.01.2019: + public readonly Type Type; + public TypeVal(Type type) => Type = type; + public bool Equals(TypeVal other) => Type == other.Type; + public override bool Equals(object obj) => !ReferenceEquals(null, obj) && obj is TypeVal other && Equals(other); + public override int GetHashCode() => Type.GetHashCode(); + } + + [HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchMispredictions, HardwareCounter.BranchInstructions)] + [MemoryDiagnoser] + public class Populate + { + /* + ## 15.01.2019: - Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------- |---------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate | 15.84 us | 0.1065 us | 0.0944 us | 1.00 | 0.00 | 7.3242 | - | - | 33.87 KB | - AddOrUpdate_v1 | 27.00 us | 0.1792 us | 0.1588 us | 1.71 | 0.02 | 7.7515 | - | - | 35.77 KB | + Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------- |---------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + AddOrUpdate | 15.84 us | 0.1065 us | 0.0944 us | 1.00 | 0.00 | 7.3242 | - | - | 33.87 KB | + AddOrUpdate_v1 | 27.00 us | 0.1792 us | 0.1588 us | 1.71 | 0.02 | 7.7515 | - | - | 35.77 KB | - - ## 16.01.2019: Total test against ImHashMap V1, System ImmutableDictionary and ConcurrentDictionary + + ## 16.01.2019: Total test against ImHashMap V1, System ImmutableDictionary and ConcurrentDictionary BenchmarkDotNet=v0.11.3, OS=Windows 10.0.17134.523 (1803/April2018Update/Redstone4) Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores Frequency=2156249 Hz, Resolution=463.7683 ns, Timer=TSC .NET Core SDK=2.2.100 - [Host] : .NET Core 2.1.6 (CoreCLR 4.6.27019.06, CoreFX 4.6.27019.05), 64bit RyuJIT - DefaultJob : .NET Core 2.1.6 (CoreCLR 4.6.27019.06, CoreFX 4.6.27019.05), 64bit RyuJIT +[Host] : .NET Core 2.1.6 (CoreCLR 4.6.27019.06, CoreFX 4.6.27019.05), 64bit RyuJIT +DefaultJob : .NET Core 2.1.6 (CoreCLR 4.6.27019.06, CoreFX 4.6.27019.05), 64bit RyuJIT - Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |---------------:|--------------:|--------------:|---------------:|------:|--------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v1 | 10 | 987.6 ns | 36.53 ns | 99.99 ns | 934.3 ns | 0.94 | 0.17 | 0.5589 | - | - | 2.58 KB | - AddOrUpdate | 10 | 1,067.5 ns | 50.43 ns | 136.33 ns | 1,073.5 ns | 1.00 | 0.00 | 0.4044 | - | - | 1.87 KB | - ConcurrentDict | 10 | 1,943.9 ns | 92.31 ns | 81.83 ns | 1,921.3 ns | 1.85 | 0.25 | 0.6371 | - | - | 2.95 KB | - AddOrUpdate_v2 | 10 | 2,688.3 ns | 229.79 ns | 677.55 ns | 2,342.9 ns | 2.50 | 0.77 | 0.4349 | - | - | 2.02 KB | - ImmutableDict | 10 | 5,903.1 ns | 749.28 ns | 801.72 ns | 5,607.8 ns | 5.66 | 1.06 | 0.5875 | - | - | 2.73 KB | - | | | | | | | | | | | | - ConcurrentDict | 100 | 14,476.1 ns | 1,184.05 ns | 1,215.93 ns | 14,193.3 ns | 0.48 | 0.21 | 3.6011 | 0.0305 | - | 16.66 KB | - AddOrUpdate_v1 | 100 | 16,999.4 ns | 1,522.75 ns | 4,441.93 ns | 14,281.8 ns | 0.59 | 0.25 | 8.4686 | - | - | 39.05 KB | - AddOrUpdate_v2 | 100 | 28,695.4 ns | 41.78 ns | 32.62 ns | 28,697.6 ns | 0.94 | 0.33 | 7.7515 | - | - | 35.81 KB | - AddOrUpdate | 100 | 31,854.4 ns | 2,882.03 ns | 8,497.72 ns | 36,547.9 ns | 1.00 | 0.00 | 7.3242 | - | - | 33.91 KB | - ImmutableDict | 100 | 89,602.2 ns | 1,873.19 ns | 2,229.89 ns | 88,767.5 ns | 2.98 | 1.05 | 9.3994 | - | - | 43.68 KB | - | | | | | | | | | | | | - ConcurrentDict | 1000 | 219,064.7 ns | 559.14 ns | 466.91 ns | 218,894.7 ns | 0.69 | 0.01 | 49.3164 | 17.8223 | - | 254.29 KB | - AddOrUpdate_v1 | 1000 | 297,651.6 ns | 1,073.37 ns | 838.02 ns | 297,421.1 ns | 0.93 | 0.01 | 120.6055 | 3.4180 | - | 556.41 KB | - AddOrUpdate | 1000 | 319,478.3 ns | 2,768.11 ns | 2,161.16 ns | 319,079.8 ns | 1.00 | 0.00 | 113.2813 | 0.9766 | - | 526.48 KB | - AddOrUpdate_v2 | 1000 | 615,321.8 ns | 72,410.43 ns | 213,503.80 ns | 467,207.0 ns | 1.97 | 0.66 | 118.6523 | 0.4883 | - | 547.3 KB | - ImmutableDict | 1000 | 1,516,613.7 ns | 107,376.35 ns | 290,298.20 ns | 1,387,325.2 ns | 4.95 | 1.19 | 140.6250 | 1.9531 | - | 648.02 KB | +AddOrUpdate_v1 | 10 | 987.6 ns | 36.53 ns | 99.99 ns | 934.3 ns | 0.94 | 0.17 | 0.5589 | - | - | 2.58 KB | +AddOrUpdate | 10 | 1,067.5 ns | 50.43 ns | 136.33 ns | 1,073.5 ns | 1.00 | 0.00 | 0.4044 | - | - | 1.87 KB | +ConcurrentDict | 10 | 1,943.9 ns | 92.31 ns | 81.83 ns | 1,921.3 ns | 1.85 | 0.25 | 0.6371 | - | - | 2.95 KB | +AddOrUpdate_v2 | 10 | 2,688.3 ns | 229.79 ns | 677.55 ns | 2,342.9 ns | 2.50 | 0.77 | 0.4349 | - | - | 2.02 KB | +ImmutableDict | 10 | 5,903.1 ns | 749.28 ns | 801.72 ns | 5,607.8 ns | 5.66 | 1.06 | 0.5875 | - | - | 2.73 KB | + | | | | | | | | | | | | +ConcurrentDict | 100 | 14,476.1 ns | 1,184.05 ns | 1,215.93 ns | 14,193.3 ns | 0.48 | 0.21 | 3.6011 | 0.0305 | - | 16.66 KB | +AddOrUpdate_v1 | 100 | 16,999.4 ns | 1,522.75 ns | 4,441.93 ns | 14,281.8 ns | 0.59 | 0.25 | 8.4686 | - | - | 39.05 KB | +AddOrUpdate_v2 | 100 | 28,695.4 ns | 41.78 ns | 32.62 ns | 28,697.6 ns | 0.94 | 0.33 | 7.7515 | - | - | 35.81 KB | +AddOrUpdate | 100 | 31,854.4 ns | 2,882.03 ns | 8,497.72 ns | 36,547.9 ns | 1.00 | 0.00 | 7.3242 | - | - | 33.91 KB | +ImmutableDict | 100 | 89,602.2 ns | 1,873.19 ns | 2,229.89 ns | 88,767.5 ns | 2.98 | 1.05 | 9.3994 | - | - | 43.68 KB | + | | | | | | | | | | | | +ConcurrentDict | 1000 | 219,064.7 ns | 559.14 ns | 466.91 ns | 218,894.7 ns | 0.69 | 0.01 | 49.3164 | 17.8223 | - | 254.29 KB | +AddOrUpdate_v1 | 1000 | 297,651.6 ns | 1,073.37 ns | 838.02 ns | 297,421.1 ns | 0.93 | 0.01 | 120.6055 | 3.4180 | - | 556.41 KB | +AddOrUpdate | 1000 | 319,478.3 ns | 2,768.11 ns | 2,161.16 ns | 319,079.8 ns | 1.00 | 0.00 | 113.2813 | 0.9766 | - | 526.48 KB | +AddOrUpdate_v2 | 1000 | 615,321.8 ns | 72,410.43 ns | 213,503.80 ns | 467,207.0 ns | 1.97 | 0.66 | 118.6523 | 0.4883 | - | 547.3 KB | +ImmutableDict | 1000 | 1,516,613.7 ns | 107,376.35 ns | 290,298.20 ns | 1,387,325.2 ns | 4.95 | 1.19 | 140.6250 | 1.9531 | - | 648.02 KB | ## 21.01.2019 - all versions compared - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v1 | 30 | 3.326 us | 0.0098 us | 0.0092 us | 0.84 | 0.01 | 2.0409 | - | - | 9.42 KB | - AddOrUpdate_v2 | 30 | 3.515 us | 0.0700 us | 0.0688 us | 0.89 | 0.02 | 1.9302 | - | - | 8.91 KB | - AddOrUpdate | 30 | 3.945 us | 0.0261 us | 0.0231 us | 1.00 | 0.00 | 2.1591 | - | - | 9.98 KB | - AddOrUpdate_v3 | 30 | 4.607 us | 0.0875 us | 0.0819 us | 1.17 | 0.02 | 1.7624 | - | - | 8.13 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 150 | 24.883 us | 0.4840 us | 0.4527 us | 0.86 | 0.02 | 13.5193 | - | - | 62.39 KB | - AddOrUpdate_v2 | 150 | 26.862 us | 0.5042 us | 0.4717 us | 0.92 | 0.02 | 13.7024 | - | - | 63.2 KB | - AddOrUpdate | 150 | 29.054 us | 0.1205 us | 0.1127 us | 1.00 | 0.00 | 15.1978 | - | - | 70.17 KB | - AddOrUpdate_v3 | 150 | 35.585 us | 0.3740 us | 0.3498 us | 1.22 | 0.01 | 12.5732 | - | - | 58.05 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 500 | 124.728 us | 0.7225 us | 0.6759 us | 0.91 | 0.01 | 54.4434 | - | - | 251.95 KB | - AddOrUpdate_v2 | 500 | 128.650 us | 1.4938 us | 1.3973 us | 0.94 | 0.01 | 58.1055 | 0.2441 | - | 267.97 KB | - AddOrUpdate | 500 | 137.325 us | 1.9010 us | 1.7782 us | 1.00 | 0.00 | 63.2324 | 0.2441 | - | 291.42 KB | - AddOrUpdate_v3 | 500 | 166.994 us | 1.7109 us | 1.6004 us | 1.22 | 0.02 | 52.7344 | - | - | 243.81 KB | +AddOrUpdate_v1 | 30 | 3.326 us | 0.0098 us | 0.0092 us | 0.84 | 0.01 | 2.0409 | - | - | 9.42 KB | +AddOrUpdate_v2 | 30 | 3.515 us | 0.0700 us | 0.0688 us | 0.89 | 0.02 | 1.9302 | - | - | 8.91 KB | +AddOrUpdate | 30 | 3.945 us | 0.0261 us | 0.0231 us | 1.00 | 0.00 | 2.1591 | - | - | 9.98 KB | +AddOrUpdate_v3 | 30 | 4.607 us | 0.0875 us | 0.0819 us | 1.17 | 0.02 | 1.7624 | - | - | 8.13 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 150 | 24.883 us | 0.4840 us | 0.4527 us | 0.86 | 0.02 | 13.5193 | - | - | 62.39 KB | +AddOrUpdate_v2 | 150 | 26.862 us | 0.5042 us | 0.4717 us | 0.92 | 0.02 | 13.7024 | - | - | 63.2 KB | +AddOrUpdate | 150 | 29.054 us | 0.1205 us | 0.1127 us | 1.00 | 0.00 | 15.1978 | - | - | 70.17 KB | +AddOrUpdate_v3 | 150 | 35.585 us | 0.3740 us | 0.3498 us | 1.22 | 0.01 | 12.5732 | - | - | 58.05 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 500 | 124.728 us | 0.7225 us | 0.6759 us | 0.91 | 0.01 | 54.4434 | - | - | 251.95 KB | +AddOrUpdate_v2 | 500 | 128.650 us | 1.4938 us | 1.3973 us | 0.94 | 0.01 | 58.1055 | 0.2441 | - | 267.97 KB | +AddOrUpdate | 500 | 137.325 us | 1.9010 us | 1.7782 us | 1.00 | 0.00 | 63.2324 | 0.2441 | - | 291.42 KB | +AddOrUpdate_v3 | 500 | 166.994 us | 1.7109 us | 1.6004 us | 1.22 | 0.02 | 52.7344 | - | - | 243.81 KB | ## Inlining With and removing not necessary call to Balance in case of Update. - - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v1 | 30 | 3.323 us | 0.0121 us | 0.0107 us | 0.91 | 0.00 | 2.0409 | - | - | 9.42 KB | - AddOrUpdate | 30 | 3.647 us | 0.0111 us | 0.0093 us | 1.00 | 0.00 | 2.0409 | - | - | 9.42 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 150 | 24.605 us | 0.2023 us | 0.1690 us | 0.92 | 0.02 | 13.5193 | - | - | 62.39 KB | - AddOrUpdate | 150 | 26.832 us | 0.5300 us | 0.5443 us | 1.00 | 0.00 | 13.5193 | - | - | 62.39 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 500 | 121.799 us | 0.9309 us | 0.8252 us | 0.92 | 0.01 | 54.4434 | - | - | 251.95 KB | - AddOrUpdate | 500 | 132.886 us | 2.3470 us | 2.0805 us | 1.00 | 0.00 | 54.4434 | - | - | 251.95 KB | +AddOrUpdate_v1 | 30 | 3.323 us | 0.0121 us | 0.0107 us | 0.91 | 0.00 | 2.0409 | - | - | 9.42 KB | +AddOrUpdate | 30 | 3.647 us | 0.0111 us | 0.0093 us | 1.00 | 0.00 | 2.0409 | - | - | 9.42 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 150 | 24.605 us | 0.2023 us | 0.1690 us | 0.92 | 0.02 | 13.5193 | - | - | 62.39 KB | +AddOrUpdate | 150 | 26.832 us | 0.5300 us | 0.5443 us | 1.00 | 0.00 | 13.5193 | - | - | 62.39 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 500 | 121.799 us | 0.9309 us | 0.8252 us | 0.92 | 0.01 | 54.4434 | - | - | 251.95 KB | +AddOrUpdate | 500 | 132.886 us | 2.3470 us | 2.0805 us | 1.00 | 0.00 | 54.4434 | - | - | 251.95 KB | ## Special fast logic for adding to empty branch. - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v1 | 30 | 3.369 us | 0.0102 us | 0.0090 us | 0.94 | 0.01 | 2.0409 | - | - | 9.42 KB | - AddOrUpdate | 30 | 3.587 us | 0.0244 us | 0.0228 us | 1.00 | 0.00 | 2.0409 | - | - | 9.42 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 150 | 25.025 us | 0.4927 us | 0.4609 us | 0.95 | 0.02 | 13.5193 | - | - | 62.39 KB | - AddOrUpdate | 150 | 26.235 us | 0.1058 us | 0.0989 us | 1.00 | 0.00 | 13.5193 | - | - | 62.39 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 500 | 126.106 us | 1.0204 us | 0.9545 us | 1.00 | 0.01 | 54.4434 | - | - | 251.95 KB | - AddOrUpdate | 500 | 126.844 us | 0.8788 us | 0.7339 us | 1.00 | 0.00 | 54.4434 | - | - | 251.95 KB | +AddOrUpdate_v1 | 30 | 3.369 us | 0.0102 us | 0.0090 us | 0.94 | 0.01 | 2.0409 | - | - | 9.42 KB | +AddOrUpdate | 30 | 3.587 us | 0.0244 us | 0.0228 us | 1.00 | 0.00 | 2.0409 | - | - | 9.42 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 150 | 25.025 us | 0.4927 us | 0.4609 us | 0.95 | 0.02 | 13.5193 | - | - | 62.39 KB | +AddOrUpdate | 150 | 26.235 us | 0.1058 us | 0.0989 us | 1.00 | 0.00 | 13.5193 | - | - | 62.39 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 500 | 126.106 us | 1.0204 us | 0.9545 us | 1.00 | 0.01 | 54.4434 | - | - | 251.95 KB | +AddOrUpdate | 500 | 126.844 us | 0.8788 us | 0.7339 us | 1.00 | 0.00 | 54.4434 | - | - | 251.95 KB | ## Removing not necessary imbalanced tree creation before balance - first memory win. - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate | 30 | 3.357 us | 0.0163 us | 0.0145 us | 1.00 | 1.9417 | - | - | 8.95 KB | - AddOrUpdate_v1 | 30 | 3.359 us | 0.0124 us | 0.0116 us | 1.00 | 2.0409 | - | - | 9.42 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 150 | 24.513 us | 0.0420 us | 0.0393 us | 0.98 | 13.5193 | - | - | 62.39 KB | - AddOrUpdate | 150 | 25.074 us | 0.1160 us | 0.1085 us | 1.00 | 12.9395 | 0.0305 | - | 59.72 KB | - | | | | | | | | | | - AddOrUpdate | 500 | 122.624 us | 0.6553 us | 0.6130 us | 1.00 | 52.4902 | 0.2441 | - | 242.06 KB | - AddOrUpdate_v1 | 500 | 122.656 us | 0.8018 us | 0.7500 us | 1.00 | 54.4434 | - | - | 251.95 KB | +AddOrUpdate | 30 | 3.357 us | 0.0163 us | 0.0145 us | 1.00 | 1.9417 | - | - | 8.95 KB | +AddOrUpdate_v1 | 30 | 3.359 us | 0.0124 us | 0.0116 us | 1.00 | 2.0409 | - | - | 9.42 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 150 | 24.513 us | 0.0420 us | 0.0393 us | 0.98 | 13.5193 | - | - | 62.39 KB | +AddOrUpdate | 150 | 25.074 us | 0.1160 us | 0.1085 us | 1.00 | 12.9395 | 0.0305 | - | 59.72 KB | + | | | | | | | | | | +AddOrUpdate | 500 | 122.624 us | 0.6553 us | 0.6130 us | 1.00 | 52.4902 | 0.2441 | - | 242.06 KB | +AddOrUpdate_v1 | 500 | 122.656 us | 0.8018 us | 0.7500 us | 1.00 | 54.4434 | - | - | 251.95 KB | ## More variety to benchmark input - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-------------:|-------------:|-------------:|------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate | 5 | 472.4 ns | 2.139 ns | 1.786 ns | 1.00 | 0.2537 | - | - | 1.17 KB | - AddOrUpdate_v1 | 5 | 488.1 ns | 2.258 ns | 2.112 ns | 1.03 | 0.2737 | - | - | 1.27 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 40 | 4,855.4 ns | 19.395 ns | 17.194 ns | 0.98 | 2.9678 | - | - | 13.69 KB | - AddOrUpdate | 40 | 4,974.1 ns | 17.098 ns | 15.157 ns | 1.00 | 2.8000 | - | - | 12.94 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 200 | 35,267.4 ns | 100.767 ns | 84.145 ns | 0.98 | 18.7378 | 0.0610 | - | 86.53 KB | - AddOrUpdate | 200 | 35,874.3 ns | 173.786 ns | 162.560 ns | 1.00 | 18.0054 | - | - | 83.02 KB | - | | | | | | | | | | - AddOrUpdate | 1000 | 302,900.4 ns | 1,116.252 ns | 1,044.143 ns | 1.00 | 115.7227 | 2.4414 | - | 535.08 KB | - AddOrUpdate_v1 | 1000 | 303,552.8 ns | 906.769 ns | 803.827 ns | 1.00 | 120.6055 | 3.4180 | - | 556.41 KB | +AddOrUpdate | 5 | 472.4 ns | 2.139 ns | 1.786 ns | 1.00 | 0.2537 | - | - | 1.17 KB | +AddOrUpdate_v1 | 5 | 488.1 ns | 2.258 ns | 2.112 ns | 1.03 | 0.2737 | - | - | 1.27 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 40 | 4,855.4 ns | 19.395 ns | 17.194 ns | 0.98 | 2.9678 | - | - | 13.69 KB | +AddOrUpdate | 40 | 4,974.1 ns | 17.098 ns | 15.157 ns | 1.00 | 2.8000 | - | - | 12.94 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 200 | 35,267.4 ns | 100.767 ns | 84.145 ns | 0.98 | 18.7378 | 0.0610 | - | 86.53 KB | +AddOrUpdate | 200 | 35,874.3 ns | 173.786 ns | 162.560 ns | 1.00 | 18.0054 | - | - | 83.02 KB | + | | | | | | | | | | +AddOrUpdate | 1000 | 302,900.4 ns | 1,116.252 ns | 1,044.143 ns | 1.00 | 115.7227 | 2.4414 | - | 535.08 KB | +AddOrUpdate_v1 | 1000 | 303,552.8 ns | 906.769 ns | 803.827 ns | 1.00 | 120.6055 | 3.4180 | - | 556.41 KB | ## Remove unnecessary temporary left leaf(y) branch creation before balancing - memory win - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-------------:|-------------:|-------------:|------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v1 | 5 | 491.4 ns | 2.121 ns | 1.984 ns | 1.00 | 0.2737 | - | - | 1.27 KB | - AddOrUpdate | 5 | 493.4 ns | 2.199 ns | 2.057 ns | 1.00 | 0.2537 | - | - | 1.17 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 40 | 4,871.0 ns | 26.907 ns | 23.852 ns | 0.98 | 2.9678 | - | - | 13.69 KB | - AddOrUpdate | 40 | 4,949.1 ns | 10.957 ns | 9.713 ns | 1.00 | 2.7542 | - | - | 12.7 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 200 | 35,540.5 ns | 271.146 ns | 240.364 ns | 0.95 | 18.7378 | 0.0610 | - | 86.53 KB | - AddOrUpdate | 200 | 37,344.9 ns | 127.081 ns | 118.871 ns | 1.00 | 17.6392 | - | - | 81.38 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 1000 | 308,033.2 ns | 1,643.015 ns | 1,536.877 ns | 0.98 | 120.6055 | 3.4180 | - | 556.41 KB | - AddOrUpdate | 1000 | 314,370.2 ns | 1,781.632 ns | 1,666.540 ns | 1.00 | 113.7695 | 0.4883 | - | 525.09 KB | +AddOrUpdate_v1 | 5 | 491.4 ns | 2.121 ns | 1.984 ns | 1.00 | 0.2737 | - | - | 1.27 KB | +AddOrUpdate | 5 | 493.4 ns | 2.199 ns | 2.057 ns | 1.00 | 0.2537 | - | - | 1.17 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 40 | 4,871.0 ns | 26.907 ns | 23.852 ns | 0.98 | 2.9678 | - | - | 13.69 KB | +AddOrUpdate | 40 | 4,949.1 ns | 10.957 ns | 9.713 ns | 1.00 | 2.7542 | - | - | 12.7 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 200 | 35,540.5 ns | 271.146 ns | 240.364 ns | 0.95 | 18.7378 | 0.0610 | - | 86.53 KB | +AddOrUpdate | 200 | 37,344.9 ns | 127.081 ns | 118.871 ns | 1.00 | 17.6392 | - | - | 81.38 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 1000 | 308,033.2 ns | 1,643.015 ns | 1,536.877 ns | 0.98 | 120.6055 | 3.4180 | - | 556.41 KB | +AddOrUpdate | 1000 | 314,370.2 ns | 1,781.632 ns | 1,666.540 ns | 1.00 | 113.7695 | 0.4883 | - | 525.09 KB | ## Remove unnecessary temporary right leaf(y) branch creation before balancing - memory win - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-------------:|-------------:|-------------:|------:|--------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v1 | 5 | 489.0 ns | 2.395 ns | 2.241 ns | 0.99 | 0.01 | 0.2737 | - | - | 1.27 KB | - AddOrUpdate | 5 | 496.0 ns | 3.869 ns | 3.619 ns | 1.00 | 0.00 | 0.2432 | - | - | 1.13 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 40 | 4,873.1 ns | 35.313 ns | 33.032 ns | 0.91 | 0.01 | 2.9678 | - | - | 13.69 KB | - AddOrUpdate | 40 | 5,346.9 ns | 18.590 ns | 16.480 ns | 1.00 | 0.00 | 2.6779 | - | - | 12.38 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 200 | 36,484.4 ns | 309.583 ns | 274.437 ns | 0.90 | 0.01 | 18.7378 | 0.0610 | - | 86.53 KB | - AddOrUpdate | 200 | 40,641.9 ns | 628.973 ns | 588.342 ns | 1.00 | 0.00 | 17.1509 | 0.0610 | - | 79.08 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 1000 | 316,678.8 ns | 5,722.780 ns | 5,353.092 ns | 0.98 | 0.02 | 120.6055 | 3.4180 | - | 556.41 KB | - AddOrUpdate | 1000 | 323,403.6 ns | 1,483.940 ns | 1,315.474 ns | 1.00 | 0.00 | 111.8164 | 32.7148 | - | 515.39 KB | +AddOrUpdate_v1 | 5 | 489.0 ns | 2.395 ns | 2.241 ns | 0.99 | 0.01 | 0.2737 | - | - | 1.27 KB | +AddOrUpdate | 5 | 496.0 ns | 3.869 ns | 3.619 ns | 1.00 | 0.00 | 0.2432 | - | - | 1.13 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 40 | 4,873.1 ns | 35.313 ns | 33.032 ns | 0.91 | 0.01 | 2.9678 | - | - | 13.69 KB | +AddOrUpdate | 40 | 5,346.9 ns | 18.590 ns | 16.480 ns | 1.00 | 0.00 | 2.6779 | - | - | 12.38 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 200 | 36,484.4 ns | 309.583 ns | 274.437 ns | 0.90 | 0.01 | 18.7378 | 0.0610 | - | 86.53 KB | +AddOrUpdate | 200 | 40,641.9 ns | 628.973 ns | 588.342 ns | 1.00 | 0.00 | 17.1509 | 0.0610 | - | 79.08 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 1000 | 316,678.8 ns | 5,722.780 ns | 5,353.092 ns | 0.98 | 0.02 | 120.6055 | 3.4180 | - | 556.41 KB | +AddOrUpdate | 1000 | 323,403.6 ns | 1,483.940 ns | 1,315.474 ns | 1.00 | 0.00 | 111.8164 | 32.7148 | - | 515.39 KB | ## Parity with v1 via handling one more special case - where `Height == 1` - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-------------:|-------------:|-------------:|------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate | 5 | 480.3 ns | 3.645 ns | 3.410 ns | 1.00 | 0.2432 | - | - | 1.13 KB | - AddOrUpdate_v1 | 5 | 487.1 ns | 2.547 ns | 2.382 ns | 1.01 | 0.2737 | - | - | 1.27 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 40 | 4,844.9 ns | 23.494 ns | 21.976 ns | 0.99 | 2.9678 | - | - | 13.69 KB | - AddOrUpdate | 40 | 4,908.8 ns | 29.374 ns | 27.477 ns | 1.00 | 2.6779 | - | - | 12.38 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 200 | 35,727.8 ns | 84.717 ns | 75.099 ns | 0.99 | 18.7378 | 0.0610 | - | 86.53 KB | - AddOrUpdate | 200 | 36,231.4 ns | 136.671 ns | 121.156 ns | 1.00 | 17.1509 | 0.0610 | - | 79.08 KB | - | | | | | | | | | | - AddOrUpdate | 1000 | 304,238.0 ns | 1,636.331 ns | 1,366.410 ns | 1.00 | 111.8164 | 32.7148 | - | 515.39 KB | - AddOrUpdate_v1 | 1000 | 307,233.3 ns | 1,487.373 ns | 1,391.290 ns | 1.01 | 120.6055 | 3.4180 | - | 556.41 KB | +AddOrUpdate | 5 | 480.3 ns | 3.645 ns | 3.410 ns | 1.00 | 0.2432 | - | - | 1.13 KB | +AddOrUpdate_v1 | 5 | 487.1 ns | 2.547 ns | 2.382 ns | 1.01 | 0.2737 | - | - | 1.27 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 40 | 4,844.9 ns | 23.494 ns | 21.976 ns | 0.99 | 2.9678 | - | - | 13.69 KB | +AddOrUpdate | 40 | 4,908.8 ns | 29.374 ns | 27.477 ns | 1.00 | 2.6779 | - | - | 12.38 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 200 | 35,727.8 ns | 84.717 ns | 75.099 ns | 0.99 | 18.7378 | 0.0610 | - | 86.53 KB | +AddOrUpdate | 200 | 36,231.4 ns | 136.671 ns | 121.156 ns | 1.00 | 17.1509 | 0.0610 | - | 79.08 KB | + | | | | | | | | | | +AddOrUpdate | 1000 | 304,238.0 ns | 1,636.331 ns | 1,366.410 ns | 1.00 | 111.8164 | 32.7148 | - | 515.39 KB | +AddOrUpdate_v1 | 1000 | 307,233.3 ns | 1,487.373 ns | 1,391.290 ns | 1.01 | 120.6055 | 3.4180 | - | 556.41 KB | ## Some fixes for updating things - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-------------:|-------------:|-------------:|------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate | 5 | 469.8 ns | 4.613 ns | 3.602 ns | 1.00 | 0.2432 | - | - | 1.13 KB | - AddOrUpdate_v1 | 5 | 480.6 ns | 2.648 ns | 2.477 ns | 1.02 | 0.2737 | - | - | 1.27 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 40 | 4,731.4 ns | 22.512 ns | 21.058 ns | 0.96 | 2.9678 | - | - | 13.69 KB | - AddOrUpdate | 40 | 4,945.0 ns | 12.489 ns | 10.429 ns | 1.00 | 2.6779 | - | - | 12.38 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 200 | 35,094.6 ns | 70.945 ns | 66.362 ns | 0.98 | 18.7378 | 0.0610 | - | 86.53 KB | - AddOrUpdate | 200 | 35,821.9 ns | 129.417 ns | 121.057 ns | 1.00 | 17.1509 | 0.0610 | - | 79.08 KB | - | | | | | | | | | | - AddOrUpdate | 1000 | 303,448.5 ns | 2,017.835 ns | 1,887.484 ns | 1.00 | 111.8164 | 32.7148 | - | 515.39 KB | - AddOrUpdate_v1 | 1000 | 304,471.0 ns | 1,549.817 ns | 1,449.700 ns | 1.00 | 120.6055 | 3.4180 | - | 556.41 KB | +AddOrUpdate | 5 | 469.8 ns | 4.613 ns | 3.602 ns | 1.00 | 0.2432 | - | - | 1.13 KB | +AddOrUpdate_v1 | 5 | 480.6 ns | 2.648 ns | 2.477 ns | 1.02 | 0.2737 | - | - | 1.27 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 40 | 4,731.4 ns | 22.512 ns | 21.058 ns | 0.96 | 2.9678 | - | - | 13.69 KB | +AddOrUpdate | 40 | 4,945.0 ns | 12.489 ns | 10.429 ns | 1.00 | 2.6779 | - | - | 12.38 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 200 | 35,094.6 ns | 70.945 ns | 66.362 ns | 0.98 | 18.7378 | 0.0610 | - | 86.53 KB | +AddOrUpdate | 200 | 35,821.9 ns | 129.417 ns | 121.057 ns | 1.00 | 17.1509 | 0.0610 | - | 79.08 KB | + | | | | | | | | | | +AddOrUpdate | 1000 | 303,448.5 ns | 2,017.835 ns | 1,887.484 ns | 1.00 | 111.8164 | 32.7148 | - | 515.39 KB | +AddOrUpdate_v1 | 1000 | 304,471.0 ns | 1,549.817 ns | 1,449.700 ns | 1.00 | 120.6055 | 3.4180 | - | 556.41 KB | ## Inlining the Balance and removing its not used if-branches - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-------------:|-------------:|-------------:|------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v2 | 5 | 418.2 ns | 2.540 ns | 2.376 ns | 0.90 | 0.2170 | - | - | 1024 B | - AddOrUpdate | 5 | 464.6 ns | 2.301 ns | 2.039 ns | 1.00 | 0.2437 | - | - | 1152 B | - AddOrUpdate_v1 | 5 | 487.7 ns | 1.769 ns | 1.655 ns | 1.05 | 0.2737 | - | - | 1296 B | - AddOrUpdate_v3 | 5 | 516.5 ns | 2.289 ns | 2.141 ns | 1.11 | 0.1993 | - | - | 944 B | - | | | | | | | | | | - AddOrUpdate | 40 | 4,621.6 ns | 19.899 ns | 18.614 ns | 1.00 | 2.6779 | - | - | 12672 B | - AddOrUpdate_v1 | 40 | 4,910.9 ns | 19.805 ns | 18.525 ns | 1.06 | 2.9678 | - | - | 14016 B | - AddOrUpdate_v2 | 40 | 5,115.0 ns | 26.410 ns | 23.411 ns | 1.11 | 2.8458 | - | - | 13456 B | - AddOrUpdate_v3 | 40 | 6,953.3 ns | 24.391 ns | 22.815 ns | 1.50 | 2.6016 | - | - | 12304 B | - | | | | | | | | | | - AddOrUpdate | 200 | 34,250.7 ns | 87.221 ns | 81.586 ns | 1.00 | 17.1509 | 0.0610 | - | 80976 B | - AddOrUpdate_v1 | 200 | 35,425.8 ns | 137.918 ns | 129.008 ns | 1.03 | 18.7378 | 0.0610 | - | 88608 B | - AddOrUpdate_v2 | 200 | 37,942.8 ns | 188.112 ns | 175.960 ns | 1.11 | 19.2261 | - | - | 91008 B | - AddOrUpdate_v3 | 200 | 50,640.8 ns | 66.485 ns | 55.518 ns | 1.48 | 17.6392 | 0.0610 | - | 83352 B | - | | | | | | | | | | - AddOrUpdate | 1000 | 290,830.8 ns | 915.770 ns | 811.806 ns | 1.00 | 111.8164 | 32.7148 | - | 527760 B | - AddOrUpdate_v2 | 1000 | 308,260.3 ns | 2,278.822 ns | 2,020.116 ns | 1.06 | 130.8594 | 0.9766 | - | 619056 B | - AddOrUpdate_v1 | 1000 | 308,355.6 ns | 2,046.461 ns | 1,914.261 ns | 1.06 | 120.6055 | 3.4180 | - | 569760 B | - AddOrUpdate_v3 | 1000 | 375,880.3 ns | 1,710.716 ns | 1,600.205 ns | 1.29 | 118.6523 | 0.4883 | - | 560440 B | - - - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | +AddOrUpdate_v2 | 5 | 418.2 ns | 2.540 ns | 2.376 ns | 0.90 | 0.2170 | - | - | 1024 B | +AddOrUpdate | 5 | 464.6 ns | 2.301 ns | 2.039 ns | 1.00 | 0.2437 | - | - | 1152 B | +AddOrUpdate_v1 | 5 | 487.7 ns | 1.769 ns | 1.655 ns | 1.05 | 0.2737 | - | - | 1296 B | +AddOrUpdate_v3 | 5 | 516.5 ns | 2.289 ns | 2.141 ns | 1.11 | 0.1993 | - | - | 944 B | + | | | | | | | | | | +AddOrUpdate | 40 | 4,621.6 ns | 19.899 ns | 18.614 ns | 1.00 | 2.6779 | - | - | 12672 B | +AddOrUpdate_v1 | 40 | 4,910.9 ns | 19.805 ns | 18.525 ns | 1.06 | 2.9678 | - | - | 14016 B | +AddOrUpdate_v2 | 40 | 5,115.0 ns | 26.410 ns | 23.411 ns | 1.11 | 2.8458 | - | - | 13456 B | +AddOrUpdate_v3 | 40 | 6,953.3 ns | 24.391 ns | 22.815 ns | 1.50 | 2.6016 | - | - | 12304 B | + | | | | | | | | | | +AddOrUpdate | 200 | 34,250.7 ns | 87.221 ns | 81.586 ns | 1.00 | 17.1509 | 0.0610 | - | 80976 B | +AddOrUpdate_v1 | 200 | 35,425.8 ns | 137.918 ns | 129.008 ns | 1.03 | 18.7378 | 0.0610 | - | 88608 B | +AddOrUpdate_v2 | 200 | 37,942.8 ns | 188.112 ns | 175.960 ns | 1.11 | 19.2261 | - | - | 91008 B | +AddOrUpdate_v3 | 200 | 50,640.8 ns | 66.485 ns | 55.518 ns | 1.48 | 17.6392 | 0.0610 | - | 83352 B | + | | | | | | | | | | +AddOrUpdate | 1000 | 290,830.8 ns | 915.770 ns | 811.806 ns | 1.00 | 111.8164 | 32.7148 | - | 527760 B | +AddOrUpdate_v2 | 1000 | 308,260.3 ns | 2,278.822 ns | 2,020.116 ns | 1.06 | 130.8594 | 0.9766 | - | 619056 B | +AddOrUpdate_v1 | 1000 | 308,355.6 ns | 2,046.461 ns | 1,914.261 ns | 1.06 | 120.6055 | 3.4180 | - | 569760 B | +AddOrUpdate_v3 | 1000 | 375,880.3 ns | 1,710.716 ns | 1,600.205 ns | 1.29 | 118.6523 | 0.4883 | - | 560440 B | + + + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-------------:|-------------:|--------------:|------:|--------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v2 | 5 | 404.8 ns | 2.504 ns | 2.0907 ns | 0.91 | 0.01 | 0.2170 | - | - | 1 KB | - AddOrUpdate | 5 | 447.1 ns | 4.673 ns | 4.1429 ns | 1.00 | 0.00 | 0.2437 | - | - | 1.13 KB | - AddOrUpdate_v1 | 5 | 481.5 ns | 1.112 ns | 0.9854 ns | 1.08 | 0.01 | 0.2737 | - | - | 1.27 KB | - | | | | | | | | | | | - AddOrUpdate | 40 | 4,563.0 ns | 91.302 ns | 89.6712 ns | 1.00 | 0.00 | 2.6779 | - | - | 12.38 KB | - AddOrUpdate_v1 | 40 | 4,712.5 ns | 27.017 ns | 25.2716 ns | 1.03 | 0.02 | 2.9678 | - | - | 13.69 KB | - AddOrUpdate_v2 | 40 | 4,943.3 ns | 22.715 ns | 21.2474 ns | 1.08 | 0.02 | 2.8458 | - | - | 13.14 KB | - | | | | | | | | | | | - AddOrUpdate | 200 | 33,629.4 ns | 647.198 ns | 635.6353 ns | 1.00 | 0.00 | 17.1509 | 0.0610 | - | 79.08 KB | - AddOrUpdate_v1 | 200 | 34,643.5 ns | 155.549 ns | 145.5005 ns | 1.03 | 0.02 | 18.7378 | 0.0610 | - | 86.53 KB | - AddOrUpdate_v2 | 200 | 36,726.4 ns | 419.812 ns | 372.1521 ns | 1.10 | 0.01 | 19.2261 | - | - | 88.88 KB | - | | | | | | | | | | | - AddOrUpdate | 1000 | 291,376.3 ns | 3,419.191 ns | 3,198.3136 ns | 1.00 | 0.00 | 111.8164 | 32.7148 | - | 515.39 KB | - AddOrUpdate_v2 | 1000 | 302,027.2 ns | 4,981.373 ns | 4,659.5798 ns | 1.04 | 0.02 | 130.8594 | 0.9766 | - | 604.55 KB | - AddOrUpdate_v1 | 1000 | 304,899.6 ns | 4,634.673 ns | 4,335.2763 ns | 1.05 | 0.02 | 120.6055 | 3.4180 | - | 556.41 KB | +AddOrUpdate_v2 | 5 | 404.8 ns | 2.504 ns | 2.0907 ns | 0.91 | 0.01 | 0.2170 | - | - | 1 KB | +AddOrUpdate | 5 | 447.1 ns | 4.673 ns | 4.1429 ns | 1.00 | 0.00 | 0.2437 | - | - | 1.13 KB | +AddOrUpdate_v1 | 5 | 481.5 ns | 1.112 ns | 0.9854 ns | 1.08 | 0.01 | 0.2737 | - | - | 1.27 KB | + | | | | | | | | | | | +AddOrUpdate | 40 | 4,563.0 ns | 91.302 ns | 89.6712 ns | 1.00 | 0.00 | 2.6779 | - | - | 12.38 KB | +AddOrUpdate_v1 | 40 | 4,712.5 ns | 27.017 ns | 25.2716 ns | 1.03 | 0.02 | 2.9678 | - | - | 13.69 KB | +AddOrUpdate_v2 | 40 | 4,943.3 ns | 22.715 ns | 21.2474 ns | 1.08 | 0.02 | 2.8458 | - | - | 13.14 KB | + | | | | | | | | | | | +AddOrUpdate | 200 | 33,629.4 ns | 647.198 ns | 635.6353 ns | 1.00 | 0.00 | 17.1509 | 0.0610 | - | 79.08 KB | +AddOrUpdate_v1 | 200 | 34,643.5 ns | 155.549 ns | 145.5005 ns | 1.03 | 0.02 | 18.7378 | 0.0610 | - | 86.53 KB | +AddOrUpdate_v2 | 200 | 36,726.4 ns | 419.812 ns | 372.1521 ns | 1.10 | 0.01 | 19.2261 | - | - | 88.88 KB | + | | | | | | | | | | | +AddOrUpdate | 1000 | 291,376.3 ns | 3,419.191 ns | 3,198.3136 ns | 1.00 | 0.00 | 111.8164 | 32.7148 | - | 515.39 KB | +AddOrUpdate_v2 | 1000 | 302,027.2 ns | 4,981.373 ns | 4,659.5798 ns | 1.04 | 0.02 | 130.8594 | 0.9766 | - | 604.55 KB | +AddOrUpdate_v1 | 1000 | 304,899.6 ns | 4,634.673 ns | 4,335.2763 ns | 1.05 | 0.02 | 120.6055 | 3.4180 | - | 556.41 KB | ## Some base line results @@ -296,8 +304,8 @@ public class Populate BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores .NET Core SDK=3.1.100 - [Host] : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT - DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT +[Host] : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT +DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | @@ -355,8 +363,8 @@ public class Populate BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.572 (2004/?/20H1) Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores .NET Core SDK=3.1.403 - [Host] : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT - DefaultJob : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT +[Host] : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT +DefaultJob : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | |----------------------------------- |------ |-------------:|------------:|------------:|------:|--------:|---------:|--------:|------:|----------:| @@ -381,8 +389,8 @@ public class Populate BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores .NET Core SDK=5.0.201 - [Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT - DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT +[Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT +DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | @@ -423,13 +431,13 @@ public class Populate | ImmutableDict_Builder_Add | 1000 | 445,419.2 ns | 4,140.17 ns | 3,872.72 ns | 445,259.3 ns | 1.55 | 0.03 | 9.7656 | 1.9531 | - | 64208 B | | ImmutableDict_Add | 1000 | 815,930.7 ns | 15,795.06 ns | 17,556.16 ns | 810,772.7 ns | 2.83 | 0.06 | 105.4688 | 24.4141 | - | 662168 B | -## Against static TypeDictionary by @rogeralsing (need to substract the cost of enumerating the array when adding items to ImHashMap and PartitionedHashMap) +## Against static TypeDictionary by @rogeralsing (need to subtract the cost of enumerating the array when adding items to ImHashMap and PartitionedHashMap) BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores .NET Core SDK=5.0.201 - [Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT - DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT +[Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT +DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | |---------------------------------- |------ |---------:|---------:|---------:|------:|--------:|-------:|-------:|------:|----------:| @@ -461,8 +469,8 @@ public class Populate BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores .NET Core SDK=6.0.202 - [Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT - DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT +[Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT +DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | @@ -510,8 +518,8 @@ public class Populate Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores .NET Core SDK=6.0.202 - [Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT - DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT +[Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT +DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | |---------------------------------- |------ |--------------:|-------------:|-------------:|------:|--------:|---------:|--------:|------:|----------:| @@ -582,8 +590,8 @@ public class Populate 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 +[Host] : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 +DefaultJob : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | @@ -633,8 +641,8 @@ public class Populate 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 +[Host] : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 +DefaultJob : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 ``` | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | @@ -789,8 +797,8 @@ public class Populate BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1702/22H2/2022Update/SunValley2) 11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores .NET SDK=7.0.304 - [Host] : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 +[Host] : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 +DefaultJob : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Gen0 | Gen1 | Allocated | Alloc Ratio | |----------------------- |------ |---------:|----------:|----------:|------:|--------:|----------------------:|---------------:|------------------------:|-------:|-------:|----------:|------------:| @@ -853,7 +861,7 @@ public class Populate | DictSlim_TryAdd | 1000 | 28.42 us | 0.585 us | 1.661 us | 1.00 | 0.00 | 51,595 | 266 | 338 | 9.1553 | 0.7935 | 56.45 KB | 1.00 | | FHashMap91_AddOrUpdate | 1000 | 32.17 us | 0.621 us | 1.151 us | 1.11 | 0.07 | 56,017 | 165 | 255 | 5.9814 | 0.5493 | 36.7 KB | 0.65 | -## Removing sparce and against the fibonacci hashing +## Removing sparse and against the fibonacci hashing | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Gen0 | Gen1 | Allocated | Alloc Ratio | |------------------------------ |------ |-------------:|-----------:|-----------:|-------------:|------:|--------:|----------------------:|---------------:|------------------------:|-------:|-------:|----------:|------------:| @@ -974,1777 +982,2067 @@ public class Populate | FHashMap91_GetOrAddValueRef | 100 | 2.399 us | 0.1387 us | 0.3630 us | 2.264 us | 0.72 | 0.16 | 0.8469 | 4,366 | 26 | 8 | 0.0076 | 5.22 KB | 0.71 | */ - // [Params(1, 10, 100, 1000)] - [Params(100)]//, 1000)] - // [Params(1000)] - public int Count; + // [Params(1, 10, 100, 1000)] + [Params(10)]//, 1000)] + // [Params(1000)] + public int Count; - private Type[] _types; + private Type[] _types; - [GlobalSetup] - public void Setup() - { - _types = _keys.Take(Count).ToArray(); - } + [GlobalSetup] + public void Setup() + { + _types = _keys.Take(Count).ToArray(); + } - // [Benchmark(Baseline = true)] - // [Benchmark] - public ImTools.ImHashMap V4_ImHashMap_AddOrUpdate() - { - var map = ImTools.ImHashMap.Empty; + // [Benchmark(Baseline = true)] + // [Benchmark] + public ImTools.ImHashMap V4_ImHashMap_AddOrUpdate() + { + var map = ImTools.ImHashMap.Empty; - foreach (var key in _types) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + foreach (var key in _types) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - } + return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + } - // [Benchmark] - public ImTools.ImHashMap V4_ImMap_Add() - { - var map = ImTools.ImHashMap.Empty; + // [Benchmark] + public ImTools.ImHashMap V4_ImMap_Add() + { + var map = ImTools.ImHashMap.Empty; - foreach (var key in _types) - map = map.AddSureNotPresent(key.GetHashCode(), key, "a"); + foreach (var key in _types) + map = map.AddSureNotPresent(key.GetHashCode(), key, "a"); - return map.AddSureNotPresent(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - } + return map.AddSureNotPresent(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + } - // [Benchmark] - public ImTools.ImHashMap> V3_ImMap_AddOrUpdate() - { - var map = ImTools.ImHashMap>.Empty; + // [Benchmark] + public ImTools.ImHashMap> V3_ImMap_AddOrUpdate() + { + var map = ImTools.ImHashMap>.Empty; - foreach (var key in _types) - map = map.AddOrUpdate(key.GetHashCode(), new KeyValuePair(key, "a")); + foreach (var key in _types) + map = map.AddOrUpdate(key.GetHashCode(), new KeyValuePair(key, "a")); - return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new KeyValuePair(typeof(ImHashMapBenchmarks), "!")); - } + return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new KeyValuePair(typeof(ImHashMapBenchmarks), "!")); + } - // [Benchmark] - public ImTools.ImHashMap[] V4_PartitionedHashMap_AddOrUpdate() - { - var map = ImTools.PartitionedHashMap.CreateEmpty(); + // [Benchmark] + public ImTools.ImHashMap[] V4_PartitionedHashMap_AddOrUpdate() + { + var map = ImTools.PartitionedHashMap.CreateEmpty(); - foreach (var key in _types) - map.AddOrUpdate(key, "a"); + foreach (var key in _types) + map.AddOrUpdate(key, "a"); - map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; + } - // // [Benchmark] - // public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() - // { - // var map = ImToolsV3.ImHashMap.Empty; + // // [Benchmark] + // public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() + // { + // var map = ImToolsV3.ImHashMap.Empty; - // foreach (var key in _types) - // map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + // foreach (var key in _types) + // map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - // return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - // } + // return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + // } - // // [Benchmark] - // public ImToolsV3.ImHashMap[] V3_PartitionedHashMap_AddOrUpdate() - // { - // var map = ImToolsV3.PartitionedHashMap.CreateEmpty(); + // // [Benchmark] + // public ImToolsV3.ImHashMap[] V3_PartitionedHashMap_AddOrUpdate() + // { + // var map = ImToolsV3.PartitionedHashMap.CreateEmpty(); - // foreach (var key in _types) - // map.AddOrUpdate(key, "a"); + // foreach (var key in _types) + // map.AddOrUpdate(key, "a"); - // map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - // return map; - // } + // map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + // return map; + // } - // // [Benchmark] - // public ImTools.V2.ImHashMap V2_ImHashMap_AddOrUpdate() - // { - // var map = ImTools.V2.ImHashMap.Empty; + // // [Benchmark] + // public ImTools.V2.ImHashMap V2_ImHashMap_AddOrUpdate() + // { + // var map = ImTools.V2.ImHashMap.Empty; - // foreach (var key in _types) - // map = map.AddOrUpdate(key, "a"); + // foreach (var key in _types) + // map = map.AddOrUpdate(key, "a"); - // return map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - // } + // return map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + // } - // // [Benchmark] - // public ImTools.V2.ImHashMap[] ImHashMapSlots32_AddOrUpdate() - // { - // var map = ImTools.V2.ImHashMapSlots.CreateWithEmpty(); + // // [Benchmark] + // public ImTools.V2.ImHashMap[] ImHashMapSlots32_AddOrUpdate() + // { + // var map = ImTools.V2.ImHashMapSlots.CreateWithEmpty(); - // foreach (var key in _types) - // map.AddOrUpdate(key, "a"); + // foreach (var key in _types) + // map.AddOrUpdate(key, "a"); - // map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - // return map; - // } + // map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + // return map; + // } - // // [Benchmark] - // public ImTools.V2.Experimental.ImMap> V2_ImHashMap_AVLOptimizedForAdd_AddOrUpdate() - // { - // var map = ImTools.V2.Experimental.ImMap>.Empty; + // // [Benchmark] + // public ImTools.V2.Experimental.ImMap> V2_ImHashMap_AVLOptimizedForAdd_AddOrUpdate() + // { + // var map = ImTools.V2.Experimental.ImMap>.Empty; - // foreach (var key in _types) - // map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + // foreach (var key in _types) + // map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - // return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - // } + // return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + // } - // // [Benchmark] - // public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots32_AddOrUpdate() - // { - // var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(); + // // [Benchmark] + // public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots32_AddOrUpdate() + // { + // var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(); - // foreach (var key in _types) - // map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a")); + // foreach (var key in _types) + // map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a")); - // map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!")); - // return map; - // } + // map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!")); + // return map; + // } - // // [Benchmark] - // public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots64_AddOrUpdate() - // { - // var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(64); + // // [Benchmark] + // public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots64_AddOrUpdate() + // { + // var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(64); - // foreach (var key in _types) - // map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a"), 63); + // foreach (var key in _types) + // map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a"), 63); - // map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!"), 63); - // return map; - // } + // map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!"), 63); + // return map; + // } - // [Benchmark(Baseline = true)] - public ImTools.Experiments.RefEqHashMap RefEqHashMap_AddOrUpdate() - { - var map = new ImTools.Experiments.RefEqHashMap(5); + // [Benchmark(Baseline = true)] + public ImTools.Experiments.RefEqHashMap RefEqHashMap_AddOrUpdate() + { + var map = new ImTools.Experiments.RefEqHashMap(5); - foreach (var key in _types) - map.AddOrUpdate(key, "a"); + foreach (var key in _types) + map.AddOrUpdate(key, "a"); - map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; + } - // [Benchmark] - public ImTools.Experiments.HashMapLeapfrog HashMapLeapfrog_AddOrUpdate() - { - var map = new ImTools.Experiments.HashMapLeapfrog(); + // [Benchmark] + public ImTools.Experiments.HashMapLeapfrog HashMapLeapfrog_AddOrUpdate() + { + var map = new ImTools.Experiments.HashMapLeapfrog(); - foreach (var key in _types) - map.AddOrUpdate(key, "a"); + foreach (var key in _types) + map.AddOrUpdate(key, "a"); - map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; + } - [Benchmark(Baseline = true)] - public DictionarySlim DictSlim_TryAdd() - { - var map = new DictionarySlim(); + [Benchmark(Baseline = true)] + public DictionarySlim DictSlim_TryAdd() + { + var map = new DictionarySlim(); - foreach (var key in _types) - map.GetOrAddValueRef(key) = "a"; + foreach (var key in _types) + map.GetOrAddValueRef(key) = "a"; - map.GetOrAddValueRef(typeof(ImHashMapBenchmarks)) = "!"; - return map; - } + map.GetOrAddValueRef(typeof(ImHashMapBenchmarks)) = "!"; + return map; + } - // [Benchmark] - public ImTools.Experiments.FHashMap7> FHashMap7_AddOrUpdate() - { - var map = new ImTools.Experiments.FHashMap7>(); + // [Benchmark] + public ImTools.Experiments.FHashMap7> FHashMap7_AddOrUpdate() + { + var map = new ImTools.Experiments.FHashMap7>(); - foreach (var key in _types) - map.AddOrUpdate(key, "a"); + foreach (var key in _types) + map.AddOrUpdate(key, "a"); - map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; + } - // [Benchmark(Baseline = true)] - [Benchmark] - public FHashMap91TypeString FHashMap91_GetOrAddValueRef() - { - var map = new FHashMap91TypeString(); + // [Benchmark(Baseline = true)] + [Benchmark] + public FHashMap91TypeString FHashMap91_GetOrAddValueRef() + { + var map = new FHashMap91TypeString(); - foreach (var key in _types) - map.GetOrAddValueRef(key) = "a"; + foreach (var key in _types) + map.GetOrAddValueRef(key) = "a"; - map.GetOrAddValueRef(typeof(ImHashMapBenchmarks)) = "!"; - return map; - } + map.GetOrAddValueRef(typeof(ImHashMapBenchmarks)) = "!"; + return map; + } - // [Benchmark] - public ImTools.Experiments.FHashMap8> FHashMap8_AddOrUpdate() - { - var map = new ImTools.Experiments.FHashMap8>(); + // [Benchmark] + public ImTools.Experiments.FHashMap8> FHashMap8_AddOrUpdate() + { + var map = new ImTools.Experiments.FHashMap8>(); - foreach (var key in _types) - map.AddOrUpdate(key, "a"); + foreach (var key in _types) + map.AddOrUpdate(key, "a"); - map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; + } - // [Benchmark] - public Dictionary Dict_TryAdd() - { - var map = new Dictionary(); + // [Benchmark] + public Dictionary Dict_TryAdd() + { + var map = new Dictionary(); - foreach (var key in _types) - map.TryAdd(key, "a"); + foreach (var key in _types) + map.TryAdd(key, "a"); - map.TryAdd(typeof(ImHashMapBenchmarks), "!"); - return map; - } + map.TryAdd(typeof(ImHashMapBenchmarks), "!"); + return map; + } - // [Benchmark] - public ConcurrentDictionary ConcurrentDictionary_TryAdd() - { - var map = new ConcurrentDictionary(); + // [Benchmark] + public ConcurrentDictionary ConcurrentDictionary_TryAdd() + { + var map = new ConcurrentDictionary(); - foreach (var key in _types) - map.TryAdd(key, "a"); + foreach (var key in _types) + map.TryAdd(key, "a"); - map.TryAdd(typeof(ImHashMapBenchmarks), "!"); - return map; - } + map.TryAdd(typeof(ImHashMapBenchmarks), "!"); + return map; + } - // [Benchmark] - public ImmutableDictionary ImmutableDict_Builder_Add() - { - var builder = ImmutableDictionary.CreateBuilder(); + // [Benchmark] + public ImmutableDictionary ImmutableDict_Builder_Add() + { + var builder = ImmutableDictionary.CreateBuilder(); - foreach (var key in _types) - builder.Add(key, "a"); - builder.Add(typeof(ImHashMapBenchmarks), "!"); - return builder.ToImmutable(); - } + foreach (var key in _types) + builder.Add(key, "a"); + builder.Add(typeof(ImHashMapBenchmarks), "!"); + return builder.ToImmutable(); + } - // [Benchmark] - public ImmutableDictionary ImmutableDict_Add() - { - var map = ImmutableDictionary.Empty; + // [Benchmark] + public ImmutableDictionary ImmutableDict_Add() + { + var map = ImmutableDictionary.Empty; - foreach (var key in _types) - map = map.Add(key, "a"); + foreach (var key in _types) + map = map.Add(key, "a"); - return map.Add(typeof(ImHashMapBenchmarks), "!"); - } + return map.Add(typeof(ImHashMapBenchmarks), "!"); + } - // [Benchmark] - public TypeDictionary TypeDictionary_Add() + // [Benchmark] + public TypeDictionary TypeDictionary_Add() + { + var map = new TypeDictionary(); + + map.Add("a"); + map.Add("a"); + map.Add("a"); + map.Add("a"); + map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + + map.Add("!"); + + return map; + } + } + + class A1 { } + class A2 { } + class A3 { } + class A4 { } + class A5 { } + class A6 { } + class A7 { } + class A8 { } + class A9 { } + class A10 { } + class B1 { } + class B2 { } + class B3 { } + class B4 { } + class B5 { } + class B6 { } + class B7 { } + class B8 { } + class B9 { } + class B10 { } + + public class TypeDictionary + { + // ReSharper disable once StaticMemberInGenericType + private static int typeIndex; + private readonly object _lockObject = new object(); + + private TValue[] _values = new TValue[100]; + + public void Add(TValue value) + { + lock (_lockObject) { - var map = new TypeDictionary(); - - map.Add("a"); - map.Add("a"); - map.Add("a"); - map.Add("a"); - map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - - map.Add("!"); - - return map; + var id = TypeKey.Id; + if (id >= _values.Length) + Array.Resize(ref _values, id * 2); + _values[id] = value; } } - class A1 { } - class A2 { } - class A3 { } - class A4 { } - class A5 { } - class A6 { } - class A7 { } - class A8 { } - class A9 { } - class A10 { } - class B1 { } - class B2 { } - class B3 { } - class B4 { } - class B5 { } - class B6 { } - class B7 { } - class B8 { } - class B9 { } - class B10 { } - - public class TypeDictionary + public TValue Get() + { + var id = TypeKey.Id; + return id >= _values.Length ? default : _values[id]; + } + + // ReSharper disable once UnusedTypeParameter + private static class TypeKey { // ReSharper disable once StaticMemberInGenericType - private static int typeIndex; - private readonly object _lockObject = new object(); + internal static readonly int Id = Interlocked.Increment(ref typeIndex); + } + } - private TValue[] _values = new TValue[100]; + [MemoryDiagnoser, RankColumn, Orderer(SummaryOrderPolicy.FastestToSlowest)] + // [HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchMispredictions, HardwareCounter.BranchInstructions)] + public class Lookup + { + /* + ## 21.01.2019: All versions. + + Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |---------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault_v1 | 13.74 ns | 0.0686 ns | 0.0642 ns | 0.79 | - | - | - | - | + GetValueOrDefault | 17.43 ns | 0.0924 ns | 0.0864 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v2 | 19.15 ns | 0.0786 ns | 0.0656 ns | 1.10 | - | - | - | - | + GetValueOrDefault_v3 | 25.73 ns | 0.0711 ns | 0.0665 ns | 1.48 | - | - | - | - | + + ## For some reason dropping lookup speed with only changes to AddOrUpdate + + Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |---------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault_v1 | 13.89 ns | 0.0938 ns | 0.0877 ns | 0.80 | - | - | - | - | + GetValueOrDefault | 17.40 ns | 0.0888 ns | 0.0831 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v2 | 19.04 ns | 0.0712 ns | 0.0666 ns | 1.09 | - | - | - | - | + GetValueOrDefault_v3 | 25.93 ns | 0.0474 ns | 0.0420 ns | 1.49 | - | - | - | - | + + ## Got back some perf by moving GetValueOrDefault to static method and specializing for Type + + Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |---------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault_v1 | 13.87 ns | 0.0400 ns | 0.0355 ns | 0.85 | - | - | - | - | + GetValueOrDefault | 16.34 ns | 0.0932 ns | 0.0826 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v2 | 19.18 ns | 0.0460 ns | 0.0430 ns | 1.17 | - | - | - | - | + GetValueOrDefault_v3 | 25.96 ns | 0.0756 ns | 0.0707 ns | 1.59 | - | - | - | - | + + ## Benchmark against variety of inputs on par with Populate benchmark + + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault_v1 | 5 | 6.155 ns | 0.0321 ns | 0.0301 ns | 0.98 | 0.01 | - | - | - | - | + GetValueOrDefault | 5 | 6.267 ns | 0.0510 ns | 0.0452 ns | 1.00 | 0.00 | - | - | - | - | + GetValueOrDefault_v2 | 5 | 7.439 ns | 0.0763 ns | 0.0676 ns | 1.19 | 0.02 | - | - | - | - | + GetValueOrDefault_v3 | 5 | 9.558 ns | 0.0409 ns | 0.0383 ns | 1.52 | 0.01 | - | - | - | - | + | | | | | | | | | | | + GetValueOrDefault_v1 | 40 | 10.897 ns | 0.0673 ns | 0.0629 ns | 0.95 | 0.01 | - | - | - | - | + GetValueOrDefault | 40 | 11.467 ns | 0.0325 ns | 0.0304 ns | 1.00 | 0.00 | - | - | - | - | + GetValueOrDefault_v2 | 40 | 14.012 ns | 0.1092 ns | 0.1022 ns | 1.22 | 0.01 | - | - | - | - | + GetValueOrDefault_v3 | 40 | 19.945 ns | 0.1032 ns | 0.0965 ns | 1.74 | 0.01 | - | - | - | - | + | | | | | | | | | | | + GetValueOrDefault_v1 | 200 | 13.664 ns | 0.0291 ns | 0.0258 ns | 0.97 | 0.00 | - | - | - | - | + GetValueOrDefault | 200 | 14.051 ns | 0.0524 ns | 0.0491 ns | 1.00 | 0.00 | - | - | - | - | + GetValueOrDefault_v2 | 200 | 16.722 ns | 0.0568 ns | 0.0531 ns | 1.19 | 0.01 | - | - | - | - | + GetValueOrDefault_v3 | 200 | 24.473 ns | 0.0792 ns | 0.0702 ns | 1.74 | 0.01 | - | - | - | - | + | | | | | | | | | | | + GetValueOrDefault_v1 | 1000 | 14.213 ns | 0.1528 ns | 0.1354 ns | 0.96 | 0.01 | - | - | - | - | + GetValueOrDefault | 1000 | 14.805 ns | 0.0518 ns | 0.0485 ns | 1.00 | 0.00 | - | - | - | - | + GetValueOrDefault_v2 | 1000 | 16.645 ns | 0.0447 ns | 0.0419 ns | 1.12 | 0.01 | - | - | - | - | + GetValueOrDefault_v3 | 1000 | 27.489 ns | 0.0890 ns | 0.0832 ns | 1.86 | 0.01 | - | - | - | - | + + ## Adding aggressive inlining to the Data { Hash, Key, Value } properties + + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault | 5 | 5.853 ns | 0.0685 ns | 0.0607 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v1 | 5 | 5.913 ns | 0.0373 ns | 0.0349 ns | 1.01 | - | - | - | - | + | | | | | | | | | | + GetValueOrDefault | 40 | 9.649 ns | 0.0235 ns | 0.0220 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v1 | 40 | 10.266 ns | 0.0236 ns | 0.0221 ns | 1.06 | - | - | - | - | + | | | | | | | | | | + GetValueOrDefault | 200 | 11.613 ns | 0.0554 ns | 0.0491 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v1 | 200 | 12.052 ns | 0.0555 ns | 0.0520 ns | 1.04 | - | - | - | - | + + ## Using `!= Empty` instead of `.Height != 0` drops some perf + + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault_v1 | 5 | 5.933 ns | 0.0310 ns | 0.0290 ns | 0.93 | 0.03 | - | - | - | - | + GetValueOrDefault | 5 | 6.386 ns | 0.1807 ns | 0.1602 ns | 1.00 | 0.00 | - | - | - | - | + | | | | | | | | | | | + GetValueOrDefault | 40 | 9.820 ns | 0.0521 ns | 0.0488 ns | 1.00 | 0.00 | - | - | - | - | + GetValueOrDefault_v1 | 40 | 10.257 ns | 0.0300 ns | 0.0266 ns | 1.05 | 0.01 | - | - | - | - | + | | | | | | | | | | | + GetValueOrDefault | 200 | 11.717 ns | 0.0689 ns | 0.0644 ns | 1.00 | 0.00 | - | - | - | - | + GetValueOrDefault_v1 | 200 | 12.104 ns | 0.0548 ns | 0.0486 ns | 1.03 | 0.01 | - | - | - | - | + + ## Removing `.Height != 0` check completely did not change much, but let it stay cause less code is better + + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault | 5 | 5.903 ns | 0.0612 ns | 0.0573 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v1 | 5 | 5.931 ns | 0.0503 ns | 0.0470 ns | 1.00 | - | - | - | - | + | | | | | | | | | | + GetValueOrDefault | 40 | 9.636 ns | 0.0419 ns | 0.0392 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v1 | 40 | 10.231 ns | 0.0333 ns | 0.0312 ns | 1.06 | - | - | - | - | + | | | | | | | | | | + GetValueOrDefault | 200 | 11.637 ns | 0.0721 ns | 0.0602 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v1 | 200 | 12.042 ns | 0.0607 ns | 0.0568 ns | 1.03 | - | - | - | - | + + ## TryFind + + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + ----------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + TryFind_v1 | 5 | 5.229 ns | 0.0307 ns | 0.0257 ns | 1.00 | - | - | - | - | + TryFind | 5 | 6.766 ns | 0.0695 ns | 0.0650 ns | 1.30 | - | - | - | - | + | | | | | | | | | | + TryFind_v1 | 40 | 9.268 ns | 0.0116 ns | 0.0108 ns | 1.00 | - | - | - | - | + TryFind | 40 | 9.755 ns | 0.0219 ns | 0.0205 ns | 1.05 | - | - | - | - | + | | | | | | | | | | + TryFind_v1 | 200 | 11.773 ns | 0.0558 ns | 0.0494 ns | 1.00 | - | - | - | - | + TryFind | 200 | 12.212 ns | 0.0456 ns | 0.0380 ns | 1.04 | - | - | - | - | + + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + ----------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + TryFind | 5 | 5.906 ns | 0.0191 ns | 0.0178 ns | 0.97 | - | - | - | - | + TryFind_v1 | 5 | 6.079 ns | 0.0947 ns | 0.0839 ns | 1.00 | - | - | - | - | + | | | | | | | | | | + TryFind | 40 | 9.211 ns | 0.0214 ns | 0.0200 ns | 0.87 | - | - | - | - | + TryFind_v1 | 40 | 10.566 ns | 0.0149 ns | 0.0132 ns | 1.00 | - | - | - | - | + | | | | | | | | | | + TryFind | 200 | 11.400 ns | 0.1152 ns | 0.1078 ns | 0.88 | - | - | - | - | + TryFind_v1 | 200 | 12.929 ns | 0.0712 ns | 0.0666 ns | 1.00 | - | - | - | - | + + ## GetOrDefault a bit optimized + + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault | 5 | 6.782 ns | 0.0449 ns | 0.0398 ns | 0.96 | - | - | - | - | + GetValueOrDefault_v1 | 5 | 7.042 ns | 0.0659 ns | 0.0616 ns | 1.00 | - | - | - | - | + | | | | | | | | | | + GetValueOrDefault | 40 | 10.962 ns | 0.0866 ns | 0.0768 ns | 0.99 | - | - | - | - | + GetValueOrDefault_v1 | 40 | 11.094 ns | 0.0973 ns | 0.0813 ns | 1.00 | - | - | - | - | + | | | | | | | | | | + GetValueOrDefault | 200 | 13.329 ns | 0.0338 ns | 0.0299 ns | 0.97 | - | - | - | - | + GetValueOrDefault_v1 | 200 | 13.722 ns | 0.0537 ns | 0.0448 ns | 1.00 | - | - | - | - | + + ## The whole result for the docs + + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + ---------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + TryFind_v1 | 10 | 7.274 ns | 0.0410 ns | 0.0384 ns | 0.98 | 0.01 | - | - | - | - | + TryFind | 10 | 7.422 ns | 0.0237 ns | 0.0222 ns | 1.00 | 0.00 | - | - | - | - | + ConcurrentDict_TryGet | 10 | 21.664 ns | 0.0213 ns | 0.0189 ns | 2.92 | 0.01 | - | - | - | - | + ImmutableDict_TryGet | 10 | 71.199 ns | 0.1312 ns | 0.1228 ns | 9.59 | 0.03 | - | - | - | - | + | | | | | | | | | | | + TryFind_v1 | 100 | 8.426 ns | 0.0236 ns | 0.0221 ns | 0.91 | 0.00 | - | - | - | - | + TryFind | 100 | 9.304 ns | 0.0305 ns | 0.0270 ns | 1.00 | 0.00 | - | - | - | - | + ConcurrentDict_TryGet | 100 | 21.791 ns | 0.1072 ns | 0.0951 ns | 2.34 | 0.01 | - | - | - | - | + ImmutableDict_TryGet | 100 | 74.985 ns | 0.1053 ns | 0.0879 ns | 8.06 | 0.03 | - | - | - | - | + | | | | | | | | | | | + TryFind | 1000 | 13.837 ns | 0.0291 ns | 0.0272 ns | 1.00 | 0.00 | - | - | - | - | + TryFind_v1 | 1000 | 16.108 ns | 0.0415 ns | 0.0367 ns | 1.16 | 0.00 | - | - | - | - | + ConcurrentDict_TryGet | 1000 | 21.876 ns | 0.0325 ns | 0.0288 ns | 1.58 | 0.00 | - | - | - | - | + ImmutableDict_TryGet | 1000 | 83.563 ns | 0.1046 ns | 0.0873 ns | 6.04 | 0.01 | - | - | - | - | + + + ## 2019-03-28: Comparing vs `Dictionary`: + + Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + TryFind | 10 | 7.722 ns | 0.0451 ns | 0.0422 ns | 7.718 ns | 1.00 | 0.00 | - | - | - | - | + Dictionary_TryGetValue | 10 | 18.475 ns | 0.0502 ns | 0.0470 ns | 18.470 ns | 2.39 | 0.01 | - | - | - | - | + ConcurrentDictionary_TryGetValue | 10 | 22.661 ns | 0.0463 ns | 0.0433 ns | 22.653 ns | 2.93 | 0.02 | - | - | - | - | + ImmutableDict_TryGet | 10 | 72.911 ns | 1.5234 ns | 2.1355 ns | 74.134 ns | 9.25 | 0.23 | - | - | - | - | + | | | | | | | | | | | | + TryFind | 100 | 9.987 ns | 0.0543 ns | 0.0508 ns | 9.978 ns | 1.00 | 0.00 | - | - | - | - | + Dictionary_TryGetValue | 100 | 18.110 ns | 0.0644 ns | 0.0602 ns | 18.088 ns | 1.81 | 0.01 | - | - | - | - | + ConcurrentDictionary_TryGetValue | 100 | 24.402 ns | 0.0978 ns | 0.0915 ns | 24.435 ns | 2.44 | 0.02 | - | - | - | - | + ImmutableDict_TryGet | 100 | 76.689 ns | 0.3632 ns | 0.3397 ns | 76.704 ns | 7.68 | 0.04 | - | - | - | - | + | | | | | | | | | | | | + TryFind | 1000 | 12.600 ns | 0.1506 ns | 0.1335 ns | 12.551 ns | 1.00 | 0.00 | - | - | - | - | + Dictionary_TryGetValue | 1000 | 19.023 ns | 0.0575 ns | 0.0538 ns | 19.036 ns | 1.51 | 0.02 | - | - | - | - | + ConcurrentDictionary_TryGetValue | 1000 | 22.651 ns | 0.1238 ns | 0.1097 ns | 22.613 ns | 1.80 | 0.02 | - | - | - | - | + ImmutableDict_TryGet | 1000 | 83.608 ns | 0.3105 ns | 0.2904 ns | 83.612 ns | 6.64 | 0.07 | - | - | - | - | + + ## 2019-03-29: Comparing vs `DictionarySlim`: + + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + DictionarySlim_TryGetValue | 10 | 8.228 ns | 0.0682 ns | 0.0604 ns | 1.00 | 0.01 | - | - | - | - | + TryFind | 10 | 8.257 ns | 0.0796 ns | 0.0706 ns | 1.00 | 0.00 | - | - | - | - | + Dictionary_TryGetValue | 10 | 19.615 ns | 0.0251 ns | 0.0209 ns | 2.38 | 0.02 | - | - | - | - | + ConcurrentDictionary_TryGetValue | 10 | 22.339 ns | 0.0922 ns | 0.0863 ns | 2.71 | 0.03 | - | - | - | - | + ImmutableDict_TryGet | 10 | 69.872 ns | 0.2699 ns | 0.2524 ns | 8.46 | 0.07 | - | - | - | - | + | | | | | | | | | | | + DictionarySlim_TryGetValue | 100 | 8.351 ns | 0.1613 ns | 0.1508 ns | 0.69 | 0.01 | - | - | - | - | + TryFind | 100 | 12.144 ns | 0.0570 ns | 0.0533 ns | 1.00 | 0.00 | - | - | - | - | + Dictionary_TryGetValue | 100 | 17.985 ns | 0.0880 ns | 0.0823 ns | 1.48 | 0.01 | - | - | - | - | + ConcurrentDictionary_TryGetValue | 100 | 22.312 ns | 0.0564 ns | 0.0471 ns | 1.84 | 0.01 | - | - | - | - | + ImmutableDict_TryGet | 100 | 75.374 ns | 0.3042 ns | 0.2846 ns | 6.21 | 0.04 | - | - | - | - | + | | | | | | | | | | | + DictionarySlim_TryGetValue | 1000 | 8.202 ns | 0.0713 ns | 0.0667 ns | 0.55 | 0.01 | - | - | - | - | + TryFind | 1000 | 14.919 ns | 0.1101 ns | 0.0919 ns | 1.00 | 0.00 | - | - | - | - | + Dictionary_TryGetValue | 1000 | 18.073 ns | 0.2415 ns | 0.2141 ns | 1.21 | 0.02 | - | - | - | - | + ConcurrentDictionary_TryGetValue | 1000 | 22.406 ns | 0.1039 ns | 0.0921 ns | 1.50 | 0.01 | - | - | - | - | + ImmutableDict_TryGet | 1000 | 84.215 ns | 0.2835 ns | 0.2513 ns | 5.65 | 0.04 | - | - | - | - | + + ## 2019-04-08: Full test + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + | ImHashMap_TryFind | 10 | 9.072 ns | 0.0301 ns | 0.0282 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 10 | 8.405 ns | 0.0124 ns | 0.0116 ns | 0.93 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 10 | 8.199 ns | 0.0118 ns | 0.0105 ns | 0.90 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 18.151 ns | 0.0724 ns | 0.0677 ns | 2.00 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 22.281 ns | 0.1872 ns | 0.1462 ns | 2.45 | 0.02 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 70.143 ns | 0.2833 ns | 0.2650 ns | 7.73 | 0.04 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 100 | 12.698 ns | 0.0545 ns | 0.0510 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 100 | 12.440 ns | 0.0145 ns | 0.0129 ns | 0.98 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 100 | 8.197 ns | 0.0157 ns | 0.0139 ns | 0.65 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 100 | 18.108 ns | 0.0263 ns | 0.0205 ns | 1.43 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 22.834 ns | 0.0627 ns | 0.0524 ns | 1.80 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 76.253 ns | 0.2767 ns | 0.2311 ns | 6.00 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 1000 | 14.960 ns | 0.0457 ns | 0.0427 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 1000 | 14.614 ns | 0.0508 ns | 0.0451 ns | 0.98 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 1000 | 8.209 ns | 0.0534 ns | 0.0499 ns | 0.55 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 1000 | 18.256 ns | 0.0383 ns | 0.0320 ns | 1.22 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 22.261 ns | 0.1509 ns | 0.1411 ns | 1.49 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 83.095 ns | 0.3395 ns | 0.3176 ns | 5.55 | 0.03 | - | - | - | - | + + ## 2019-04-08: Different variants tested + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + | ImHashMap_TryFind2 | 10 | 7.979 ns | 0.0446 ns | 0.0417 ns | 0.93 | 0.01 | - | - | - | - | + | ImHashMap_TryFind3 | 10 | 7.908 ns | 0.0295 ns | 0.0262 ns | 0.92 | 0.00 | - | - | - | - | + | ImHashMap_TryFind | 10 | 8.608 ns | 0.0186 ns | 0.0174 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 10 | 8.248 ns | 0.0262 ns | 0.0232 ns | 0.96 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 10 | 7.212 ns | 0.0194 ns | 0.0181 ns | 0.84 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 17.707 ns | 0.0631 ns | 0.0590 ns | 2.06 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 22.210 ns | 0.1074 ns | 0.1004 ns | 2.58 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 72.093 ns | 0.4162 ns | 0.3893 ns | 8.37 | 0.05 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind2 | 100 | 11.245 ns | 0.0174 ns | 0.0163 ns | 0.92 | 0.00 | - | - | - | - | + | ImHashMap_TryFind3 | 100 | 11.488 ns | 0.0801 ns | 0.0710 ns | 0.94 | 0.01 | - | - | - | - | + | ImHashMap_TryFind | 100 | 12.165 ns | 0.0141 ns | 0.0118 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 100 | 12.272 ns | 0.0367 ns | 0.0343 ns | 1.01 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 100 | 7.019 ns | 0.0516 ns | 0.0458 ns | 0.58 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 100 | 17.825 ns | 0.1278 ns | 0.1196 ns | 1.47 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 22.189 ns | 0.1034 ns | 0.0968 ns | 1.82 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 75.564 ns | 0.3778 ns | 0.3534 ns | 6.21 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind2 | 1000 | 15.909 ns | 0.0924 ns | 0.0864 ns | 1.07 | 0.01 | - | - | - | - | + | ImHashMap_TryFind3 | 1000 | 13.643 ns | 0.0715 ns | 0.0669 ns | 0.92 | 0.01 | - | - | - | - | + | ImHashMap_TryFind | 1000 | 14.819 ns | 0.0465 ns | 0.0412 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 1000 | 14.541 ns | 0.0555 ns | 0.0520 ns | 0.98 | 0.01 | - | - | - | - | + | DictionarySlim_TryGetValue | 1000 | 7.678 ns | 0.0341 ns | 0.0302 ns | 0.52 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 1000 | 17.664 ns | 0.0403 ns | 0.0377 ns | 1.19 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 22.010 ns | 0.0497 ns | 0.0465 ns | 1.48 | 0.00 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 83.661 ns | 0.4033 ns | 0.3772 ns | 5.65 | 0.03 | - | - | - | - | + + ## Selecting the 3rd variant: + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + | ImHashMap_TryFind | 10 | 8.078 ns | 0.0405 ns | 0.0379 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 10 | 8.224 ns | 0.0114 ns | 0.0101 ns | 1.02 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 10 | 7.387 ns | 0.0406 ns | 0.0380 ns | 0.91 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 17.917 ns | 0.0429 ns | 0.0401 ns | 2.22 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 22.256 ns | 0.0726 ns | 0.0643 ns | 2.75 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 70.638 ns | 0.6266 ns | 0.5861 ns | 8.74 | 0.09 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 100 | 11.577 ns | 0.0168 ns | 0.0141 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 100 | 12.329 ns | 0.0295 ns | 0.0276 ns | 1.07 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 100 | 7.410 ns | 0.0398 ns | 0.0353 ns | 0.64 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 100 | 17.890 ns | 0.0425 ns | 0.0377 ns | 1.55 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 22.240 ns | 0.0654 ns | 0.0580 ns | 1.92 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 78.697 ns | 1.1242 ns | 1.0516 ns | 6.78 | 0.08 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 1000 | 13.731 ns | 0.0292 ns | 0.0258 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 1000 | 14.553 ns | 0.0370 ns | 0.0346 ns | 1.06 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 1000 | 7.345 ns | 0.0208 ns | 0.0194 ns | 0.53 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 1000 | 18.672 ns | 0.0483 ns | 0.0451 ns | 1.36 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 22.150 ns | 0.1141 ns | 0.1068 ns | 1.61 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 82.402 ns | 0.9798 ns | 0.9165 ns | 6.01 | 0.07 | - | - | - | - | + + ## V2: + + BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 + Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=3.1.100 + [Host] : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT + DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT + + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | ImHashMap_TryFind | 1 | 4.755 ns | 0.1690 ns | 0.1878 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 1 | 3.657 ns | 0.0458 ns | 0.0406 ns | 0.78 | 0.04 | - | - | - | - | + | ImHashMapSlots_TryFind | 1 | 2.518 ns | 0.0149 ns | 0.0132 ns | 0.53 | 0.02 | - | - | - | - | + | DictionarySlim_TryGetValue | 1 | 6.804 ns | 0.0272 ns | 0.0254 ns | 1.44 | 0.07 | - | - | - | - | + | Dictionary_TryGetValue | 1 | 16.495 ns | 0.3948 ns | 0.4993 ns | 3.49 | 0.16 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1 | 15.812 ns | 0.1016 ns | 0.0951 ns | 3.35 | 0.15 | - | - | - | - | + | ImmutableDict_TryGet | 1 | 24.346 ns | 0.1253 ns | 0.1172 ns | 5.16 | 0.23 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 10 | 6.080 ns | 0.0235 ns | 0.0208 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 10 | 6.091 ns | 0.0707 ns | 0.0590 ns | 1.00 | 0.01 | - | - | - | - | + | ImHashMapSlots_TryFind | 10 | 2.517 ns | 0.0206 ns | 0.0193 ns | 0.41 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 10 | 6.670 ns | 0.0278 ns | 0.0260 ns | 1.10 | 0.01 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 16.202 ns | 0.0634 ns | 0.0562 ns | 2.66 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 15.764 ns | 0.0659 ns | 0.0617 ns | 2.59 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 26.282 ns | 0.2232 ns | 0.2088 ns | 4.32 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 100 | 9.350 ns | 0.0315 ns | 0.0295 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 100 | 10.752 ns | 0.0199 ns | 0.0166 ns | 1.15 | 0.00 | - | - | - | - | + | ImHashMapSlots_TryFind | 100 | 5.664 ns | 0.0391 ns | 0.0366 ns | 0.61 | 0.01 | - | - | - | - | + | DictionarySlim_TryGetValue | 100 | 6.665 ns | 0.0287 ns | 0.0254 ns | 0.71 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 100 | 17.007 ns | 0.0615 ns | 0.0576 ns | 1.82 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 16.024 ns | 0.3340 ns | 0.3124 ns | 1.71 | 0.03 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 30.667 ns | 0.1026 ns | 0.0960 ns | 3.28 | 0.02 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 1000 | 12.729 ns | 0.0393 ns | 0.0368 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 1000 | 13.352 ns | 0.0638 ns | 0.0597 ns | 1.05 | 0.00 | - | - | - | - | + | ImHashMapSlots_TryFind | 1000 | 7.131 ns | 0.0246 ns | 0.0230 ns | 0.56 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 1000 | 6.686 ns | 0.0317 ns | 0.0297 ns | 0.53 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 1000 | 16.848 ns | 0.0593 ns | 0.0526 ns | 1.32 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 15.684 ns | 0.0695 ns | 0.0650 ns | 1.23 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 33.579 ns | 0.1077 ns | 0.0955 ns | 2.64 | 0.01 | - | - | - | - | + + + BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 + Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=3.1.100 + [Host] : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT + DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT + + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |-------------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | ImHashMap_TryFind | 1 | 4.365 ns | 0.0198 ns | 0.0185 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMapSlots32_TryFind | 1 | 2.400 ns | 0.0229 ns | 0.0214 ns | 0.55 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 1 | 3.385 ns | 0.0071 ns | 0.0063 ns | 0.78 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1 | 4.892 ns | 0.0134 ns | 0.0118 ns | 1.12 | 0.01 | - | - | - | - | + | Experimental_ImHashMapSlots32_TryFind | 1 | 6.268 ns | 0.0455 ns | 0.0425 ns | 1.44 | 0.01 | - | - | - | - | + | Experimental_ImHashMapSlots64_TryFind | 1 | 6.175 ns | 0.0335 ns | 0.0313 ns | 1.41 | 0.01 | - | - | - | - | + | DictionarySlim_TryGetValue | 1 | 6.190 ns | 0.0524 ns | 0.0490 ns | 1.42 | 0.02 | - | - | - | - | + | Dictionary_TryGetValue | 1 | 15.425 ns | 0.0636 ns | 0.0595 ns | 3.53 | 0.02 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1 | 14.754 ns | 0.0807 ns | 0.0716 ns | 3.38 | 0.02 | - | - | - | - | + | ImmutableDict_TryGet | 1 | 23.449 ns | 0.1027 ns | 0.0960 ns | 5.37 | 0.04 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 10 | 5.449 ns | 0.0216 ns | 0.0202 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMapSlots32_TryFind | 10 | 2.911 ns | 0.0089 ns | 0.0083 ns | 0.53 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 10 | 6.019 ns | 0.0129 ns | 0.0115 ns | 1.10 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 10 | 8.060 ns | 0.0239 ns | 0.0224 ns | 1.48 | 0.01 | - | - | - | - | + | Experimental_ImHashMapSlots32_TryFind | 10 | 5.605 ns | 0.0261 ns | 0.0244 ns | 1.03 | 0.00 | - | - | - | - | + | Experimental_ImHashMapSlots64_TryFind | 10 | 5.504 ns | 0.0456 ns | 0.0404 ns | 1.01 | 0.01 | - | - | - | - | + | DictionarySlim_TryGetValue | 10 | 5.216 ns | 0.0252 ns | 0.0236 ns | 0.96 | 0.01 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 15.346 ns | 0.0732 ns | 0.0685 ns | 2.82 | 0.02 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 14.870 ns | 0.1094 ns | 0.1023 ns | 2.73 | 0.02 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 24.888 ns | 0.0798 ns | 0.0746 ns | 4.57 | 0.02 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 100 | 7.665 ns | 0.0208 ns | 0.0184 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMapSlots32_TryFind | 100 | 5.240 ns | 0.0238 ns | 0.0223 ns | 0.68 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 100 | 9.222 ns | 0.0223 ns | 0.0208 ns | 1.20 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 100 | 12.248 ns | 0.0584 ns | 0.0546 ns | 1.60 | 0.01 | - | - | - | - | + | Experimental_ImHashMapSlots32_TryFind | 100 | 7.546 ns | 0.0966 ns | 0.0903 ns | 0.98 | 0.01 | - | - | - | - | + | Experimental_ImHashMapSlots64_TryFind | 100 | 5.780 ns | 0.0250 ns | 0.0234 ns | 0.75 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 100 | 6.452 ns | 0.0538 ns | 0.0503 ns | 0.84 | 0.01 | - | - | - | - | + | Dictionary_TryGetValue | 100 | 15.705 ns | 0.0586 ns | 0.0549 ns | 2.05 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 15.321 ns | 0.0413 ns | 0.0386 ns | 2.00 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 27.937 ns | 0.1050 ns | 0.0982 ns | 3.64 | 0.02 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 1000 | 11.459 ns | 0.0306 ns | 0.0286 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMapSlots32_TryFind | 1000 | 8.633 ns | 0.0154 ns | 0.0144 ns | 0.75 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 1000 | 12.486 ns | 0.0527 ns | 0.0493 ns | 1.09 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1000 | 16.002 ns | 0.0378 ns | 0.0353 ns | 1.40 | 0.00 | - | - | - | - | + | Experimental_ImHashMapSlots32_TryFind | 1000 | 11.673 ns | 0.0638 ns | 0.0597 ns | 1.02 | 0.00 | - | - | - | - | + | Experimental_ImHashMapSlots64_TryFind | 1000 | 9.817 ns | 0.0123 ns | 0.0115 ns | 0.86 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 1000 | 6.469 ns | 0.0428 ns | 0.0401 ns | 0.56 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 1000 | 19.170 ns | 0.0708 ns | 0.0628 ns | 1.67 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 16.273 ns | 0.4046 ns | 0.5116 ns | 1.43 | 0.05 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 29.662 ns | 0.1529 ns | 0.1355 ns | 2.59 | 0.01 | - | - | - | - | + + ## V3 - baseline + + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.572 (2004/?/20H1) + Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores + .NET Core SDK=3.1.403 + [Host] : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT + DefaultJob : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT + + ## v3 - baseline + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |------------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | ImHashMap_TryFind | 1 | 4.614 ns | 0.1073 ns | 0.0951 ns | 4.589 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1 | 7.398 ns | 0.0926 ns | 0.0821 ns | 7.413 ns | 1.60 | 0.04 | - | - | - | - | + | ImHashMap234_TryFind | 1 | 4.912 ns | 0.0595 ns | 0.0528 ns | 4.903 ns | 1.07 | 0.03 | - | - | - | - | + | | | | | | | | | | | | | + | ImHashMap_TryFind | 10 | 8.205 ns | 0.0765 ns | 0.0639 ns | 8.186 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 10 | 10.689 ns | 0.2738 ns | 0.2561 ns | 10.684 ns | 1.31 | 0.03 | - | - | - | - | + | ImHashMap234_TryFind | 10 | 6.418 ns | 0.0731 ns | 0.0611 ns | 6.412 ns | 0.78 | 0.01 | - | - | - | - | + | | | | | | | | | | | | | + | ImHashMap_TryFind | 100 | 10.398 ns | 0.0434 ns | 0.0385 ns | 10.391 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 100 | 13.982 ns | 0.2432 ns | 0.2275 ns | 13.946 ns | 1.35 | 0.02 | - | - | - | - | + | ImHashMap234_TryFind | 100 | 10.759 ns | 0.1757 ns | 0.1467 ns | 10.761 ns | 1.03 | 0.01 | - | - | - | - | + | | | | | | | | | | | | | + | ImHashMap_TryFind | 1000 | 14.397 ns | 0.2039 ns | 0.1907 ns | 14.369 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1000 | 30.810 ns | 1.0842 ns | 2.9681 ns | 29.878 ns | 2.25 | 0.38 | - | - | - | - | + | ImHashMap234_TryFind | 1000 | 19.953 ns | 0.4996 ns | 0.4429 ns | 19.772 ns | 1.39 | 0.03 | - | - | - | - | + + ### Leaf3Plus1 + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | ImHashMap_TryFind | 1 | 5.611 ns | 0.1067 ns | 0.0998 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1 | 6.348 ns | 0.2010 ns | 0.2150 ns | 1.14 | 0.05 | - | - | - | - | + | ImHashMap234_TryFind | 1 | 4.572 ns | 0.0966 ns | 0.0904 ns | 0.82 | 0.02 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 10 | 7.582 ns | 0.0703 ns | 0.0587 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 10 | 10.133 ns | 0.1315 ns | 0.1098 ns | 1.34 | 0.02 | - | - | - | - | + | ImHashMap234_TryFind | 10 | 5.763 ns | 0.0670 ns | 0.0594 ns | 0.76 | 0.01 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 100 | 10.642 ns | 0.2276 ns | 0.1901 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 100 | 13.526 ns | 0.1563 ns | 0.1385 ns | 1.27 | 0.03 | - | - | - | - | + | ImHashMap234_TryFind | 100 | 9.271 ns | 0.1695 ns | 0.1503 ns | 0.87 | 0.02 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 1000 | 14.909 ns | 0.2118 ns | 0.1981 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1000 | 37.315 ns | 1.4352 ns | 4.2316 ns | 2.76 | 0.21 | - | - | - | - | + | ImHashMap234_TryFind | 1000 | 24.536 ns | 0.6190 ns | 0.7602 ns | 1.66 | 0.06 | - | - | - | - | + + ### Leaf5Plus1 + Leaf3Plus1 + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | ImHashMap_TryFind | 1 | 6.659 ns | 0.1975 ns | 0.1751 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1 | 8.220 ns | 0.1876 ns | 0.1842 ns | 1.23 | 0.05 | - | - | - | - | + | ImHashMap234_TryFind | 1 | 5.644 ns | 0.1702 ns | 0.1592 ns | 0.85 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 5 | 7.223 ns | 0.2332 ns | 0.2068 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 5 | 9.832 ns | 0.2499 ns | 0.2337 ns | 1.36 | 0.05 | - | - | - | - | + | ImHashMap234_TryFind | 5 | 5.353 ns | 0.1591 ns | 0.1410 ns | 0.74 | 0.04 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 10 | 8.325 ns | 0.1680 ns | 0.1403 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 10 | 12.221 ns | 0.3406 ns | 0.5098 ns | 1.50 | 0.08 | - | - | - | - | + | ImHashMap234_TryFind | 10 | 7.012 ns | 0.2045 ns | 0.2273 ns | 0.85 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 100 | 12.118 ns | 0.3031 ns | 0.4629 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 100 | 18.145 ns | 0.4551 ns | 0.8321 ns | 1.50 | 0.08 | - | - | - | - | + | ImHashMap234_TryFind | 100 | 11.908 ns | 0.2829 ns | 0.3679 ns | 0.97 | 0.04 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 1000 | 17.113 ns | 0.3848 ns | 0.3411 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1000 | 26.034 ns | 0.6183 ns | 0.8255 ns | 1.52 | 0.07 | - | - | - | - | + | ImHashMap234_TryFind | 1000 | 16.927 ns | 0.3743 ns | 0.5828 ns | 0.99 | 0.05 | - | - | - | - | + + ### V3 candidate + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |----------------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | V2_ImHashMap_AVL_TryFind | 1 | 6.084 ns | 0.1295 ns | 0.1082 ns | 1.00 | 0.00 | - | - | - | - | + | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 1 | 7.319 ns | 0.2009 ns | 0.1879 ns | 1.20 | 0.03 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 1 | 5.372 ns | 0.1142 ns | 0.0954 ns | 0.88 | 0.02 | - | - | - | - | + | V3_PartitionedHashMap_23Tree_TryFind | 1 | 6.578 ns | 0.1517 ns | 0.1345 ns | 1.08 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 5 | 6.867 ns | 0.1035 ns | 0.0918 ns | 1.00 | 0.00 | - | - | - | - | + | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 5 | 9.599 ns | 0.1509 ns | 0.1411 ns | 1.40 | 0.03 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 5 | 5.319 ns | 0.1028 ns | 0.1142 ns | 0.77 | 0.02 | - | - | - | - | + | V3_PartitionedHashMap_23Tree_TryFind | 5 | 6.550 ns | 0.0856 ns | 0.0801 ns | 0.95 | 0.01 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 10 | 7.646 ns | 0.0905 ns | 0.0802 ns | 1.00 | 0.00 | - | - | - | - | + | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 10 | 11.002 ns | 0.2011 ns | 0.1881 ns | 1.44 | 0.03 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 10 | 6.986 ns | 0.2160 ns | 0.2122 ns | 0.91 | 0.03 | - | - | - | - | + | V3_PartitionedHashMap_23Tree_TryFind | 10 | 6.714 ns | 0.2153 ns | 0.1798 ns | 0.88 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 100 | 12.413 ns | 0.3100 ns | 0.2748 ns | 1.00 | 0.00 | - | - | - | - | + | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 100 | 15.134 ns | 0.2375 ns | 0.2917 ns | 1.23 | 0.04 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 100 | 11.147 ns | 0.2990 ns | 0.3443 ns | 0.90 | 0.03 | - | - | - | - | + | V3_PartitionedHashMap_23Tree_TryFind | 100 | 8.245 ns | 0.1473 ns | 0.1150 ns | 0.67 | 0.01 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 1000 | 16.528 ns | 0.1799 ns | 0.1502 ns | 1.00 | 0.00 | - | - | - | - | + | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 1000 | 22.341 ns | 0.2584 ns | 0.2291 ns | 1.35 | 0.02 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 1000 | 13.178 ns | 0.2153 ns | 0.2014 ns | 0.80 | 0.01 | - | - | - | - | + | V3_PartitionedHashMap_23Tree_TryFind | 1000 | 11.198 ns | 0.2784 ns | 0.3094 ns | 0.68 | 0.02 | - | - | - | - | + + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | V3_ImHashMap_23Tree_TryFind | 1 | 4.467 ns | 0.0977 ns | 0.0816 ns | 1.00 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1 | 19.103 ns | 0.2302 ns | 0.2040 ns | 4.28 | 0.08 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 5 | 4.588 ns | 0.1057 ns | 0.0937 ns | 1.00 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 5 | 19.000 ns | 0.1738 ns | 0.1625 ns | 4.15 | 0.09 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 10 | 5.948 ns | 0.1194 ns | 0.1116 ns | 1.00 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 19.598 ns | 0.4600 ns | 0.4303 ns | 3.30 | 0.11 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 100 | 9.838 ns | 0.1607 ns | 0.1342 ns | 1.00 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 20.336 ns | 0.3591 ns | 0.2998 ns | 2.07 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 1000 | 11.980 ns | 0.2353 ns | 0.2201 ns | 1.00 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 19.939 ns | 0.4273 ns | 0.8824 ns | 1.73 | 0.10 | - | - | - | - | + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |----------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | V3_ImHashMap_23Tree_TryFind | 1 | 4.647 ns | 0.0516 ns | 0.0431 ns | 1.00 | 0.00 | - | - | - | - | + | ImmutableDict_TryGet | 1 | 23.396 ns | 0.3703 ns | 0.3092 ns | 5.04 | 0.10 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 5 | 5.242 ns | 0.1822 ns | 0.3143 ns | 1.00 | 0.00 | - | - | - | - | + | ImmutableDict_TryGet | 5 | 23.739 ns | 0.1941 ns | 0.1816 ns | 4.42 | 0.32 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 10 | 5.749 ns | 0.1206 ns | 0.1128 ns | 1.00 | 0.00 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 24.769 ns | 0.3152 ns | 0.2949 ns | 4.31 | 0.11 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 100 | 8.252 ns | 0.1754 ns | 0.1465 ns | 1.00 | 0.00 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 27.288 ns | 0.6118 ns | 0.6009 ns | 3.31 | 0.10 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 1000 | 12.053 ns | 0.1955 ns | 0.1829 ns | 1.00 | 0.00 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 30.762 ns | 0.3980 ns | 0.3723 ns | 2.55 | 0.04 | - | - | - | - | + + ### Branch3 as two Branch2 -- looks ok + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |----------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | V2_ImHashMap_AVL_TryFind | 1 | 6.236 ns | 0.2150 ns | 0.3084 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 1 | 5.467 ns | 0.1410 ns | 0.1250 ns | 0.85 | 0.05 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 5 | 7.390 ns | 0.2037 ns | 0.1701 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 5 | 5.552 ns | 0.1412 ns | 0.1252 ns | 0.75 | 0.02 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 10 | 9.402 ns | 0.1381 ns | 0.1153 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 10 | 6.820 ns | 0.1766 ns | 0.1652 ns | 0.73 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 100 | 11.085 ns | 0.2337 ns | 0.2186 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 100 | 10.257 ns | 0.2037 ns | 0.1905 ns | 0.93 | 0.02 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 1000 | 16.883 ns | 0.2438 ns | 0.2280 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 1000 | 15.516 ns | 0.3248 ns | 0.4554 ns | 0.93 | 0.04 | - | - | - | - | + + ### V3 RTM + + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 + Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=5.0.201 + [Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT + DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT + + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | V2_ImHashMap_AVL_TryFind | 1 | 5.100 ns | 0.1212 ns | 0.1134 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 1 | 5.539 ns | 0.1676 ns | 0.1568 ns | 1.09 | 0.03 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 1 | 5.813 ns | 0.1844 ns | 0.1973 ns | 1.13 | 0.04 | - | - | - | - | + | DictionarySlim_TryGetValue | 1 | 6.614 ns | 0.0839 ns | 0.0744 ns | 1.29 | 0.03 | - | - | - | - | + | Dictionary_TryGetValue | 1 | 16.495 ns | 0.1270 ns | 0.1126 ns | 3.23 | 0.06 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1 | 12.715 ns | 0.1584 ns | 0.1323 ns | 2.49 | 0.05 | - | - | - | - | + | ImmutableDict_TryGet | 1 | 22.615 ns | 0.3097 ns | 0.2418 ns | 4.42 | 0.08 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 10 | 6.270 ns | 0.1421 ns | 0.1187 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 10 | 6.597 ns | 0.1095 ns | 0.1024 ns | 1.06 | 0.02 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 10 | 5.416 ns | 0.0908 ns | 0.0805 ns | 0.87 | 0.02 | - | - | - | - | + | DictionarySlim_TryGetValue | 10 | 7.543 ns | 0.1274 ns | 0.1064 ns | 1.20 | 0.02 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 16.907 ns | 0.1892 ns | 0.1769 ns | 2.70 | 0.05 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 12.694 ns | 0.1740 ns | 0.1453 ns | 2.03 | 0.04 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 24.049 ns | 0.3034 ns | 0.2838 ns | 3.84 | 0.07 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 100 | 9.459 ns | 0.2667 ns | 0.2739 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 100 | 9.526 ns | 0.1814 ns | 0.1697 ns | 1.01 | 0.03 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 100 | 5.827 ns | 0.0867 ns | 0.0768 ns | 0.62 | 0.02 | - | - | - | - | + | DictionarySlim_TryGetValue | 100 | 6.897 ns | 0.1033 ns | 0.0967 ns | 0.73 | 0.02 | - | - | - | - | + | Dictionary_TryGetValue | 100 | 18.189 ns | 0.2100 ns | 0.1640 ns | 1.92 | 0.06 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 13.412 ns | 0.2041 ns | 0.1909 ns | 1.42 | 0.04 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 26.023 ns | 0.3854 ns | 0.3417 ns | 2.76 | 0.08 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 1000 | 15.172 ns | 0.2617 ns | 0.2320 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 1000 | 14.473 ns | 0.2030 ns | 0.1799 ns | 0.95 | 0.01 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 1000 | 8.182 ns | 0.0809 ns | 0.0756 ns | 0.54 | 0.01 | - | - | - | - | + | DictionarySlim_TryGetValue | 1000 | 6.893 ns | 0.1590 ns | 0.1410 ns | 0.45 | 0.01 | - | - | - | - | + | Dictionary_TryGetValue | 1000 | 18.230 ns | 0.2012 ns | 0.1882 ns | 1.20 | 0.02 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 14.920 ns | 0.2296 ns | 0.2035 ns | 0.98 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 29.555 ns | 0.5926 ns | 0.5543 ns | 1.95 | 0.04 | - | - | - | - | + + + ## Against static TypeDictionary by @rogeralsing (need to subtract the cost of enumerating the array when adding items to ImHashMap and PartitionedHashMap) + + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 + Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=5.0.201 + [Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT + DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |---------------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | V3_ImHashMap_GetValueOrDefault | 5 | 4.497 ns | 0.1607 ns | 0.1578 ns | 1.00 | 0.00 | - | - | - | - | + | V3_PartitionedHashMap_GetValueOrDefault | 5 | 4.763 ns | 0.1675 ns | 0.1484 ns | 1.06 | 0.05 | - | - | - | - | + | TypeDict_TryFind | 5 | 3.198 ns | 0.1221 ns | 0.1406 ns | 0.71 | 0.03 | - | - | - | - | + | Dictionary_TryGetValue | 5 | 16.248 ns | 0.3500 ns | 0.3102 ns | 3.61 | 0.12 | - | - | - | - | + + | V3_ImHashMap_GetValueOrDefault | 10 | 5.569 ns | 0.1801 ns | 0.2751 ns | 1.00 | 0.00 | - | - | - | - | + | V3_PartitionedHashMap_GetValueOrDefault | 10 | 5.016 ns | 0.1509 ns | 0.1178 ns | 0.89 | 0.05 | - | - | - | - | + | TypeDict_TryFind | 10 | 2.513 ns | 0.0824 ns | 0.0688 ns | 0.45 | 0.03 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 15.954 ns | 0.3234 ns | 0.3025 ns | 2.85 | 0.17 | - | - | - | - | + + | V3_ImHashMap_GetValueOrDefault | 20 | 6.747 ns | 0.1964 ns | 0.2017 ns | 1.00 | 0.00 | - | - | - | - | + | V3_PartitionedHashMap_GetValueOrDefault | 20 | 3.985 ns | 0.1034 ns | 0.0863 ns | 0.59 | 0.02 | - | - | - | - | + | TypeDict_TryFind | 20 | 2.476 ns | 0.1174 ns | 0.1257 ns | 0.37 | 0.02 | - | - | - | - | + | Dictionary_TryGetValue | 20 | 17.766 ns | 0.4131 ns | 0.3865 ns | 2.64 | 0.11 | - | - | - | - | + + + ## V4 + + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 + Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=6.0.202 + [Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT + DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | V4_ImHashMap_TryFind | 1 | 8.915 ns | 0.1853 ns | 0.1733 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_TryFind | 1 | 7.834 ns | 0.1769 ns | 0.1568 ns | 0.88 | 0.02 | - | - | - | - | + | V4_PartitionedHashMap_TryFind | 1 | 8.292 ns | 0.1082 ns | 0.0959 ns | 0.93 | 0.02 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 1 | 7.681 ns | 0.1245 ns | 0.1039 ns | 0.86 | 0.02 | - | - | - | - | + | DictionarySlim_TryGetValue | 1 | 8.861 ns | 0.1423 ns | 0.1188 ns | 0.99 | 0.02 | - | - | - | - | + | Dictionary_TryGetValue | 1 | 17.914 ns | 0.3447 ns | 0.3055 ns | 2.01 | 0.05 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1 | 13.381 ns | 0.2709 ns | 0.2401 ns | 1.50 | 0.04 | - | - | - | - | + | ImmutableDict_TryGet | 1 | 19.040 ns | 0.3068 ns | 0.2870 ns | 2.14 | 0.06 | - | - | - | - | + | | | | | | | | | | | | + | V4_ImHashMap_TryFind | 10 | 9.462 ns | 0.2690 ns | 0.2246 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_TryFind | 10 | 9.038 ns | 0.1650 ns | 0.1463 ns | 0.96 | 0.02 | - | - | - | - | + | V4_PartitionedHashMap_TryFind | 10 | 8.575 ns | 0.2031 ns | 0.1900 ns | 0.91 | 0.03 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 10 | 5.869 ns | 0.1026 ns | 0.1743 ns | 0.63 | 0.02 | - | - | - | - | + | DictionarySlim_TryGetValue | 10 | 7.384 ns | 0.1882 ns | 0.1668 ns | 0.78 | 0.03 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 14.082 ns | 0.2168 ns | 0.1921 ns | 1.49 | 0.04 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 11.436 ns | 0.1398 ns | 0.1239 ns | 1.21 | 0.03 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 17.927 ns | 0.4467 ns | 0.4780 ns | 1.90 | 0.09 | - | - | - | - | + | | | | | | | | | | | | + | V4_ImHashMap_TryFind | 100 | 12.147 ns | 0.1475 ns | 0.1308 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_TryFind | 100 | 11.357 ns | 0.1695 ns | 0.1323 ns | 0.94 | 0.01 | - | - | - | - | + | V4_PartitionedHashMap_TryFind | 100 | 8.520 ns | 0.2142 ns | 0.1899 ns | 0.70 | 0.02 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 100 | 7.785 ns | 0.1374 ns | 0.1147 ns | 0.64 | 0.01 | - | - | - | - | + | DictionarySlim_TryGetValue | 100 | 6.918 ns | 0.1544 ns | 0.1289 ns | 0.57 | 0.01 | - | - | - | - | + | Dictionary_TryGetValue | 100 | 13.804 ns | 0.3474 ns | 0.3717 ns | 1.14 | 0.04 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 11.432 ns | 0.2194 ns | 0.2052 ns | 0.94 | 0.02 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 21.593 ns | 0.4048 ns | 0.3786 ns | 1.78 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | V4_ImHashMap_TryFind | 1000 | 15.492 ns | 0.3859 ns | 0.4881 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_TryFind | 1000 | 14.339 ns | 0.2612 ns | 0.2444 ns | 0.92 | 0.03 | - | - | - | - | + | V4_PartitionedHashMap_TryFind | 1000 | 11.508 ns | 0.1839 ns | 0.1630 ns | 0.74 | 0.03 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 1000 | 9.508 ns | 0.2340 ns | 0.2074 ns | 0.61 | 0.02 | - | - | - | - | + | DictionarySlim_TryGetValue | 1000 | 7.099 ns | 0.1984 ns | 0.2037 ns | 0.46 | 0.01 | - | - | - | - | + | Dictionary_TryGetValue | 1000 | 13.696 ns | 0.1341 ns | 0.1120 ns | 0.88 | 0.03 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 11.358 ns | 0.1464 ns | 0.1222 ns | 0.73 | 0.03 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 25.875 ns | 0.3752 ns | 0.3510 ns | 1.67 | 0.06 | - | - | - | - | + + ## Baseline FHashMap6 vs DictionarySlim vs Dictionary + + 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 + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------:|------------:| + | Dictionary_TryGetValue | 100 | 15.234 ns | 0.3434 ns | 0.6779 ns | 14.971 ns | 1.00 | 0.00 | - | NA | + | DictionarySlim_TryGetValue | 100 | 9.790 ns | 0.5518 ns | 1.6271 ns | 8.944 ns | 0.70 | 0.11 | - | NA | + | FHashMap_TryGetValue | 100 | 20.591 ns | 0.4773 ns | 0.4232 ns | 20.521 ns | 1.32 | 0.09 | - | NA | + + ## Never happened? + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|---------:|---------:|---------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 100 | 11.14 ns | 0.613 ns | 1.749 ns | 10.75 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 100 | 11.29 ns | 0.641 ns | 1.850 ns | 10.77 ns | 1.03 | 0.20 | - | NA | + + ## Almost funny + + | Method | Count | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|----------:|------------:| + | DictionarySlim_TryGetValue | 100 | 8.703 ns | 0.0774 ns | 0.0686 ns | 1.00 | - | NA | + | FHashMap_TryGetValue | 100 | 7.181 ns | 0.0442 ns | 0.0345 ns | 0.83 | - | NA | + + ## Initial SIMD FHashMap7 vs DictionarySlim + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 100 | 8.968 ns | 0.2581 ns | 0.4168 ns | 8.796 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 100 | 8.866 ns | 0.4456 ns | 1.3069 ns | 9.244 ns | 0.86 | 0.09 | - | NA | + + ## Round 2 and 3, SIMD FHashMap7 vs DictionarySlim + + | Method | Count | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|----------:|------------:| + | DictionarySlim_TryGetValue | 100 | 8.925 ns | 0.0383 ns | 0.0299 ns | 1.00 | - | NA | + | FHashMap_TryGetValue | 100 | 6.376 ns | 0.1207 ns | 0.1129 ns | 0.72 | - | NA | + + ## Funny 4, SIMD FHashMap7 vs DictionarySlim + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 100 | 9.131 ns | 0.1991 ns | 0.1663 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 100 | 6.020 ns | 0.1766 ns | 0.2644 ns | 0.65 | 0.04 | - | NA | + + ## FHashMap7 vs DictionarySlim (multiple params) + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 10.945 ns | 0.3136 ns | 0.5574 ns | 10.770 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1 | 6.852 ns | 0.4387 ns | 1.2157 ns | 7.022 ns | 0.57 | 0.11 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 12.432 ns | 0.7856 ns | 2.3042 ns | 12.383 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 10 | 5.161 ns | 0.1755 ns | 0.2932 ns | 5.050 ns | 0.38 | 0.05 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 16.811 ns | 0.7006 ns | 2.0657 ns | 15.940 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 100 | 9.176 ns | 0.6841 ns | 1.9517 ns | 8.838 ns | 0.56 | 0.14 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 11.542 ns | 0.3251 ns | 0.7535 ns | 11.400 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1000 | 9.579 ns | 0.4608 ns | 1.2921 ns | 9.556 ns | 0.82 | 0.12 | - | NA | + + ## No!!! SIMD FHashMap7 vs DictionarySlim (multiple params) + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 9.134 ns | 0.2546 ns | 0.3569 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1 | 5.174 ns | 0.0317 ns | 0.0281 ns | 0.56 | 0.02 | - | NA | + | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 8.908 ns | 0.0430 ns | 0.0359 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 10 | 5.615 ns | 0.0722 ns | 0.0675 ns | 0.63 | 0.01 | - | NA | + | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 8.964 ns | 0.1536 ns | 0.1282 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 100 | 7.468 ns | 0.2187 ns | 0.2148 ns | 0.83 | 0.03 | - | NA | + | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 8.826 ns | 0.0929 ns | 0.0776 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1000 | 5.271 ns | 0.1362 ns | 0.1514 ns | 0.60 | 0.02 | - | NA | + + ## Small opti No!!! SIMD FHashMap7 vs DictionarySlim (multiple params) + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 8.965 ns | 0.3526 ns | 1.0285 ns | 8.416 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1 | 3.940 ns | 0.1476 ns | 0.2117 ns | 3.894 ns | 0.43 | 0.05 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 9.154 ns | 0.2472 ns | 0.3036 ns | 9.041 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 10 | 5.734 ns | 0.0922 ns | 0.0770 ns | 5.733 ns | 0.62 | 0.02 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 8.638 ns | 0.1980 ns | 0.1654 ns | 8.599 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 100 | 7.259 ns | 0.1361 ns | 0.1206 ns | 7.212 ns | 0.84 | 0.02 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 9.280 ns | 0.2481 ns | 0.4537 ns | 9.115 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1000 | 3.765 ns | 0.0650 ns | 0.0576 ns | 3.760 ns | 0.40 | 0.02 | - | NA | + + ## Adding small SIMD to FHashMap7 vs DictionarySlim (multiple params) - winning the medium + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 9.314 ns | 0.2492 ns | 0.4494 ns | 9.149 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1 | 4.819 ns | 0.1521 ns | 0.1423 ns | 4.775 ns | 0.51 | 0.03 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 9.012 ns | 0.2230 ns | 0.4297 ns | 8.841 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 10 | 6.407 ns | 0.1963 ns | 0.2182 ns | 6.375 ns | 0.70 | 0.05 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 9.261 ns | 0.2620 ns | 0.6019 ns | 8.994 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 100 | 7.136 ns | 0.1941 ns | 0.1515 ns | 7.104 ns | 0.74 | 0.06 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 9.066 ns | 0.1819 ns | 0.1612 ns | 9.013 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1000 | 4.935 ns | 0.1353 ns | 0.1661 ns | 4.891 ns | 0.55 | 0.02 | - | NA | + + ## Lookup FHashMap8 vs DictionarySlim + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 9.446 ns | 0.2718 ns | 0.5038 ns | 9.329 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 1 | 5.485 ns | 0.1832 ns | 0.3009 ns | 5.402 ns | 0.58 | 0.04 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 9.659 ns | 0.2123 ns | 0.1882 ns | 9.654 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 10 | 7.044 ns | 0.1443 ns | 0.1205 ns | 7.003 ns | 0.73 | 0.01 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 9.368 ns | 0.2697 ns | 0.3600 ns | 9.259 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 100 | 9.770 ns | 0.2736 ns | 0.3360 ns | 9.748 ns | 1.04 | 0.06 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 9.743 ns | 0.2676 ns | 0.4686 ns | 9.573 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 1000 | 6.043 ns | 0.1946 ns | 0.4146 ns | 5.883 ns | 0.62 | 0.05 | - | NA | + + ## Lookup FHashMap8 vs DictionarySlim after simplifying + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 8.649 ns | 0.0707 ns | 0.0662 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 1 | 5.447 ns | 0.0784 ns | 0.0654 ns | 0.63 | 0.01 | - | NA | + | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 8.948 ns | 0.0405 ns | 0.0316 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 10 | 5.471 ns | 0.1656 ns | 0.2375 ns | 0.62 | 0.03 | - | NA | + | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 9.081 ns | 0.2568 ns | 0.5583 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 100 | 6.294 ns | 0.1585 ns | 0.1886 ns | 0.69 | 0.05 | - | NA | + | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 8.990 ns | 0.2087 ns | 0.1850 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 1000 | 4.950 ns | 0.1508 ns | 0.2719 ns | 0.55 | 0.03 | - | NA | + + ## Lookup FHashMap7 vs FHashMap8 vs DictionarySlim vs Dictionary + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 9.470 ns | 0.2655 ns | 0.7269 ns | 9.193 ns | 1.00 | 0.00 | - | NA | + | Dictionary_TryGetValue | 1 | 12.842 ns | 0.3375 ns | 0.7192 ns | 12.628 ns | 1.36 | 0.12 | - | NA | + | FHashMap8_TryGetValue | 1 | 5.212 ns | 0.1734 ns | 0.1448 ns | 5.202 ns | 0.52 | 0.05 | - | NA | + | FHashMap7_TryGetValue | 1 | 4.365 ns | 0.1630 ns | 0.2981 ns | 4.269 ns | 0.45 | 0.05 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 9.585 ns | 0.2755 ns | 0.7992 ns | 9.292 ns | 1.00 | 0.00 | - | NA | + | Dictionary_TryGetValue | 10 | 10.675 ns | 0.2063 ns | 0.1723 ns | 10.674 ns | 1.13 | 0.10 | - | NA | + | FHashMap8_TryGetValue | 10 | 5.584 ns | 0.1895 ns | 0.2028 ns | 5.546 ns | 0.57 | 0.07 | - | NA | + | FHashMap7_TryGetValue | 10 | 5.676 ns | 0.0982 ns | 0.0964 ns | 5.680 ns | 0.58 | 0.06 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 11.095 ns | 0.4470 ns | 1.2236 ns | 11.427 ns | 1.00 | 0.00 | - | NA | + | Dictionary_TryGetValue | 100 | 10.694 ns | 0.2975 ns | 0.7013 ns | 10.484 ns | 0.94 | 0.11 | - | NA | + | FHashMap8_TryGetValue | 100 | 7.313 ns | 0.2226 ns | 0.4071 ns | 7.107 ns | 0.65 | 0.07 | - | NA | + | FHashMap7_TryGetValue | 100 | 7.119 ns | 0.2135 ns | 0.1997 ns | 7.095 ns | 0.67 | 0.11 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 9.049 ns | 0.2662 ns | 0.5953 ns | 8.826 ns | 1.00 | 0.00 | - | NA | + | Dictionary_TryGetValue | 1000 | 13.465 ns | 0.3476 ns | 0.3251 ns | 13.483 ns | 1.50 | 0.12 | - | NA | + | FHashMap8_TryGetValue | 1000 | 4.926 ns | 0.1781 ns | 0.3120 ns | 4.809 ns | 0.54 | 0.05 | - | NA | + | FHashMap7_TryGetValue | 1000 | 4.314 ns | 0.1559 ns | 0.1601 ns | 4.290 ns | 0.48 | 0.04 | - | NA | + + ## FHM9.1 vs Dict and DictSlim + + BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1702/22H2/2022Update/SunValley2) + 11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores + .NET SDK=7.0.304 + [Host] : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 + DefaultJob : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | + |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------------------:|---------------:|------------------------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 6.236 ns | 0.1823 ns | 0.1872 ns | 6.221 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | Dictionary_TryGetValue | 1 | 10.070 ns | 0.2035 ns | 0.2090 ns | 10.038 ns | 1.62 | 0.05 | 31 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 1 | 5.084 ns | 0.2665 ns | 0.7773 ns | 5.267 ns | 0.67 | 0.08 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 8.515 ns | 0.2330 ns | 0.3763 ns | 8.407 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | Dictionary_TryGetValue | 10 | 10.296 ns | 0.4517 ns | 1.3247 ns | 10.439 ns | 1.30 | 0.10 | 25 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 10 | 3.857 ns | 0.1311 ns | 0.2904 ns | 3.777 ns | 0.46 | 0.04 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 7.105 ns | 0.1931 ns | 0.5221 ns | 6.860 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | Dictionary_TryGetValue | 100 | 8.443 ns | 0.2081 ns | 0.2397 ns | 8.402 ns | 1.14 | 0.11 | 25 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 100 | 4.037 ns | 0.1339 ns | 0.2201 ns | 3.994 ns | 0.56 | 0.06 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 6.325 ns | 0.1933 ns | 0.4632 ns | 6.124 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | Dictionary_TryGetValue | 1000 | 9.146 ns | 0.2185 ns | 0.2044 ns | 9.176 ns | 1.42 | 0.10 | 25 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 1000 | 5.917 ns | 0.1505 ns | 0.1334 ns | 5.909 ns | 0.92 | 0.07 | 13 | 0 | 0 | - | NA | + + ## FHM9.1 sparse entries + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | CacheMisses/Op | BranchInstructions/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|---------------:|----------------------:|------------------------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 7.813 ns | 0.2096 ns | 0.2243 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | + | FHashMap91_TryGetValue | 1 | 4.608 ns | 0.1493 ns | 0.1888 ns | 0.59 | 0.03 | 0 | 13 | -0 | - | NA | + | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 7.677 ns | 0.1647 ns | 0.1375 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | + | FHashMap91_TryGetValue | 10 | 4.488 ns | 0.1183 ns | 0.1049 ns | 0.59 | 0.02 | 0 | 13 | 0 | - | NA | + | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 7.824 ns | 0.2102 ns | 0.2947 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | + | FHashMap91_TryGetValue | 100 | 3.459 ns | 0.1538 ns | 0.3277 ns | 0.45 | 0.05 | -0 | 13 | -0 | - | NA | + | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 7.608 ns | 0.1997 ns | 0.1770 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | + | FHashMap91_TryGetValue | 1000 | 6.384 ns | 0.1427 ns | 0.1114 ns | 0.84 | 0.03 | 0 | 14 | 0 | - | NA | + + ## No more sparce anymore + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | + |------------------------------ |------ |---------:|----------:|----------:|---------:|------:|--------:|----------------------:|---------------:|------------------------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 6.759 ns | 0.2002 ns | 0.2603 ns | 6.704 ns | 1.00 | 0.00 | 20 | 0 | -0 | - | NA | + | FHashMap91_TryGetValue | 1 | 4.512 ns | 0.1337 ns | 0.2271 ns | 4.432 ns | 0.67 | 0.05 | 11 | 0 | -0 | - | NA | + | FHashMap91_TryGetValue_Golden | 1 | 4.187 ns | 0.1022 ns | 0.0906 ns | 4.167 ns | 0.62 | 0.03 | 11 | -0 | -0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 8.225 ns | 0.4388 ns | 1.2938 ns | 7.602 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 10 | 4.434 ns | 0.1415 ns | 0.1453 ns | 4.434 ns | 0.58 | 0.06 | 11 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue_Golden | 10 | 4.498 ns | 0.1236 ns | 0.1033 ns | 4.472 ns | 0.59 | 0.05 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 7.067 ns | 0.1470 ns | 0.1303 ns | 7.080 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 100 | 4.481 ns | 0.1484 ns | 0.2311 ns | 4.398 ns | 0.64 | 0.04 | 11 | 0 | -0 | - | NA | + | FHashMap91_TryGetValue_Golden | 100 | 8.215 ns | 0.2181 ns | 0.2240 ns | 8.178 ns | 1.17 | 0.04 | 17 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 8.744 ns | 0.6189 ns | 1.8151 ns | 7.838 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 1000 | 5.755 ns | 0.1660 ns | 0.1912 ns | 5.715 ns | 0.73 | 0.09 | 14 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue_Golden | 1000 | 7.732 ns | 0.1953 ns | 0.2325 ns | 7.705 ns | 0.97 | 0.13 | 17 | 0 | 0 | - | NA | + + ## Final load factor 87.5% and no golden results + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | BranchInstructions/Op | BranchMispredictions/Op | CacheMisses/Op | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------------------:|------------------------:|---------------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 6.830 ns | 0.1073 ns | 0.1004 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 1 | 4.230 ns | 0.1149 ns | 0.1075 ns | 0.62 | 0.02 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 6.980 ns | 0.0918 ns | 0.0717 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 10 | 4.259 ns | 0.0673 ns | 0.0562 ns | 0.61 | 0.01 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 6.975 ns | 0.1016 ns | 0.0849 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 100 | 4.414 ns | 0.1426 ns | 0.2342 ns | 0.64 | 0.03 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 6.889 ns | 0.0773 ns | 0.0685 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 1000 | 5.633 ns | 0.1079 ns | 0.1243 ns | 0.82 | 0.02 | 14 | 0 | 0 | - | NA | + + ## ArrayEntries + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | CacheMisses/Op | BranchInstructions/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|---------------:|----------------------:|------------------------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1000 | 7.223 ns | 0.2535 ns | 0.7193 ns | 6.887 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | + | FHashMap91_TryGetValue | 1000 | 5.108 ns | 0.1634 ns | 0.2819 ns | 4.996 ns | 0.69 | 0.09 | -0 | 14 | 0 | - | NA | + + ## ArrayArrayEntries + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | BranchMispredictions/Op | CacheMisses/Op | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------------------:|------------------------:|---------------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 7.053 ns | 0.1174 ns | 0.1041 ns | 7.021 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 1 | 4.948 ns | 0.1446 ns | 0.2119 ns | 4.845 ns | 0.70 | 0.04 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 7.475 ns | 0.1732 ns | 0.2747 ns | 7.384 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 10 | 4.900 ns | 0.1541 ns | 0.1366 ns | 4.862 ns | 0.65 | 0.03 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 8.120 ns | 0.4747 ns | 1.3995 ns | 7.386 ns | 1.00 | 0.00 | 20 | -0 | 0 | - | NA | + | FHashMap91_TryGetValue | 100 | 5.129 ns | 0.1593 ns | 0.4251 ns | 4.985 ns | 0.64 | 0.12 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 7.289 ns | 0.1912 ns | 0.3088 ns | 7.137 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 1000 | 6.262 ns | 0.1232 ns | 0.0962 ns | 6.277 ns | 0.86 | 0.04 | 14 | 0 | 0 | - | NA | + + ## SmallMap + net8.0 + + BenchmarkDotNet v0.13.10, Windows 11 (10.0.22621.2428/22H2/2022Update/SunValley2) + 11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores + .NET SDK 8.0.100-rc.2.23502.2 + [Host] : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2 + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | BranchInstructions/Op | BranchMispredictions/Op | CacheMisses/Op | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------------------:|------------------------:|---------------:|----------:|------------:| + | Dictionary_TryGetValue | 100 | 6.938 ns | 0.1719 ns | 0.2574 ns | 1.00 | 0.00 | 21 | 0 | 0 | - | NA | + | DictionarySlim_TryGetValue | 100 | 5.371 ns | 0.1277 ns | 0.1132 ns | 0.77 | 0.03 | 15 | 0 | 0 | - | NA | + | SmallMap_TryGetValue | 100 | 3.718 ns | 0.1360 ns | 0.2453 ns | 0.54 | 0.04 | 9 | 0 | 0 | - | NA | + + + ## SmallMap + FEC_FHashMap + net6.0 + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------------------:|---------------:|------------------------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 5.592 ns | 0.4244 ns | 1.2448 ns | 4.835 ns | 1.00 | 0.00 | 15 | 0 | 0 | - | NA | + | SmallMap_TryGetValue | 1 | 2.958 ns | 0.1059 ns | 0.0990 ns | 2.970 ns | 0.44 | 0.03 | 9 | -0 | -0 | - | NA | + | FHashMap_TryGetValue | 1 | 1.297 ns | 0.0757 ns | 0.0777 ns | 1.288 ns | 0.19 | 0.01 | 6 | -0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 5.208 ns | 0.1485 ns | 0.1240 ns | 5.181 ns | 1.00 | 0.00 | 15 | 0 | 0 | - | NA | + | SmallMap_TryGetValue | 10 | 3.709 ns | 0.1739 ns | 0.4551 ns | 3.548 ns | 0.69 | 0.07 | 9 | 0 | 0 | - | NA | + | FHashMap_TryGetValue | 10 | 5.539 ns | 0.4004 ns | 1.1487 ns | 5.612 ns | 0.98 | 0.22 | 10 | -0 | -0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 5.296 ns | 0.1654 ns | 0.3898 ns | 5.176 ns | 1.00 | 0.00 | 15 | 0 | 0 | - | NA | + | SmallMap_TryGetValue | 100 | 3.549 ns | 0.1293 ns | 0.2490 ns | 3.486 ns | 0.66 | 0.07 | 9 | 0 | 0 | - | NA | + | FHashMap_TryGetValue | 100 | 4.265 ns | 0.1406 ns | 0.1098 ns | 4.268 ns | 0.80 | 0.08 | 10 | 0 | -0 | - | NA | + + ## SmallMap SIMD vs... + + BenchmarkDotNet v0.15.0, Windows 11 (10.0.26100.4202/24H2/2024Update/HudsonValley) + Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET SDK 9.0.203 + [Host] : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2 + DefaultJob : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2 + + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|-----:|----------:|------------:| + | FHashMap_TryGetValue | 1 | 1.580 ns | 0.0212 ns | 0.0199 ns | 1.587 ns | 0.35 | 0.03 | 1 | - | NA | + | DictionarySlim_TryGetValue | 1 | 4.592 ns | 0.1535 ns | 0.4202 ns | 4.452 ns | 1.01 | 0.13 | 2 | - | NA | + | SmallMap_TryGetValue | 1 | 5.367 ns | 0.0884 ns | 0.0738 ns | 5.378 ns | 1.18 | 0.10 | 3 | - | NA | + | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 4.165 ns | 0.0467 ns | 0.0414 ns | 4.156 ns | 1.00 | 0.01 | 1 | - | NA | + | FHashMap_TryGetValue | 10 | 4.455 ns | 0.0285 ns | 0.0253 ns | 4.457 ns | 1.07 | 0.01 | 2 | - | NA | + | SmallMap_TryGetValue | 10 | 5.302 ns | 0.1556 ns | 0.1852 ns | 5.369 ns | 1.27 | 0.05 | 3 | - | NA | + | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 4.777 ns | 0.1559 ns | 0.4472 ns | 4.576 ns | 1.01 | 0.13 | 1 | - | NA | + | FHashMap_TryGetValue | 100 | 6.020 ns | 0.1795 ns | 0.2335 ns | 6.017 ns | 1.27 | 0.13 | 2 | - | NA | + | SmallMap_TryGetValue | 100 | 6.021 ns | 0.1808 ns | 0.4827 ns | 6.293 ns | 1.27 | 0.15 | 2 | - | NA | + + ## SmallMap vs FHashMap11 + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|-----:|----------:|------------:| + | DictionarySlim_TryGetValue | 1000 | 2.225 us | 0.0445 us | 0.1313 us | 2.162 us | 1.00 | 0.08 | 1 | - | NA | + | SmallMap_TryGetValue | 1000 | 2.364 us | 0.0387 us | 0.0302 us | 2.377 us | 1.07 | 0.06 | 1 | - | NA | + | FHashMap11_TryGetValue | 1000 | 2.387 us | 0.0477 us | 0.1170 us | 2.434 us | 1.08 | 0.08 | 1 | - | NA | + + ## APL FHashMap11 + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|-----:|----------:|------------:| + | DictionarySlim_TryGetValue | 1000 | 2.195 us | 0.0437 us | 0.1217 us | 1.00 | 0.08 | 1 | - | NA | + | FHashMap11_TryGetValue | 1000 | 2.333 us | 0.0465 us | 0.0929 us | 1.07 | 0.07 | 2 | - | NA | + | SmallMap_TryGetValue | 1000 | 2.514 us | 0.0489 us | 0.0789 us | 1.15 | 0.07 | 3 | - | NA | + + ## Reshuffle the lookup comparison + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | + |--------------------------- |------ |------------:|----------:|-----------:|------:|--------:|-----:|----------:|------------:| + | SmallMap_TryGetValue | 10 | 44.90 ns | 0.536 ns | 0.419 ns | 0.99 | 0.04 | 1 | - | NA | + | DictionarySlim_TryGetValue | 10 | 45.43 ns | 0.941 ns | 1.857 ns | 1.00 | 0.06 | 1 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 385.36 ns | 4.621 ns | 4.097 ns | 1.00 | 0.01 | 1 | - | NA | + | SmallMap_TryGetValue | 100 | 513.52 ns | 5.702 ns | 5.054 ns | 1.33 | 0.02 | 2 | - | NA | + | | | | | | | | | | | + | SmallMap_TryGetValue | 1000 | 4,255.63 ns | 82.286 ns | 130.514 ns | 0.89 | 0.04 | 1 | - | NA | + | DictionarySlim_TryGetValue | 1000 | 4,794.12 ns | 95.912 ns | 189.322 ns | 1.00 | 0.06 | 2 | - | NA | + + ## Stack hybrid from the FEC is tested + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|--------:|---------:|------:|--------:|-----:|-------:|----------:|------------:| + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 203.5 ns | 3.88 ns | 3.63 ns | 0.70 | 0.03 | 1 | 0.1147 | 720 B | 0.67 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 277.6 ns | 3.61 ns | 5.52 ns | 0.95 | 0.04 | 2 | - | - | 0.00 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 292.1 ns | 5.84 ns | 12.19 ns | 1.00 | 0.06 | 2 | 0.1707 | 1072 B | 1.00 | + + ## After inlining in the FecHashMap + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |-----------:|---------:|---------:|-----------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 110.8 ns | 2.26 ns | 4.90 ns | 110.1 ns | 0.38 | 0.02 | 1 | - | - | - | 0.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 194.1 ns | 1.80 ns | 1.50 ns | 194.2 ns | 0.67 | 0.03 | 2 | 0.1147 | - | 720 B | 0.67 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 288.7 ns | 5.82 ns | 14.04 ns | 280.1 ns | 1.00 | 0.07 | 3 | 0.1707 | - | 1072 B | 1.00 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,003.8 ns | 30.26 ns | 31.08 ns | 1,995.1 ns | 1.00 | 0.02 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,306.8 ns | 18.67 ns | 15.59 ns | 2,307.0 ns | 1.15 | 0.02 | 2 | 0.8507 | - | 5344 B | 0.71 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 3,473.7 ns | 15.97 ns | 13.34 ns | 3,470.3 ns | 1.73 | 0.03 | 3 | 0.7782 | - | 4904 B | 0.65 | + + ## Probes are separate from the packed hashes and indexes into their own array + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|----------:|----------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.118 us | 0.0421 us | 0.0679 us | 1.00 | 0.04 | 1 | 1.1902 | 0.0229 | 7.31 KB | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.346 us | 0.0404 us | 0.0378 us | 1.11 | 0.04 | 2 | 0.8507 | - | 5.22 KB | 0.71 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 3.294 us | 0.0648 us | 0.0970 us | 1.56 | 0.07 | 3 | 0.8278 | - | 5.08 KB | 0.69 | + + ## GoldenRatio Hash adjustment + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.150 us | 0.0428 us | 0.0783 us | 2.115 us | 1.00 | 0.05 | 1 | 1.1902 | 0.0229 | 7.31 KB | 1.00 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 3.092 us | 0.0608 us | 0.0871 us | 3.068 us | 1.44 | 0.06 | 2 | 0.8278 | - | 5.08 KB | 0.69 | + + ## After probes go to their own array + padding + wrapping + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |------------:|----------:|------------:|------------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 111.6 ns | 1.16 ns | 0.97 ns | 111.8 ns | 0.38 | 0.01 | 1 | - | - | - | 0.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 205.1 ns | 3.46 ns | 3.24 ns | 204.4 ns | 0.70 | 0.02 | 2 | 0.1147 | - | 720 B | 0.67 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 292.2 ns | 5.53 ns | 5.68 ns | 291.4 ns | 1.00 | 0.03 | 3 | 0.1707 | - | 1072 B | 1.00 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,121.8 ns | 42.34 ns | 91.14 ns | 2,070.2 ns | 1.00 | 0.06 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,181.5 ns | 43.38 ns | 91.50 ns | 2,127.6 ns | 1.03 | 0.06 | 1 | 0.8659 | 0.0038 | 5440 B | 0.73 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,347.8 ns | 14.31 ns | 12.69 ns | 2,343.7 ns | 1.11 | 0.05 | 2 | 0.8507 | - | 5344 B | 0.71 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 23,640.4 ns | 280.02 ns | 248.23 ns | 23,620.0 ns | 1.00 | 0.01 | 1 | 9.1553 | 1.2817 | 57808 B | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 45,226.9 ns | 293.55 ns | 245.13 ns | 45,221.9 ns | 1.91 | 0.02 | 2 | 7.8735 | 0.3052 | 49544 B | 0.86 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 49,441.1 ns | 987.34 ns | 1,780.38 ns | 49,657.1 ns | 2.09 | 0.08 | 3 | 8.9111 | 0.0610 | 55976 B | 0.97 | + + ## Initial SIMD for Lookup - Regression in @perf + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |------------:|----------:|------------:|------------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 111.3 ns | 1.88 ns | 1.67 ns | 110.6 ns | 0.38 | 0.01 | 1 | - | - | - | 0.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 211.8 ns | 4.21 ns | 7.91 ns | 210.4 ns | 0.72 | 0.03 | 2 | 0.1147 | - | 720 B | 0.67 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 293.1 ns | 5.18 ns | 4.33 ns | 293.5 ns | 1.00 | 0.02 | 3 | 0.1707 | - | 1072 B | 1.00 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,112.8 ns | 19.17 ns | 16.00 ns | 2,108.5 ns | 1.00 | 0.01 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,433.3 ns | 47.99 ns | 95.84 ns | 2,383.6 ns | 1.15 | 0.05 | 2 | 0.8507 | - | 5344 B | 0.71 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,548.7 ns | 50.70 ns | 106.95 ns | 2,491.5 ns | 1.21 | 0.05 | 2 | 0.8659 | 0.0038 | 5440 B | 0.73 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 23,127.6 ns | 233.36 ns | 182.20 ns | 23,143.0 ns | 1.00 | 0.01 | 1 | 9.1553 | 1.2817 | 57808 B | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 45,630.3 ns | 912.10 ns | 1,735.36 ns | 44,696.8 ns | 1.97 | 0.08 | 2 | 7.8735 | 0.3052 | 49544 B | 0.86 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 49,295.9 ns | 327.44 ns | 273.43 ns | 49,364.4 ns | 2.13 | 0.02 | 3 | 8.9111 | 0.0610 | 55976 B | 0.97 | + + ## Using StableArrayEntries + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |------------:|------------:|------------:|------------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 113.3 ns | 2.32 ns | 5.42 ns | 110.1 ns | 0.37 | 0.02 | 1 | - | - | - | 0.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 204.5 ns | 2.73 ns | 2.42 ns | 204.0 ns | 0.67 | 0.03 | 2 | 0.1147 | - | 720 B | 0.67 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 305.6 ns | 6.11 ns | 14.40 ns | 299.0 ns | 1.00 | 0.06 | 3 | 0.1707 | - | 1072 B | 1.00 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,172.7 ns | 43.50 ns | 102.53 ns | 2,138.9 ns | 1.00 | 0.07 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,317.2 ns | 46.33 ns | 100.71 ns | 2,255.9 ns | 1.07 | 0.07 | 1 | 0.4921 | - | 3088 B | 0.41 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,452.2 ns | 49.04 ns | 109.69 ns | 2,512.7 ns | 1.13 | 0.07 | 2 | 0.8507 | - | 5344 B | 0.71 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 29,665.4 ns | 588.68 ns | 1,581.45 ns | 29,786.1 ns | 1.00 | 0.08 | 1 | 9.1553 | 1.2817 | 57808 B | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 55,723.9 ns | 1,098.60 ns | 1,078.97 ns | 55,599.6 ns | 1.88 | 0.12 | 2 | 7.8735 | 0.3052 | 49544 B | 0.86 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 63,373.1 ns | 1,260.87 ns | 2,106.62 ns | 63,624.5 ns | 2.14 | 0.14 | 3 | 6.2256 | 0.2441 | 39288 B | 0.68 | + + ## Packing indexes, hashes and probes together + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|---------:|---------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 24.17 us | 0.483 us | 1.176 us | 23.48 us | 1.00 | 0.07 | 1 | 9.1553 | 1.2817 | 56.45 KB | 1.00 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 52.85 us | 1.053 us | 1.871 us | 52.14 us | 2.19 | 0.13 | 2 | 7.8125 | 0.2441 | 48.05 KB | 0.85 | + + ## Packing indexes, hashes and probes together with Resize vs no Resize + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |-------------------------------------------------------------- |------ |---------:|---------:|---------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_Init1000_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 36.08 us | 0.329 us | 0.275 us | 36.08 us | 0.66 | 0.03 | 1 | 7.9346 | 0.4883 | 48.73 KB | 1.01 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 55.17 us | 0.985 us | 2.453 us | 54.41 us | 1.00 | 0.06 | 2 | 7.8125 | 0.2441 | 48.05 KB | 1.00 | + + ## SmallMap is meta longs SingleArrayEntries with index|hash-higher-bits|probe + + BenchmarkDotNet v0.15.2, Windows 11 (10.0.26200.9457) + Unknown processor + .NET SDK 11.0.100-rc.1.26425.128 + [Host] : .NET 10.0.11 (10.0.1126.37416), X64 RyuJIT AVX2 + DefaultJob : .NET 10.0.11 (10.0.1126.37416), X64 RyuJIT AVX2 + + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|---------:|---------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 23.41 us | 0.468 us | 1.066 us | 23.33 us | 1.00 | 0.06 | 1 | 9.1553 | 1.2817 | 56.45 KB | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 36.28 us | 0.718 us | 1.331 us | 37.02 us | 1.55 | 0.09 | 2 | 7.8735 | 0.3052 | 48.38 KB | 0.86 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 43.45 us | 0.868 us | 1.652 us | 44.48 us | 1.86 | 0.11 | 3 | 7.8735 | - | 48.23 KB | 0.85 | + + */ + // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] + // [Params(10, 100, 1000)] + [Params(1000)] + public int Count; + + private Type[] _presentKeys; + private Type[] _randomPresentKeys; + + [GlobalSetup] + public void Populate() + { + _presentKeys = _keys.Take(Count).ToArray(); + _randomPresentKeys = _presentKeys.OrderBy(_ => _seed.Next()).ToArray(); + + // _mapV4 = V4_ImHashMap_AddOrUpdate(); + // _mapV3 = V3_ImHashMap_AddOrUpdate(); + // _mapV2 = V2_AddOrUpdate(); + // _partMapV4 = V4_PartitionedHashMap_AddOrUpdate(); + // _partMapV3 = V3_PartitionedHashMap_AddOrUpdate(); + // _typeDict = TypeDictionary_Add(); + // _dict = Dict(); + // _dictSlim = DictSlim(); + // _fHashMap7 = FillFHashMap7(); + // _fHashMap9 = FillFHashMap9(); + // _fHashMap91 = FillFHashMap91(); + // _smallMap = FillSmallMap(); + // _fHashMap11 = FillFHashMap11(); + + // _fecSmallMap = new FecSmallMapTypeString(); + // FillFecHashMap(ref _fecSmallMap); + + // _concurrentDict = ConcurrentDict(); + // _immutableDict = ImmutableDict(); + } - public void Add(TValue value) - { - lock (_lockObject) - { - var id = TypeKey.Id; - if (id >= _values.Length) - Array.Resize(ref _values, id * 2); - _values[id] = value; - } - } + #region Population - public TValue Get() - { - var id = TypeKey.Id; - return id >= _values.Length ? default : _values[id]; - } + private ImTools.V2.ImHashMap _mapV2; + public ImTools.V2.ImHashMap V2_AddOrUpdate() + { + var map = ImTools.V2.ImHashMap.Empty; - // ReSharper disable once UnusedTypeParameter - private static class TypeKey - { - // ReSharper disable once StaticMemberInGenericType - internal static readonly int Id = Interlocked.Increment(ref typeIndex); - } + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key, "a"); + + map = map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; } - [HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchMispredictions, HardwareCounter.BranchInstructions)] - [MemoryDiagnoser] - public class Lookup - { - /* - ## 21.01.2019: All versions. - - Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |---------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault_v1 | 13.74 ns | 0.0686 ns | 0.0642 ns | 0.79 | - | - | - | - | - GetValueOrDefault | 17.43 ns | 0.0924 ns | 0.0864 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v2 | 19.15 ns | 0.0786 ns | 0.0656 ns | 1.10 | - | - | - | - | - GetValueOrDefault_v3 | 25.73 ns | 0.0711 ns | 0.0665 ns | 1.48 | - | - | - | - | - - ## For some reason dropping lookup speed with only changes to AddOrUpdate - - Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |---------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault_v1 | 13.89 ns | 0.0938 ns | 0.0877 ns | 0.80 | - | - | - | - | - GetValueOrDefault | 17.40 ns | 0.0888 ns | 0.0831 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v2 | 19.04 ns | 0.0712 ns | 0.0666 ns | 1.09 | - | - | - | - | - GetValueOrDefault_v3 | 25.93 ns | 0.0474 ns | 0.0420 ns | 1.49 | - | - | - | - | - - ## Got back some perf by moving GetValueOrDefault to static method and specializing for Type - - Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |---------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault_v1 | 13.87 ns | 0.0400 ns | 0.0355 ns | 0.85 | - | - | - | - | - GetValueOrDefault | 16.34 ns | 0.0932 ns | 0.0826 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v2 | 19.18 ns | 0.0460 ns | 0.0430 ns | 1.17 | - | - | - | - | - GetValueOrDefault_v3 | 25.96 ns | 0.0756 ns | 0.0707 ns | 1.59 | - | - | - | - | - - ## Benchmark against variety of inputs on par with Populate benchmark - - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault_v1 | 5 | 6.155 ns | 0.0321 ns | 0.0301 ns | 0.98 | 0.01 | - | - | - | - | - GetValueOrDefault | 5 | 6.267 ns | 0.0510 ns | 0.0452 ns | 1.00 | 0.00 | - | - | - | - | - GetValueOrDefault_v2 | 5 | 7.439 ns | 0.0763 ns | 0.0676 ns | 1.19 | 0.02 | - | - | - | - | - GetValueOrDefault_v3 | 5 | 9.558 ns | 0.0409 ns | 0.0383 ns | 1.52 | 0.01 | - | - | - | - | - | | | | | | | | | | | - GetValueOrDefault_v1 | 40 | 10.897 ns | 0.0673 ns | 0.0629 ns | 0.95 | 0.01 | - | - | - | - | - GetValueOrDefault | 40 | 11.467 ns | 0.0325 ns | 0.0304 ns | 1.00 | 0.00 | - | - | - | - | - GetValueOrDefault_v2 | 40 | 14.012 ns | 0.1092 ns | 0.1022 ns | 1.22 | 0.01 | - | - | - | - | - GetValueOrDefault_v3 | 40 | 19.945 ns | 0.1032 ns | 0.0965 ns | 1.74 | 0.01 | - | - | - | - | - | | | | | | | | | | | - GetValueOrDefault_v1 | 200 | 13.664 ns | 0.0291 ns | 0.0258 ns | 0.97 | 0.00 | - | - | - | - | - GetValueOrDefault | 200 | 14.051 ns | 0.0524 ns | 0.0491 ns | 1.00 | 0.00 | - | - | - | - | - GetValueOrDefault_v2 | 200 | 16.722 ns | 0.0568 ns | 0.0531 ns | 1.19 | 0.01 | - | - | - | - | - GetValueOrDefault_v3 | 200 | 24.473 ns | 0.0792 ns | 0.0702 ns | 1.74 | 0.01 | - | - | - | - | - | | | | | | | | | | | - GetValueOrDefault_v1 | 1000 | 14.213 ns | 0.1528 ns | 0.1354 ns | 0.96 | 0.01 | - | - | - | - | - GetValueOrDefault | 1000 | 14.805 ns | 0.0518 ns | 0.0485 ns | 1.00 | 0.00 | - | - | - | - | - GetValueOrDefault_v2 | 1000 | 16.645 ns | 0.0447 ns | 0.0419 ns | 1.12 | 0.01 | - | - | - | - | - GetValueOrDefault_v3 | 1000 | 27.489 ns | 0.0890 ns | 0.0832 ns | 1.86 | 0.01 | - | - | - | - | - - ## Adding aggressive inlining to the Data { Hash, Key, Value } properties - - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault | 5 | 5.853 ns | 0.0685 ns | 0.0607 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v1 | 5 | 5.913 ns | 0.0373 ns | 0.0349 ns | 1.01 | - | - | - | - | - | | | | | | | | | | - GetValueOrDefault | 40 | 9.649 ns | 0.0235 ns | 0.0220 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v1 | 40 | 10.266 ns | 0.0236 ns | 0.0221 ns | 1.06 | - | - | - | - | - | | | | | | | | | | - GetValueOrDefault | 200 | 11.613 ns | 0.0554 ns | 0.0491 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v1 | 200 | 12.052 ns | 0.0555 ns | 0.0520 ns | 1.04 | - | - | - | - | - - ## Using `!= Empty` instead of `.Height != 0` drops some perf - - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault_v1 | 5 | 5.933 ns | 0.0310 ns | 0.0290 ns | 0.93 | 0.03 | - | - | - | - | - GetValueOrDefault | 5 | 6.386 ns | 0.1807 ns | 0.1602 ns | 1.00 | 0.00 | - | - | - | - | - | | | | | | | | | | | - GetValueOrDefault | 40 | 9.820 ns | 0.0521 ns | 0.0488 ns | 1.00 | 0.00 | - | - | - | - | - GetValueOrDefault_v1 | 40 | 10.257 ns | 0.0300 ns | 0.0266 ns | 1.05 | 0.01 | - | - | - | - | - | | | | | | | | | | | - GetValueOrDefault | 200 | 11.717 ns | 0.0689 ns | 0.0644 ns | 1.00 | 0.00 | - | - | - | - | - GetValueOrDefault_v1 | 200 | 12.104 ns | 0.0548 ns | 0.0486 ns | 1.03 | 0.01 | - | - | - | - | - - ## Removing `.Height != 0` check completely did not change much, but let it stay cause less code is better - - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault | 5 | 5.903 ns | 0.0612 ns | 0.0573 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v1 | 5 | 5.931 ns | 0.0503 ns | 0.0470 ns | 1.00 | - | - | - | - | - | | | | | | | | | | - GetValueOrDefault | 40 | 9.636 ns | 0.0419 ns | 0.0392 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v1 | 40 | 10.231 ns | 0.0333 ns | 0.0312 ns | 1.06 | - | - | - | - | - | | | | | | | | | | - GetValueOrDefault | 200 | 11.637 ns | 0.0721 ns | 0.0602 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v1 | 200 | 12.042 ns | 0.0607 ns | 0.0568 ns | 1.03 | - | - | - | - | - - ## TryFind - - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - ----------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - TryFind_v1 | 5 | 5.229 ns | 0.0307 ns | 0.0257 ns | 1.00 | - | - | - | - | - TryFind | 5 | 6.766 ns | 0.0695 ns | 0.0650 ns | 1.30 | - | - | - | - | - | | | | | | | | | | - TryFind_v1 | 40 | 9.268 ns | 0.0116 ns | 0.0108 ns | 1.00 | - | - | - | - | - TryFind | 40 | 9.755 ns | 0.0219 ns | 0.0205 ns | 1.05 | - | - | - | - | - | | | | | | | | | | - TryFind_v1 | 200 | 11.773 ns | 0.0558 ns | 0.0494 ns | 1.00 | - | - | - | - | - TryFind | 200 | 12.212 ns | 0.0456 ns | 0.0380 ns | 1.04 | - | - | - | - | - - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - ----------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - TryFind | 5 | 5.906 ns | 0.0191 ns | 0.0178 ns | 0.97 | - | - | - | - | - TryFind_v1 | 5 | 6.079 ns | 0.0947 ns | 0.0839 ns | 1.00 | - | - | - | - | - | | | | | | | | | | - TryFind | 40 | 9.211 ns | 0.0214 ns | 0.0200 ns | 0.87 | - | - | - | - | - TryFind_v1 | 40 | 10.566 ns | 0.0149 ns | 0.0132 ns | 1.00 | - | - | - | - | - | | | | | | | | | | - TryFind | 200 | 11.400 ns | 0.1152 ns | 0.1078 ns | 0.88 | - | - | - | - | - TryFind_v1 | 200 | 12.929 ns | 0.0712 ns | 0.0666 ns | 1.00 | - | - | - | - | - - ## GetOrDefault a bit optimized - - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault | 5 | 6.782 ns | 0.0449 ns | 0.0398 ns | 0.96 | - | - | - | - | - GetValueOrDefault_v1 | 5 | 7.042 ns | 0.0659 ns | 0.0616 ns | 1.00 | - | - | - | - | - | | | | | | | | | | - GetValueOrDefault | 40 | 10.962 ns | 0.0866 ns | 0.0768 ns | 0.99 | - | - | - | - | - GetValueOrDefault_v1 | 40 | 11.094 ns | 0.0973 ns | 0.0813 ns | 1.00 | - | - | - | - | - | | | | | | | | | | - GetValueOrDefault | 200 | 13.329 ns | 0.0338 ns | 0.0299 ns | 0.97 | - | - | - | - | - GetValueOrDefault_v1 | 200 | 13.722 ns | 0.0537 ns | 0.0448 ns | 1.00 | - | - | - | - | - - ## The whole result for the docs - - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - ---------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - TryFind_v1 | 10 | 7.274 ns | 0.0410 ns | 0.0384 ns | 0.98 | 0.01 | - | - | - | - | - TryFind | 10 | 7.422 ns | 0.0237 ns | 0.0222 ns | 1.00 | 0.00 | - | - | - | - | - ConcurrentDict_TryGet | 10 | 21.664 ns | 0.0213 ns | 0.0189 ns | 2.92 | 0.01 | - | - | - | - | - ImmutableDict_TryGet | 10 | 71.199 ns | 0.1312 ns | 0.1228 ns | 9.59 | 0.03 | - | - | - | - | - | | | | | | | | | | | - TryFind_v1 | 100 | 8.426 ns | 0.0236 ns | 0.0221 ns | 0.91 | 0.00 | - | - | - | - | - TryFind | 100 | 9.304 ns | 0.0305 ns | 0.0270 ns | 1.00 | 0.00 | - | - | - | - | - ConcurrentDict_TryGet | 100 | 21.791 ns | 0.1072 ns | 0.0951 ns | 2.34 | 0.01 | - | - | - | - | - ImmutableDict_TryGet | 100 | 74.985 ns | 0.1053 ns | 0.0879 ns | 8.06 | 0.03 | - | - | - | - | - | | | | | | | | | | | - TryFind | 1000 | 13.837 ns | 0.0291 ns | 0.0272 ns | 1.00 | 0.00 | - | - | - | - | - TryFind_v1 | 1000 | 16.108 ns | 0.0415 ns | 0.0367 ns | 1.16 | 0.00 | - | - | - | - | - ConcurrentDict_TryGet | 1000 | 21.876 ns | 0.0325 ns | 0.0288 ns | 1.58 | 0.00 | - | - | - | - | - ImmutableDict_TryGet | 1000 | 83.563 ns | 0.1046 ns | 0.0873 ns | 6.04 | 0.01 | - | - | - | - | - - - ## 2019-03-28: Comparing vs `Dictionary`: - - Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - TryFind | 10 | 7.722 ns | 0.0451 ns | 0.0422 ns | 7.718 ns | 1.00 | 0.00 | - | - | - | - | - Dictionary_TryGetValue | 10 | 18.475 ns | 0.0502 ns | 0.0470 ns | 18.470 ns | 2.39 | 0.01 | - | - | - | - | - ConcurrentDictionary_TryGetValue | 10 | 22.661 ns | 0.0463 ns | 0.0433 ns | 22.653 ns | 2.93 | 0.02 | - | - | - | - | - ImmutableDict_TryGet | 10 | 72.911 ns | 1.5234 ns | 2.1355 ns | 74.134 ns | 9.25 | 0.23 | - | - | - | - | - | | | | | | | | | | | | - TryFind | 100 | 9.987 ns | 0.0543 ns | 0.0508 ns | 9.978 ns | 1.00 | 0.00 | - | - | - | - | - Dictionary_TryGetValue | 100 | 18.110 ns | 0.0644 ns | 0.0602 ns | 18.088 ns | 1.81 | 0.01 | - | - | - | - | - ConcurrentDictionary_TryGetValue | 100 | 24.402 ns | 0.0978 ns | 0.0915 ns | 24.435 ns | 2.44 | 0.02 | - | - | - | - | - ImmutableDict_TryGet | 100 | 76.689 ns | 0.3632 ns | 0.3397 ns | 76.704 ns | 7.68 | 0.04 | - | - | - | - | - | | | | | | | | | | | | - TryFind | 1000 | 12.600 ns | 0.1506 ns | 0.1335 ns | 12.551 ns | 1.00 | 0.00 | - | - | - | - | - Dictionary_TryGetValue | 1000 | 19.023 ns | 0.0575 ns | 0.0538 ns | 19.036 ns | 1.51 | 0.02 | - | - | - | - | - ConcurrentDictionary_TryGetValue | 1000 | 22.651 ns | 0.1238 ns | 0.1097 ns | 22.613 ns | 1.80 | 0.02 | - | - | - | - | - ImmutableDict_TryGet | 1000 | 83.608 ns | 0.3105 ns | 0.2904 ns | 83.612 ns | 6.64 | 0.07 | - | - | - | - | - - ## 2019-03-29: Comparing vs `DictionarySlim`: - - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - DictionarySlim_TryGetValue | 10 | 8.228 ns | 0.0682 ns | 0.0604 ns | 1.00 | 0.01 | - | - | - | - | - TryFind | 10 | 8.257 ns | 0.0796 ns | 0.0706 ns | 1.00 | 0.00 | - | - | - | - | - Dictionary_TryGetValue | 10 | 19.615 ns | 0.0251 ns | 0.0209 ns | 2.38 | 0.02 | - | - | - | - | - ConcurrentDictionary_TryGetValue | 10 | 22.339 ns | 0.0922 ns | 0.0863 ns | 2.71 | 0.03 | - | - | - | - | - ImmutableDict_TryGet | 10 | 69.872 ns | 0.2699 ns | 0.2524 ns | 8.46 | 0.07 | - | - | - | - | - | | | | | | | | | | | - DictionarySlim_TryGetValue | 100 | 8.351 ns | 0.1613 ns | 0.1508 ns | 0.69 | 0.01 | - | - | - | - | - TryFind | 100 | 12.144 ns | 0.0570 ns | 0.0533 ns | 1.00 | 0.00 | - | - | - | - | - Dictionary_TryGetValue | 100 | 17.985 ns | 0.0880 ns | 0.0823 ns | 1.48 | 0.01 | - | - | - | - | - ConcurrentDictionary_TryGetValue | 100 | 22.312 ns | 0.0564 ns | 0.0471 ns | 1.84 | 0.01 | - | - | - | - | - ImmutableDict_TryGet | 100 | 75.374 ns | 0.3042 ns | 0.2846 ns | 6.21 | 0.04 | - | - | - | - | - | | | | | | | | | | | - DictionarySlim_TryGetValue | 1000 | 8.202 ns | 0.0713 ns | 0.0667 ns | 0.55 | 0.01 | - | - | - | - | - TryFind | 1000 | 14.919 ns | 0.1101 ns | 0.0919 ns | 1.00 | 0.00 | - | - | - | - | - Dictionary_TryGetValue | 1000 | 18.073 ns | 0.2415 ns | 0.2141 ns | 1.21 | 0.02 | - | - | - | - | - ConcurrentDictionary_TryGetValue | 1000 | 22.406 ns | 0.1039 ns | 0.0921 ns | 1.50 | 0.01 | - | - | - | - | - ImmutableDict_TryGet | 1000 | 84.215 ns | 0.2835 ns | 0.2513 ns | 5.65 | 0.04 | - | - | - | - | - - ## 2019-04-08: Full test - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - | ImHashMap_TryFind | 10 | 9.072 ns | 0.0301 ns | 0.0282 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 10 | 8.405 ns | 0.0124 ns | 0.0116 ns | 0.93 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 10 | 8.199 ns | 0.0118 ns | 0.0105 ns | 0.90 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 18.151 ns | 0.0724 ns | 0.0677 ns | 2.00 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 22.281 ns | 0.1872 ns | 0.1462 ns | 2.45 | 0.02 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 70.143 ns | 0.2833 ns | 0.2650 ns | 7.73 | 0.04 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 100 | 12.698 ns | 0.0545 ns | 0.0510 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 100 | 12.440 ns | 0.0145 ns | 0.0129 ns | 0.98 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 100 | 8.197 ns | 0.0157 ns | 0.0139 ns | 0.65 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 100 | 18.108 ns | 0.0263 ns | 0.0205 ns | 1.43 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 22.834 ns | 0.0627 ns | 0.0524 ns | 1.80 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 76.253 ns | 0.2767 ns | 0.2311 ns | 6.00 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 1000 | 14.960 ns | 0.0457 ns | 0.0427 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 1000 | 14.614 ns | 0.0508 ns | 0.0451 ns | 0.98 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 1000 | 8.209 ns | 0.0534 ns | 0.0499 ns | 0.55 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 1000 | 18.256 ns | 0.0383 ns | 0.0320 ns | 1.22 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 22.261 ns | 0.1509 ns | 0.1411 ns | 1.49 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 83.095 ns | 0.3395 ns | 0.3176 ns | 5.55 | 0.03 | - | - | - | - | - - ## 2019-04-08: Different variants tested - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - | ImHashMap_TryFind2 | 10 | 7.979 ns | 0.0446 ns | 0.0417 ns | 0.93 | 0.01 | - | - | - | - | - | ImHashMap_TryFind3 | 10 | 7.908 ns | 0.0295 ns | 0.0262 ns | 0.92 | 0.00 | - | - | - | - | - | ImHashMap_TryFind | 10 | 8.608 ns | 0.0186 ns | 0.0174 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 10 | 8.248 ns | 0.0262 ns | 0.0232 ns | 0.96 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 10 | 7.212 ns | 0.0194 ns | 0.0181 ns | 0.84 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 17.707 ns | 0.0631 ns | 0.0590 ns | 2.06 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 22.210 ns | 0.1074 ns | 0.1004 ns | 2.58 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 72.093 ns | 0.4162 ns | 0.3893 ns | 8.37 | 0.05 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind2 | 100 | 11.245 ns | 0.0174 ns | 0.0163 ns | 0.92 | 0.00 | - | - | - | - | - | ImHashMap_TryFind3 | 100 | 11.488 ns | 0.0801 ns | 0.0710 ns | 0.94 | 0.01 | - | - | - | - | - | ImHashMap_TryFind | 100 | 12.165 ns | 0.0141 ns | 0.0118 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 100 | 12.272 ns | 0.0367 ns | 0.0343 ns | 1.01 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 100 | 7.019 ns | 0.0516 ns | 0.0458 ns | 0.58 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 100 | 17.825 ns | 0.1278 ns | 0.1196 ns | 1.47 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 22.189 ns | 0.1034 ns | 0.0968 ns | 1.82 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 75.564 ns | 0.3778 ns | 0.3534 ns | 6.21 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind2 | 1000 | 15.909 ns | 0.0924 ns | 0.0864 ns | 1.07 | 0.01 | - | - | - | - | - | ImHashMap_TryFind3 | 1000 | 13.643 ns | 0.0715 ns | 0.0669 ns | 0.92 | 0.01 | - | - | - | - | - | ImHashMap_TryFind | 1000 | 14.819 ns | 0.0465 ns | 0.0412 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 1000 | 14.541 ns | 0.0555 ns | 0.0520 ns | 0.98 | 0.01 | - | - | - | - | - | DictionarySlim_TryGetValue | 1000 | 7.678 ns | 0.0341 ns | 0.0302 ns | 0.52 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 1000 | 17.664 ns | 0.0403 ns | 0.0377 ns | 1.19 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 22.010 ns | 0.0497 ns | 0.0465 ns | 1.48 | 0.00 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 83.661 ns | 0.4033 ns | 0.3772 ns | 5.65 | 0.03 | - | - | - | - | - - ## Selecting the 3rd variant: - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - | ImHashMap_TryFind | 10 | 8.078 ns | 0.0405 ns | 0.0379 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 10 | 8.224 ns | 0.0114 ns | 0.0101 ns | 1.02 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 10 | 7.387 ns | 0.0406 ns | 0.0380 ns | 0.91 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 17.917 ns | 0.0429 ns | 0.0401 ns | 2.22 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 22.256 ns | 0.0726 ns | 0.0643 ns | 2.75 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 70.638 ns | 0.6266 ns | 0.5861 ns | 8.74 | 0.09 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 100 | 11.577 ns | 0.0168 ns | 0.0141 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 100 | 12.329 ns | 0.0295 ns | 0.0276 ns | 1.07 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 100 | 7.410 ns | 0.0398 ns | 0.0353 ns | 0.64 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 100 | 17.890 ns | 0.0425 ns | 0.0377 ns | 1.55 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 22.240 ns | 0.0654 ns | 0.0580 ns | 1.92 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 78.697 ns | 1.1242 ns | 1.0516 ns | 6.78 | 0.08 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 1000 | 13.731 ns | 0.0292 ns | 0.0258 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 1000 | 14.553 ns | 0.0370 ns | 0.0346 ns | 1.06 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 1000 | 7.345 ns | 0.0208 ns | 0.0194 ns | 0.53 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 1000 | 18.672 ns | 0.0483 ns | 0.0451 ns | 1.36 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 22.150 ns | 0.1141 ns | 0.1068 ns | 1.61 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 82.402 ns | 0.9798 ns | 0.9165 ns | 6.01 | 0.07 | - | - | - | - | - - ## V2: - - BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 - Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=3.1.100 - [Host] : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT - DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT - - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | ImHashMap_TryFind | 1 | 4.755 ns | 0.1690 ns | 0.1878 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 1 | 3.657 ns | 0.0458 ns | 0.0406 ns | 0.78 | 0.04 | - | - | - | - | - | ImHashMapSlots_TryFind | 1 | 2.518 ns | 0.0149 ns | 0.0132 ns | 0.53 | 0.02 | - | - | - | - | - | DictionarySlim_TryGetValue | 1 | 6.804 ns | 0.0272 ns | 0.0254 ns | 1.44 | 0.07 | - | - | - | - | - | Dictionary_TryGetValue | 1 | 16.495 ns | 0.3948 ns | 0.4993 ns | 3.49 | 0.16 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1 | 15.812 ns | 0.1016 ns | 0.0951 ns | 3.35 | 0.15 | - | - | - | - | - | ImmutableDict_TryGet | 1 | 24.346 ns | 0.1253 ns | 0.1172 ns | 5.16 | 0.23 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 10 | 6.080 ns | 0.0235 ns | 0.0208 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 10 | 6.091 ns | 0.0707 ns | 0.0590 ns | 1.00 | 0.01 | - | - | - | - | - | ImHashMapSlots_TryFind | 10 | 2.517 ns | 0.0206 ns | 0.0193 ns | 0.41 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 10 | 6.670 ns | 0.0278 ns | 0.0260 ns | 1.10 | 0.01 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 16.202 ns | 0.0634 ns | 0.0562 ns | 2.66 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 15.764 ns | 0.0659 ns | 0.0617 ns | 2.59 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 26.282 ns | 0.2232 ns | 0.2088 ns | 4.32 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 100 | 9.350 ns | 0.0315 ns | 0.0295 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 100 | 10.752 ns | 0.0199 ns | 0.0166 ns | 1.15 | 0.00 | - | - | - | - | - | ImHashMapSlots_TryFind | 100 | 5.664 ns | 0.0391 ns | 0.0366 ns | 0.61 | 0.01 | - | - | - | - | - | DictionarySlim_TryGetValue | 100 | 6.665 ns | 0.0287 ns | 0.0254 ns | 0.71 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 100 | 17.007 ns | 0.0615 ns | 0.0576 ns | 1.82 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 16.024 ns | 0.3340 ns | 0.3124 ns | 1.71 | 0.03 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 30.667 ns | 0.1026 ns | 0.0960 ns | 3.28 | 0.02 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 1000 | 12.729 ns | 0.0393 ns | 0.0368 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 1000 | 13.352 ns | 0.0638 ns | 0.0597 ns | 1.05 | 0.00 | - | - | - | - | - | ImHashMapSlots_TryFind | 1000 | 7.131 ns | 0.0246 ns | 0.0230 ns | 0.56 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 1000 | 6.686 ns | 0.0317 ns | 0.0297 ns | 0.53 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 1000 | 16.848 ns | 0.0593 ns | 0.0526 ns | 1.32 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 15.684 ns | 0.0695 ns | 0.0650 ns | 1.23 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 33.579 ns | 0.1077 ns | 0.0955 ns | 2.64 | 0.01 | - | - | - | - | - - - BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 - Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=3.1.100 - [Host] : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT - DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT - - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |-------------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | ImHashMap_TryFind | 1 | 4.365 ns | 0.0198 ns | 0.0185 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMapSlots32_TryFind | 1 | 2.400 ns | 0.0229 ns | 0.0214 ns | 0.55 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 1 | 3.385 ns | 0.0071 ns | 0.0063 ns | 0.78 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1 | 4.892 ns | 0.0134 ns | 0.0118 ns | 1.12 | 0.01 | - | - | - | - | - | Experimental_ImHashMapSlots32_TryFind | 1 | 6.268 ns | 0.0455 ns | 0.0425 ns | 1.44 | 0.01 | - | - | - | - | - | Experimental_ImHashMapSlots64_TryFind | 1 | 6.175 ns | 0.0335 ns | 0.0313 ns | 1.41 | 0.01 | - | - | - | - | - | DictionarySlim_TryGetValue | 1 | 6.190 ns | 0.0524 ns | 0.0490 ns | 1.42 | 0.02 | - | - | - | - | - | Dictionary_TryGetValue | 1 | 15.425 ns | 0.0636 ns | 0.0595 ns | 3.53 | 0.02 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1 | 14.754 ns | 0.0807 ns | 0.0716 ns | 3.38 | 0.02 | - | - | - | - | - | ImmutableDict_TryGet | 1 | 23.449 ns | 0.1027 ns | 0.0960 ns | 5.37 | 0.04 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 10 | 5.449 ns | 0.0216 ns | 0.0202 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMapSlots32_TryFind | 10 | 2.911 ns | 0.0089 ns | 0.0083 ns | 0.53 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 10 | 6.019 ns | 0.0129 ns | 0.0115 ns | 1.10 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 10 | 8.060 ns | 0.0239 ns | 0.0224 ns | 1.48 | 0.01 | - | - | - | - | - | Experimental_ImHashMapSlots32_TryFind | 10 | 5.605 ns | 0.0261 ns | 0.0244 ns | 1.03 | 0.00 | - | - | - | - | - | Experimental_ImHashMapSlots64_TryFind | 10 | 5.504 ns | 0.0456 ns | 0.0404 ns | 1.01 | 0.01 | - | - | - | - | - | DictionarySlim_TryGetValue | 10 | 5.216 ns | 0.0252 ns | 0.0236 ns | 0.96 | 0.01 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 15.346 ns | 0.0732 ns | 0.0685 ns | 2.82 | 0.02 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 14.870 ns | 0.1094 ns | 0.1023 ns | 2.73 | 0.02 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 24.888 ns | 0.0798 ns | 0.0746 ns | 4.57 | 0.02 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 100 | 7.665 ns | 0.0208 ns | 0.0184 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMapSlots32_TryFind | 100 | 5.240 ns | 0.0238 ns | 0.0223 ns | 0.68 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 100 | 9.222 ns | 0.0223 ns | 0.0208 ns | 1.20 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 100 | 12.248 ns | 0.0584 ns | 0.0546 ns | 1.60 | 0.01 | - | - | - | - | - | Experimental_ImHashMapSlots32_TryFind | 100 | 7.546 ns | 0.0966 ns | 0.0903 ns | 0.98 | 0.01 | - | - | - | - | - | Experimental_ImHashMapSlots64_TryFind | 100 | 5.780 ns | 0.0250 ns | 0.0234 ns | 0.75 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 100 | 6.452 ns | 0.0538 ns | 0.0503 ns | 0.84 | 0.01 | - | - | - | - | - | Dictionary_TryGetValue | 100 | 15.705 ns | 0.0586 ns | 0.0549 ns | 2.05 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 15.321 ns | 0.0413 ns | 0.0386 ns | 2.00 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 27.937 ns | 0.1050 ns | 0.0982 ns | 3.64 | 0.02 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 1000 | 11.459 ns | 0.0306 ns | 0.0286 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMapSlots32_TryFind | 1000 | 8.633 ns | 0.0154 ns | 0.0144 ns | 0.75 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 1000 | 12.486 ns | 0.0527 ns | 0.0493 ns | 1.09 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1000 | 16.002 ns | 0.0378 ns | 0.0353 ns | 1.40 | 0.00 | - | - | - | - | - | Experimental_ImHashMapSlots32_TryFind | 1000 | 11.673 ns | 0.0638 ns | 0.0597 ns | 1.02 | 0.00 | - | - | - | - | - | Experimental_ImHashMapSlots64_TryFind | 1000 | 9.817 ns | 0.0123 ns | 0.0115 ns | 0.86 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 1000 | 6.469 ns | 0.0428 ns | 0.0401 ns | 0.56 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 1000 | 19.170 ns | 0.0708 ns | 0.0628 ns | 1.67 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 16.273 ns | 0.4046 ns | 0.5116 ns | 1.43 | 0.05 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 29.662 ns | 0.1529 ns | 0.1355 ns | 2.59 | 0.01 | - | - | - | - | - - ## V3 - baseline - - BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.572 (2004/?/20H1) - Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores - .NET Core SDK=3.1.403 - [Host] : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT - DefaultJob : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT - - ## v3 - baseline - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |------------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | ImHashMap_TryFind | 1 | 4.614 ns | 0.1073 ns | 0.0951 ns | 4.589 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1 | 7.398 ns | 0.0926 ns | 0.0821 ns | 7.413 ns | 1.60 | 0.04 | - | - | - | - | - | ImHashMap234_TryFind | 1 | 4.912 ns | 0.0595 ns | 0.0528 ns | 4.903 ns | 1.07 | 0.03 | - | - | - | - | - | | | | | | | | | | | | | - | ImHashMap_TryFind | 10 | 8.205 ns | 0.0765 ns | 0.0639 ns | 8.186 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 10 | 10.689 ns | 0.2738 ns | 0.2561 ns | 10.684 ns | 1.31 | 0.03 | - | - | - | - | - | ImHashMap234_TryFind | 10 | 6.418 ns | 0.0731 ns | 0.0611 ns | 6.412 ns | 0.78 | 0.01 | - | - | - | - | - | | | | | | | | | | | | | - | ImHashMap_TryFind | 100 | 10.398 ns | 0.0434 ns | 0.0385 ns | 10.391 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 100 | 13.982 ns | 0.2432 ns | 0.2275 ns | 13.946 ns | 1.35 | 0.02 | - | - | - | - | - | ImHashMap234_TryFind | 100 | 10.759 ns | 0.1757 ns | 0.1467 ns | 10.761 ns | 1.03 | 0.01 | - | - | - | - | - | | | | | | | | | | | | | - | ImHashMap_TryFind | 1000 | 14.397 ns | 0.2039 ns | 0.1907 ns | 14.369 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1000 | 30.810 ns | 1.0842 ns | 2.9681 ns | 29.878 ns | 2.25 | 0.38 | - | - | - | - | - | ImHashMap234_TryFind | 1000 | 19.953 ns | 0.4996 ns | 0.4429 ns | 19.772 ns | 1.39 | 0.03 | - | - | - | - | - - ### Leaf3Plus1 - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | ImHashMap_TryFind | 1 | 5.611 ns | 0.1067 ns | 0.0998 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1 | 6.348 ns | 0.2010 ns | 0.2150 ns | 1.14 | 0.05 | - | - | - | - | - | ImHashMap234_TryFind | 1 | 4.572 ns | 0.0966 ns | 0.0904 ns | 0.82 | 0.02 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 10 | 7.582 ns | 0.0703 ns | 0.0587 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 10 | 10.133 ns | 0.1315 ns | 0.1098 ns | 1.34 | 0.02 | - | - | - | - | - | ImHashMap234_TryFind | 10 | 5.763 ns | 0.0670 ns | 0.0594 ns | 0.76 | 0.01 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 100 | 10.642 ns | 0.2276 ns | 0.1901 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 100 | 13.526 ns | 0.1563 ns | 0.1385 ns | 1.27 | 0.03 | - | - | - | - | - | ImHashMap234_TryFind | 100 | 9.271 ns | 0.1695 ns | 0.1503 ns | 0.87 | 0.02 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 1000 | 14.909 ns | 0.2118 ns | 0.1981 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1000 | 37.315 ns | 1.4352 ns | 4.2316 ns | 2.76 | 0.21 | - | - | - | - | - | ImHashMap234_TryFind | 1000 | 24.536 ns | 0.6190 ns | 0.7602 ns | 1.66 | 0.06 | - | - | - | - | - - ### Leaf5Plus1 + Leaf3Plus1 - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | ImHashMap_TryFind | 1 | 6.659 ns | 0.1975 ns | 0.1751 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1 | 8.220 ns | 0.1876 ns | 0.1842 ns | 1.23 | 0.05 | - | - | - | - | - | ImHashMap234_TryFind | 1 | 5.644 ns | 0.1702 ns | 0.1592 ns | 0.85 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 5 | 7.223 ns | 0.2332 ns | 0.2068 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 5 | 9.832 ns | 0.2499 ns | 0.2337 ns | 1.36 | 0.05 | - | - | - | - | - | ImHashMap234_TryFind | 5 | 5.353 ns | 0.1591 ns | 0.1410 ns | 0.74 | 0.04 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 10 | 8.325 ns | 0.1680 ns | 0.1403 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 10 | 12.221 ns | 0.3406 ns | 0.5098 ns | 1.50 | 0.08 | - | - | - | - | - | ImHashMap234_TryFind | 10 | 7.012 ns | 0.2045 ns | 0.2273 ns | 0.85 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 100 | 12.118 ns | 0.3031 ns | 0.4629 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 100 | 18.145 ns | 0.4551 ns | 0.8321 ns | 1.50 | 0.08 | - | - | - | - | - | ImHashMap234_TryFind | 100 | 11.908 ns | 0.2829 ns | 0.3679 ns | 0.97 | 0.04 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 1000 | 17.113 ns | 0.3848 ns | 0.3411 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1000 | 26.034 ns | 0.6183 ns | 0.8255 ns | 1.52 | 0.07 | - | - | - | - | - | ImHashMap234_TryFind | 1000 | 16.927 ns | 0.3743 ns | 0.5828 ns | 0.99 | 0.05 | - | - | - | - | - - ### V3 candidate - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |----------------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | V2_ImHashMap_AVL_TryFind | 1 | 6.084 ns | 0.1295 ns | 0.1082 ns | 1.00 | 0.00 | - | - | - | - | - | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 1 | 7.319 ns | 0.2009 ns | 0.1879 ns | 1.20 | 0.03 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 1 | 5.372 ns | 0.1142 ns | 0.0954 ns | 0.88 | 0.02 | - | - | - | - | - | V3_PartitionedHashMap_23Tree_TryFind | 1 | 6.578 ns | 0.1517 ns | 0.1345 ns | 1.08 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 5 | 6.867 ns | 0.1035 ns | 0.0918 ns | 1.00 | 0.00 | - | - | - | - | - | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 5 | 9.599 ns | 0.1509 ns | 0.1411 ns | 1.40 | 0.03 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 5 | 5.319 ns | 0.1028 ns | 0.1142 ns | 0.77 | 0.02 | - | - | - | - | - | V3_PartitionedHashMap_23Tree_TryFind | 5 | 6.550 ns | 0.0856 ns | 0.0801 ns | 0.95 | 0.01 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 10 | 7.646 ns | 0.0905 ns | 0.0802 ns | 1.00 | 0.00 | - | - | - | - | - | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 10 | 11.002 ns | 0.2011 ns | 0.1881 ns | 1.44 | 0.03 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 10 | 6.986 ns | 0.2160 ns | 0.2122 ns | 0.91 | 0.03 | - | - | - | - | - | V3_PartitionedHashMap_23Tree_TryFind | 10 | 6.714 ns | 0.2153 ns | 0.1798 ns | 0.88 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 100 | 12.413 ns | 0.3100 ns | 0.2748 ns | 1.00 | 0.00 | - | - | - | - | - | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 100 | 15.134 ns | 0.2375 ns | 0.2917 ns | 1.23 | 0.04 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 100 | 11.147 ns | 0.2990 ns | 0.3443 ns | 0.90 | 0.03 | - | - | - | - | - | V3_PartitionedHashMap_23Tree_TryFind | 100 | 8.245 ns | 0.1473 ns | 0.1150 ns | 0.67 | 0.01 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 1000 | 16.528 ns | 0.1799 ns | 0.1502 ns | 1.00 | 0.00 | - | - | - | - | - | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 1000 | 22.341 ns | 0.2584 ns | 0.2291 ns | 1.35 | 0.02 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 1000 | 13.178 ns | 0.2153 ns | 0.2014 ns | 0.80 | 0.01 | - | - | - | - | - | V3_PartitionedHashMap_23Tree_TryFind | 1000 | 11.198 ns | 0.2784 ns | 0.3094 ns | 0.68 | 0.02 | - | - | - | - | - - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | V3_ImHashMap_23Tree_TryFind | 1 | 4.467 ns | 0.0977 ns | 0.0816 ns | 1.00 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1 | 19.103 ns | 0.2302 ns | 0.2040 ns | 4.28 | 0.08 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 5 | 4.588 ns | 0.1057 ns | 0.0937 ns | 1.00 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 5 | 19.000 ns | 0.1738 ns | 0.1625 ns | 4.15 | 0.09 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 10 | 5.948 ns | 0.1194 ns | 0.1116 ns | 1.00 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 19.598 ns | 0.4600 ns | 0.4303 ns | 3.30 | 0.11 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 100 | 9.838 ns | 0.1607 ns | 0.1342 ns | 1.00 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 20.336 ns | 0.3591 ns | 0.2998 ns | 2.07 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 1000 | 11.980 ns | 0.2353 ns | 0.2201 ns | 1.00 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 19.939 ns | 0.4273 ns | 0.8824 ns | 1.73 | 0.10 | - | - | - | - | - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |----------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | V3_ImHashMap_23Tree_TryFind | 1 | 4.647 ns | 0.0516 ns | 0.0431 ns | 1.00 | 0.00 | - | - | - | - | - | ImmutableDict_TryGet | 1 | 23.396 ns | 0.3703 ns | 0.3092 ns | 5.04 | 0.10 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 5 | 5.242 ns | 0.1822 ns | 0.3143 ns | 1.00 | 0.00 | - | - | - | - | - | ImmutableDict_TryGet | 5 | 23.739 ns | 0.1941 ns | 0.1816 ns | 4.42 | 0.32 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 10 | 5.749 ns | 0.1206 ns | 0.1128 ns | 1.00 | 0.00 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 24.769 ns | 0.3152 ns | 0.2949 ns | 4.31 | 0.11 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 100 | 8.252 ns | 0.1754 ns | 0.1465 ns | 1.00 | 0.00 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 27.288 ns | 0.6118 ns | 0.6009 ns | 3.31 | 0.10 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 1000 | 12.053 ns | 0.1955 ns | 0.1829 ns | 1.00 | 0.00 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 30.762 ns | 0.3980 ns | 0.3723 ns | 2.55 | 0.04 | - | - | - | - | - - ### Branch3 as two Branch2 -- looks ok - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |----------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | V2_ImHashMap_AVL_TryFind | 1 | 6.236 ns | 0.2150 ns | 0.3084 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 1 | 5.467 ns | 0.1410 ns | 0.1250 ns | 0.85 | 0.05 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 5 | 7.390 ns | 0.2037 ns | 0.1701 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 5 | 5.552 ns | 0.1412 ns | 0.1252 ns | 0.75 | 0.02 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 10 | 9.402 ns | 0.1381 ns | 0.1153 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 10 | 6.820 ns | 0.1766 ns | 0.1652 ns | 0.73 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 100 | 11.085 ns | 0.2337 ns | 0.2186 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 100 | 10.257 ns | 0.2037 ns | 0.1905 ns | 0.93 | 0.02 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 1000 | 16.883 ns | 0.2438 ns | 0.2280 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 1000 | 15.516 ns | 0.3248 ns | 0.4554 ns | 0.93 | 0.04 | - | - | - | - | - - ### V3 RTM - - BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 - Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=5.0.201 - [Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT - DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT - - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | V2_ImHashMap_AVL_TryFind | 1 | 5.100 ns | 0.1212 ns | 0.1134 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 1 | 5.539 ns | 0.1676 ns | 0.1568 ns | 1.09 | 0.03 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 1 | 5.813 ns | 0.1844 ns | 0.1973 ns | 1.13 | 0.04 | - | - | - | - | - | DictionarySlim_TryGetValue | 1 | 6.614 ns | 0.0839 ns | 0.0744 ns | 1.29 | 0.03 | - | - | - | - | - | Dictionary_TryGetValue | 1 | 16.495 ns | 0.1270 ns | 0.1126 ns | 3.23 | 0.06 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1 | 12.715 ns | 0.1584 ns | 0.1323 ns | 2.49 | 0.05 | - | - | - | - | - | ImmutableDict_TryGet | 1 | 22.615 ns | 0.3097 ns | 0.2418 ns | 4.42 | 0.08 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 10 | 6.270 ns | 0.1421 ns | 0.1187 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 10 | 6.597 ns | 0.1095 ns | 0.1024 ns | 1.06 | 0.02 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 10 | 5.416 ns | 0.0908 ns | 0.0805 ns | 0.87 | 0.02 | - | - | - | - | - | DictionarySlim_TryGetValue | 10 | 7.543 ns | 0.1274 ns | 0.1064 ns | 1.20 | 0.02 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 16.907 ns | 0.1892 ns | 0.1769 ns | 2.70 | 0.05 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 12.694 ns | 0.1740 ns | 0.1453 ns | 2.03 | 0.04 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 24.049 ns | 0.3034 ns | 0.2838 ns | 3.84 | 0.07 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 100 | 9.459 ns | 0.2667 ns | 0.2739 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 100 | 9.526 ns | 0.1814 ns | 0.1697 ns | 1.01 | 0.03 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 100 | 5.827 ns | 0.0867 ns | 0.0768 ns | 0.62 | 0.02 | - | - | - | - | - | DictionarySlim_TryGetValue | 100 | 6.897 ns | 0.1033 ns | 0.0967 ns | 0.73 | 0.02 | - | - | - | - | - | Dictionary_TryGetValue | 100 | 18.189 ns | 0.2100 ns | 0.1640 ns | 1.92 | 0.06 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 13.412 ns | 0.2041 ns | 0.1909 ns | 1.42 | 0.04 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 26.023 ns | 0.3854 ns | 0.3417 ns | 2.76 | 0.08 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 1000 | 15.172 ns | 0.2617 ns | 0.2320 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 1000 | 14.473 ns | 0.2030 ns | 0.1799 ns | 0.95 | 0.01 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 1000 | 8.182 ns | 0.0809 ns | 0.0756 ns | 0.54 | 0.01 | - | - | - | - | - | DictionarySlim_TryGetValue | 1000 | 6.893 ns | 0.1590 ns | 0.1410 ns | 0.45 | 0.01 | - | - | - | - | - | Dictionary_TryGetValue | 1000 | 18.230 ns | 0.2012 ns | 0.1882 ns | 1.20 | 0.02 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 14.920 ns | 0.2296 ns | 0.2035 ns | 0.98 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 29.555 ns | 0.5926 ns | 0.5543 ns | 1.95 | 0.04 | - | - | - | - | - - - ## Against static TypeDictionary by @rogeralsing (need to substract the cost of enumerating the array when adding items to ImHashMap and PartitionedHashMap) - - BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 - Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=5.0.201 - [Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT - DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |---------------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | V3_ImHashMap_GetValueOrDefault | 5 | 4.497 ns | 0.1607 ns | 0.1578 ns | 1.00 | 0.00 | - | - | - | - | - | V3_PartitionedHashMap_GetValueOrDefault | 5 | 4.763 ns | 0.1675 ns | 0.1484 ns | 1.06 | 0.05 | - | - | - | - | - | TypeDict_TryFind | 5 | 3.198 ns | 0.1221 ns | 0.1406 ns | 0.71 | 0.03 | - | - | - | - | - | Dictionary_TryGetValue | 5 | 16.248 ns | 0.3500 ns | 0.3102 ns | 3.61 | 0.12 | - | - | - | - | - - | V3_ImHashMap_GetValueOrDefault | 10 | 5.569 ns | 0.1801 ns | 0.2751 ns | 1.00 | 0.00 | - | - | - | - | - | V3_PartitionedHashMap_GetValueOrDefault | 10 | 5.016 ns | 0.1509 ns | 0.1178 ns | 0.89 | 0.05 | - | - | - | - | - | TypeDict_TryFind | 10 | 2.513 ns | 0.0824 ns | 0.0688 ns | 0.45 | 0.03 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 15.954 ns | 0.3234 ns | 0.3025 ns | 2.85 | 0.17 | - | - | - | - | - - | V3_ImHashMap_GetValueOrDefault | 20 | 6.747 ns | 0.1964 ns | 0.2017 ns | 1.00 | 0.00 | - | - | - | - | - | V3_PartitionedHashMap_GetValueOrDefault | 20 | 3.985 ns | 0.1034 ns | 0.0863 ns | 0.59 | 0.02 | - | - | - | - | - | TypeDict_TryFind | 20 | 2.476 ns | 0.1174 ns | 0.1257 ns | 0.37 | 0.02 | - | - | - | - | - | Dictionary_TryGetValue | 20 | 17.766 ns | 0.4131 ns | 0.3865 ns | 2.64 | 0.11 | - | - | - | - | - - - ## V4 - - BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 - Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=6.0.202 - [Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT - DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | V4_ImHashMap_TryFind | 1 | 8.915 ns | 0.1853 ns | 0.1733 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_TryFind | 1 | 7.834 ns | 0.1769 ns | 0.1568 ns | 0.88 | 0.02 | - | - | - | - | - | V4_PartitionedHashMap_TryFind | 1 | 8.292 ns | 0.1082 ns | 0.0959 ns | 0.93 | 0.02 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 1 | 7.681 ns | 0.1245 ns | 0.1039 ns | 0.86 | 0.02 | - | - | - | - | - | DictionarySlim_TryGetValue | 1 | 8.861 ns | 0.1423 ns | 0.1188 ns | 0.99 | 0.02 | - | - | - | - | - | Dictionary_TryGetValue | 1 | 17.914 ns | 0.3447 ns | 0.3055 ns | 2.01 | 0.05 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1 | 13.381 ns | 0.2709 ns | 0.2401 ns | 1.50 | 0.04 | - | - | - | - | - | ImmutableDict_TryGet | 1 | 19.040 ns | 0.3068 ns | 0.2870 ns | 2.14 | 0.06 | - | - | - | - | - | | | | | | | | | | | | - | V4_ImHashMap_TryFind | 10 | 9.462 ns | 0.2690 ns | 0.2246 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_TryFind | 10 | 9.038 ns | 0.1650 ns | 0.1463 ns | 0.96 | 0.02 | - | - | - | - | - | V4_PartitionedHashMap_TryFind | 10 | 8.575 ns | 0.2031 ns | 0.1900 ns | 0.91 | 0.03 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 10 | 5.869 ns | 0.1026 ns | 0.1743 ns | 0.63 | 0.02 | - | - | - | - | - | DictionarySlim_TryGetValue | 10 | 7.384 ns | 0.1882 ns | 0.1668 ns | 0.78 | 0.03 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 14.082 ns | 0.2168 ns | 0.1921 ns | 1.49 | 0.04 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 11.436 ns | 0.1398 ns | 0.1239 ns | 1.21 | 0.03 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 17.927 ns | 0.4467 ns | 0.4780 ns | 1.90 | 0.09 | - | - | - | - | - | | | | | | | | | | | | - | V4_ImHashMap_TryFind | 100 | 12.147 ns | 0.1475 ns | 0.1308 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_TryFind | 100 | 11.357 ns | 0.1695 ns | 0.1323 ns | 0.94 | 0.01 | - | - | - | - | - | V4_PartitionedHashMap_TryFind | 100 | 8.520 ns | 0.2142 ns | 0.1899 ns | 0.70 | 0.02 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 100 | 7.785 ns | 0.1374 ns | 0.1147 ns | 0.64 | 0.01 | - | - | - | - | - | DictionarySlim_TryGetValue | 100 | 6.918 ns | 0.1544 ns | 0.1289 ns | 0.57 | 0.01 | - | - | - | - | - | Dictionary_TryGetValue | 100 | 13.804 ns | 0.3474 ns | 0.3717 ns | 1.14 | 0.04 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 11.432 ns | 0.2194 ns | 0.2052 ns | 0.94 | 0.02 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 21.593 ns | 0.4048 ns | 0.3786 ns | 1.78 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | V4_ImHashMap_TryFind | 1000 | 15.492 ns | 0.3859 ns | 0.4881 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_TryFind | 1000 | 14.339 ns | 0.2612 ns | 0.2444 ns | 0.92 | 0.03 | - | - | - | - | - | V4_PartitionedHashMap_TryFind | 1000 | 11.508 ns | 0.1839 ns | 0.1630 ns | 0.74 | 0.03 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 1000 | 9.508 ns | 0.2340 ns | 0.2074 ns | 0.61 | 0.02 | - | - | - | - | - | DictionarySlim_TryGetValue | 1000 | 7.099 ns | 0.1984 ns | 0.2037 ns | 0.46 | 0.01 | - | - | - | - | - | Dictionary_TryGetValue | 1000 | 13.696 ns | 0.1341 ns | 0.1120 ns | 0.88 | 0.03 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 11.358 ns | 0.1464 ns | 0.1222 ns | 0.73 | 0.03 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 25.875 ns | 0.3752 ns | 0.3510 ns | 1.67 | 0.06 | - | - | - | - | - - ## Baseline FHashMap6 vs DictionarySlim vs Dictionary - - 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 - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------:|------------:| - | Dictionary_TryGetValue | 100 | 15.234 ns | 0.3434 ns | 0.6779 ns | 14.971 ns | 1.00 | 0.00 | - | NA | - | DictionarySlim_TryGetValue | 100 | 9.790 ns | 0.5518 ns | 1.6271 ns | 8.944 ns | 0.70 | 0.11 | - | NA | - | FHashMap_TryGetValue | 100 | 20.591 ns | 0.4773 ns | 0.4232 ns | 20.521 ns | 1.32 | 0.09 | - | NA | - - ## Never happened? - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|---------:|---------:|---------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 100 | 11.14 ns | 0.613 ns | 1.749 ns | 10.75 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 100 | 11.29 ns | 0.641 ns | 1.850 ns | 10.77 ns | 1.03 | 0.20 | - | NA | - - ## Almost funny - - | Method | Count | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|----------:|------------:| - | DictionarySlim_TryGetValue | 100 | 8.703 ns | 0.0774 ns | 0.0686 ns | 1.00 | - | NA | - | FHashMap_TryGetValue | 100 | 7.181 ns | 0.0442 ns | 0.0345 ns | 0.83 | - | NA | - - ## Initial SIMD FHashMap7 vs DictionarySlim - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 100 | 8.968 ns | 0.2581 ns | 0.4168 ns | 8.796 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 100 | 8.866 ns | 0.4456 ns | 1.3069 ns | 9.244 ns | 0.86 | 0.09 | - | NA | - - ## Round 2 and 3, SIMD FHashMap7 vs DictionarySlim - - | Method | Count | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|----------:|------------:| - | DictionarySlim_TryGetValue | 100 | 8.925 ns | 0.0383 ns | 0.0299 ns | 1.00 | - | NA | - | FHashMap_TryGetValue | 100 | 6.376 ns | 0.1207 ns | 0.1129 ns | 0.72 | - | NA | - - ## Funny 4, SIMD FHashMap7 vs DictionarySlim - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 100 | 9.131 ns | 0.1991 ns | 0.1663 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 100 | 6.020 ns | 0.1766 ns | 0.2644 ns | 0.65 | 0.04 | - | NA | - - ## FHashMap7 vs DictionarySlim (multiple params) - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 10.945 ns | 0.3136 ns | 0.5574 ns | 10.770 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1 | 6.852 ns | 0.4387 ns | 1.2157 ns | 7.022 ns | 0.57 | 0.11 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 12.432 ns | 0.7856 ns | 2.3042 ns | 12.383 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 10 | 5.161 ns | 0.1755 ns | 0.2932 ns | 5.050 ns | 0.38 | 0.05 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 16.811 ns | 0.7006 ns | 2.0657 ns | 15.940 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 100 | 9.176 ns | 0.6841 ns | 1.9517 ns | 8.838 ns | 0.56 | 0.14 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 11.542 ns | 0.3251 ns | 0.7535 ns | 11.400 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1000 | 9.579 ns | 0.4608 ns | 1.2921 ns | 9.556 ns | 0.82 | 0.12 | - | NA | - - ## No!!! SIMD FHashMap7 vs DictionarySlim (multiple params) - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 9.134 ns | 0.2546 ns | 0.3569 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1 | 5.174 ns | 0.0317 ns | 0.0281 ns | 0.56 | 0.02 | - | NA | - | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 8.908 ns | 0.0430 ns | 0.0359 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 10 | 5.615 ns | 0.0722 ns | 0.0675 ns | 0.63 | 0.01 | - | NA | - | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 8.964 ns | 0.1536 ns | 0.1282 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 100 | 7.468 ns | 0.2187 ns | 0.2148 ns | 0.83 | 0.03 | - | NA | - | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 8.826 ns | 0.0929 ns | 0.0776 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1000 | 5.271 ns | 0.1362 ns | 0.1514 ns | 0.60 | 0.02 | - | NA | - - ## Small opti No!!! SIMD FHashMap7 vs DictionarySlim (multiple params) - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 8.965 ns | 0.3526 ns | 1.0285 ns | 8.416 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1 | 3.940 ns | 0.1476 ns | 0.2117 ns | 3.894 ns | 0.43 | 0.05 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 9.154 ns | 0.2472 ns | 0.3036 ns | 9.041 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 10 | 5.734 ns | 0.0922 ns | 0.0770 ns | 5.733 ns | 0.62 | 0.02 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 8.638 ns | 0.1980 ns | 0.1654 ns | 8.599 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 100 | 7.259 ns | 0.1361 ns | 0.1206 ns | 7.212 ns | 0.84 | 0.02 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 9.280 ns | 0.2481 ns | 0.4537 ns | 9.115 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1000 | 3.765 ns | 0.0650 ns | 0.0576 ns | 3.760 ns | 0.40 | 0.02 | - | NA | - - ## Adding small SIMD to FHashMap7 vs DictionarySlim (multiple params) - winning the medium - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 9.314 ns | 0.2492 ns | 0.4494 ns | 9.149 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1 | 4.819 ns | 0.1521 ns | 0.1423 ns | 4.775 ns | 0.51 | 0.03 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 9.012 ns | 0.2230 ns | 0.4297 ns | 8.841 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 10 | 6.407 ns | 0.1963 ns | 0.2182 ns | 6.375 ns | 0.70 | 0.05 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 9.261 ns | 0.2620 ns | 0.6019 ns | 8.994 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 100 | 7.136 ns | 0.1941 ns | 0.1515 ns | 7.104 ns | 0.74 | 0.06 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 9.066 ns | 0.1819 ns | 0.1612 ns | 9.013 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1000 | 4.935 ns | 0.1353 ns | 0.1661 ns | 4.891 ns | 0.55 | 0.02 | - | NA | - - ## Lookup FHashMap8 vs DictionarySlim - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 9.446 ns | 0.2718 ns | 0.5038 ns | 9.329 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 1 | 5.485 ns | 0.1832 ns | 0.3009 ns | 5.402 ns | 0.58 | 0.04 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 9.659 ns | 0.2123 ns | 0.1882 ns | 9.654 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 10 | 7.044 ns | 0.1443 ns | 0.1205 ns | 7.003 ns | 0.73 | 0.01 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 9.368 ns | 0.2697 ns | 0.3600 ns | 9.259 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 100 | 9.770 ns | 0.2736 ns | 0.3360 ns | 9.748 ns | 1.04 | 0.06 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 9.743 ns | 0.2676 ns | 0.4686 ns | 9.573 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 1000 | 6.043 ns | 0.1946 ns | 0.4146 ns | 5.883 ns | 0.62 | 0.05 | - | NA | - - ## Lookup FHashMap8 vs DictionarySlim after simplifying - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 8.649 ns | 0.0707 ns | 0.0662 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 1 | 5.447 ns | 0.0784 ns | 0.0654 ns | 0.63 | 0.01 | - | NA | - | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 8.948 ns | 0.0405 ns | 0.0316 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 10 | 5.471 ns | 0.1656 ns | 0.2375 ns | 0.62 | 0.03 | - | NA | - | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 9.081 ns | 0.2568 ns | 0.5583 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 100 | 6.294 ns | 0.1585 ns | 0.1886 ns | 0.69 | 0.05 | - | NA | - | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 8.990 ns | 0.2087 ns | 0.1850 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 1000 | 4.950 ns | 0.1508 ns | 0.2719 ns | 0.55 | 0.03 | - | NA | - - ## Lookup FHashMap7 vs FHashMap8 vs DictionarySlim vs Dictionary - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 9.470 ns | 0.2655 ns | 0.7269 ns | 9.193 ns | 1.00 | 0.00 | - | NA | - | Dictionary_TryGetValue | 1 | 12.842 ns | 0.3375 ns | 0.7192 ns | 12.628 ns | 1.36 | 0.12 | - | NA | - | FHashMap8_TryGetValue | 1 | 5.212 ns | 0.1734 ns | 0.1448 ns | 5.202 ns | 0.52 | 0.05 | - | NA | - | FHashMap7_TryGetValue | 1 | 4.365 ns | 0.1630 ns | 0.2981 ns | 4.269 ns | 0.45 | 0.05 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 9.585 ns | 0.2755 ns | 0.7992 ns | 9.292 ns | 1.00 | 0.00 | - | NA | - | Dictionary_TryGetValue | 10 | 10.675 ns | 0.2063 ns | 0.1723 ns | 10.674 ns | 1.13 | 0.10 | - | NA | - | FHashMap8_TryGetValue | 10 | 5.584 ns | 0.1895 ns | 0.2028 ns | 5.546 ns | 0.57 | 0.07 | - | NA | - | FHashMap7_TryGetValue | 10 | 5.676 ns | 0.0982 ns | 0.0964 ns | 5.680 ns | 0.58 | 0.06 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 11.095 ns | 0.4470 ns | 1.2236 ns | 11.427 ns | 1.00 | 0.00 | - | NA | - | Dictionary_TryGetValue | 100 | 10.694 ns | 0.2975 ns | 0.7013 ns | 10.484 ns | 0.94 | 0.11 | - | NA | - | FHashMap8_TryGetValue | 100 | 7.313 ns | 0.2226 ns | 0.4071 ns | 7.107 ns | 0.65 | 0.07 | - | NA | - | FHashMap7_TryGetValue | 100 | 7.119 ns | 0.2135 ns | 0.1997 ns | 7.095 ns | 0.67 | 0.11 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 9.049 ns | 0.2662 ns | 0.5953 ns | 8.826 ns | 1.00 | 0.00 | - | NA | - | Dictionary_TryGetValue | 1000 | 13.465 ns | 0.3476 ns | 0.3251 ns | 13.483 ns | 1.50 | 0.12 | - | NA | - | FHashMap8_TryGetValue | 1000 | 4.926 ns | 0.1781 ns | 0.3120 ns | 4.809 ns | 0.54 | 0.05 | - | NA | - | FHashMap7_TryGetValue | 1000 | 4.314 ns | 0.1559 ns | 0.1601 ns | 4.290 ns | 0.48 | 0.04 | - | NA | - - ## FHM9.1 vs Dict and DictSlim - - BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1702/22H2/2022Update/SunValley2) - 11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores - .NET SDK=7.0.304 - [Host] : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | - |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------------------:|---------------:|------------------------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 6.236 ns | 0.1823 ns | 0.1872 ns | 6.221 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | Dictionary_TryGetValue | 1 | 10.070 ns | 0.2035 ns | 0.2090 ns | 10.038 ns | 1.62 | 0.05 | 31 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 1 | 5.084 ns | 0.2665 ns | 0.7773 ns | 5.267 ns | 0.67 | 0.08 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 8.515 ns | 0.2330 ns | 0.3763 ns | 8.407 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | Dictionary_TryGetValue | 10 | 10.296 ns | 0.4517 ns | 1.3247 ns | 10.439 ns | 1.30 | 0.10 | 25 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 10 | 3.857 ns | 0.1311 ns | 0.2904 ns | 3.777 ns | 0.46 | 0.04 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 7.105 ns | 0.1931 ns | 0.5221 ns | 6.860 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | Dictionary_TryGetValue | 100 | 8.443 ns | 0.2081 ns | 0.2397 ns | 8.402 ns | 1.14 | 0.11 | 25 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 100 | 4.037 ns | 0.1339 ns | 0.2201 ns | 3.994 ns | 0.56 | 0.06 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 6.325 ns | 0.1933 ns | 0.4632 ns | 6.124 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | Dictionary_TryGetValue | 1000 | 9.146 ns | 0.2185 ns | 0.2044 ns | 9.176 ns | 1.42 | 0.10 | 25 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 1000 | 5.917 ns | 0.1505 ns | 0.1334 ns | 5.909 ns | 0.92 | 0.07 | 13 | 0 | 0 | - | NA | - - ## FHM9.1 sparse entries - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | CacheMisses/Op | BranchInstructions/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|---------------:|----------------------:|------------------------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 7.813 ns | 0.2096 ns | 0.2243 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | - | FHashMap91_TryGetValue | 1 | 4.608 ns | 0.1493 ns | 0.1888 ns | 0.59 | 0.03 | 0 | 13 | -0 | - | NA | - | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 7.677 ns | 0.1647 ns | 0.1375 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | - | FHashMap91_TryGetValue | 10 | 4.488 ns | 0.1183 ns | 0.1049 ns | 0.59 | 0.02 | 0 | 13 | 0 | - | NA | - | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 7.824 ns | 0.2102 ns | 0.2947 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | - | FHashMap91_TryGetValue | 100 | 3.459 ns | 0.1538 ns | 0.3277 ns | 0.45 | 0.05 | -0 | 13 | -0 | - | NA | - | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 7.608 ns | 0.1997 ns | 0.1770 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | - | FHashMap91_TryGetValue | 1000 | 6.384 ns | 0.1427 ns | 0.1114 ns | 0.84 | 0.03 | 0 | 14 | 0 | - | NA | - - ## No more sparce anymore - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | - |------------------------------ |------ |---------:|----------:|----------:|---------:|------:|--------:|----------------------:|---------------:|------------------------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 6.759 ns | 0.2002 ns | 0.2603 ns | 6.704 ns | 1.00 | 0.00 | 20 | 0 | -0 | - | NA | - | FHashMap91_TryGetValue | 1 | 4.512 ns | 0.1337 ns | 0.2271 ns | 4.432 ns | 0.67 | 0.05 | 11 | 0 | -0 | - | NA | - | FHashMap91_TryGetValue_Golden | 1 | 4.187 ns | 0.1022 ns | 0.0906 ns | 4.167 ns | 0.62 | 0.03 | 11 | -0 | -0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 8.225 ns | 0.4388 ns | 1.2938 ns | 7.602 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 10 | 4.434 ns | 0.1415 ns | 0.1453 ns | 4.434 ns | 0.58 | 0.06 | 11 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue_Golden | 10 | 4.498 ns | 0.1236 ns | 0.1033 ns | 4.472 ns | 0.59 | 0.05 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 7.067 ns | 0.1470 ns | 0.1303 ns | 7.080 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 100 | 4.481 ns | 0.1484 ns | 0.2311 ns | 4.398 ns | 0.64 | 0.04 | 11 | 0 | -0 | - | NA | - | FHashMap91_TryGetValue_Golden | 100 | 8.215 ns | 0.2181 ns | 0.2240 ns | 8.178 ns | 1.17 | 0.04 | 17 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 8.744 ns | 0.6189 ns | 1.8151 ns | 7.838 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 1000 | 5.755 ns | 0.1660 ns | 0.1912 ns | 5.715 ns | 0.73 | 0.09 | 14 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue_Golden | 1000 | 7.732 ns | 0.1953 ns | 0.2325 ns | 7.705 ns | 0.97 | 0.13 | 17 | 0 | 0 | - | NA | - - ## Final load factor 87.5% and no golden results - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | BranchInstructions/Op | BranchMispredictions/Op | CacheMisses/Op | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------------------:|------------------------:|---------------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 6.830 ns | 0.1073 ns | 0.1004 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 1 | 4.230 ns | 0.1149 ns | 0.1075 ns | 0.62 | 0.02 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 6.980 ns | 0.0918 ns | 0.0717 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 10 | 4.259 ns | 0.0673 ns | 0.0562 ns | 0.61 | 0.01 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 6.975 ns | 0.1016 ns | 0.0849 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 100 | 4.414 ns | 0.1426 ns | 0.2342 ns | 0.64 | 0.03 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 6.889 ns | 0.0773 ns | 0.0685 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 1000 | 5.633 ns | 0.1079 ns | 0.1243 ns | 0.82 | 0.02 | 14 | 0 | 0 | - | NA | - - ## ArrayEntries - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | CacheMisses/Op | BranchInstructions/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|---------------:|----------------------:|------------------------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1000 | 7.223 ns | 0.2535 ns | 0.7193 ns | 6.887 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | - | FHashMap91_TryGetValue | 1000 | 5.108 ns | 0.1634 ns | 0.2819 ns | 4.996 ns | 0.69 | 0.09 | -0 | 14 | 0 | - | NA | - - ## ArrayArrayEntries - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | BranchMispredictions/Op | CacheMisses/Op | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------------------:|------------------------:|---------------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 7.053 ns | 0.1174 ns | 0.1041 ns | 7.021 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 1 | 4.948 ns | 0.1446 ns | 0.2119 ns | 4.845 ns | 0.70 | 0.04 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 7.475 ns | 0.1732 ns | 0.2747 ns | 7.384 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 10 | 4.900 ns | 0.1541 ns | 0.1366 ns | 4.862 ns | 0.65 | 0.03 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 8.120 ns | 0.4747 ns | 1.3995 ns | 7.386 ns | 1.00 | 0.00 | 20 | -0 | 0 | - | NA | - | FHashMap91_TryGetValue | 100 | 5.129 ns | 0.1593 ns | 0.4251 ns | 4.985 ns | 0.64 | 0.12 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 7.289 ns | 0.1912 ns | 0.3088 ns | 7.137 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 1000 | 6.262 ns | 0.1232 ns | 0.0962 ns | 6.277 ns | 0.86 | 0.04 | 14 | 0 | 0 | - | NA | - - ## SmallMap + net8.0 - - BenchmarkDotNet v0.13.10, Windows 11 (10.0.22621.2428/22H2/2022Update/SunValley2) - 11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores - .NET SDK 8.0.100-rc.2.23502.2 - [Host] : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2 - DefaultJob : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2 - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | BranchInstructions/Op | BranchMispredictions/Op | CacheMisses/Op | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------------------:|------------------------:|---------------:|----------:|------------:| - | Dictionary_TryGetValue | 100 | 6.938 ns | 0.1719 ns | 0.2574 ns | 1.00 | 0.00 | 21 | 0 | 0 | - | NA | - | DictionarySlim_TryGetValue | 100 | 5.371 ns | 0.1277 ns | 0.1132 ns | 0.77 | 0.03 | 15 | 0 | 0 | - | NA | - | SmallMap_TryGetValue | 100 | 3.718 ns | 0.1360 ns | 0.2453 ns | 0.54 | 0.04 | 9 | 0 | 0 | - | NA | - - - ## SmallMap + FEC_FHashMap + net6.0 - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------------------:|---------------:|------------------------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 5.592 ns | 0.4244 ns | 1.2448 ns | 4.835 ns | 1.00 | 0.00 | 15 | 0 | 0 | - | NA | - | SmallMap_TryGetValue | 1 | 2.958 ns | 0.1059 ns | 0.0990 ns | 2.970 ns | 0.44 | 0.03 | 9 | -0 | -0 | - | NA | - | FHashMap_TryGetValue | 1 | 1.297 ns | 0.0757 ns | 0.0777 ns | 1.288 ns | 0.19 | 0.01 | 6 | -0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 5.208 ns | 0.1485 ns | 0.1240 ns | 5.181 ns | 1.00 | 0.00 | 15 | 0 | 0 | - | NA | - | SmallMap_TryGetValue | 10 | 3.709 ns | 0.1739 ns | 0.4551 ns | 3.548 ns | 0.69 | 0.07 | 9 | 0 | 0 | - | NA | - | FHashMap_TryGetValue | 10 | 5.539 ns | 0.4004 ns | 1.1487 ns | 5.612 ns | 0.98 | 0.22 | 10 | -0 | -0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 5.296 ns | 0.1654 ns | 0.3898 ns | 5.176 ns | 1.00 | 0.00 | 15 | 0 | 0 | - | NA | - | SmallMap_TryGetValue | 100 | 3.549 ns | 0.1293 ns | 0.2490 ns | 3.486 ns | 0.66 | 0.07 | 9 | 0 | 0 | - | NA | - | FHashMap_TryGetValue | 100 | 4.265 ns | 0.1406 ns | 0.1098 ns | 4.268 ns | 0.80 | 0.08 | 10 | 0 | -0 | - | NA | - - */ - // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] - [Params(1, 10, 100)] - public int Count; - - [GlobalSetup] - public void Populate() - { - _mapV4 = V4_ImHashMap_AddOrUpdate(); - _mapV3 = V3_ImHashMap_AddOrUpdate(); - _mapV2 = V2_AddOrUpdate(); - _partMapV4 = V4_PartitionedHashMap_AddOrUpdate(); - _partMapV3 = V3_PartitionedHashMap_AddOrUpdate(); - _typeDict = TypeDictionary_Add(); - _dict = Dict(); - _dictSlim = DictSlim(); - _fHashMap7 = FillFHashMap7(); - _fHashMap9 = FillFHashMap9(); - _fHashMap91 = FillFHashMap91(); - _smallMap = FillSmallMap(); - _fHashMap = FillFHashMap(); - _concurrentDict = ConcurrentDict(); - _immutableDict = ImmutableDict(); - } + private ImTools.V2.ImHashMap[] _mapSlots; + public ImTools.V2.ImHashMap[] ImHashMapSlots_AddOrUpdate() + { + var map = ImHashMapSlots.CreateWithEmpty(); - #region Population + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key, "a"); - private ImTools.V2.ImHashMap _mapV2; - public ImTools.V2.ImHashMap V2_AddOrUpdate() - { - var map = ImTools.V2.ImHashMap.Empty; + map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; + } - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key, "a"); + private ImTools.V2.Experimental.ImMap> _mapExp; + public ImTools.V2.Experimental.ImMap> Experimental_ImHashMap_AddOrUpdate() + { + var map = ImTools.V2.Experimental.ImMap>.Empty; - map = map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - private ImTools.V2.ImHashMap[] _mapSlots; - public ImTools.V2.ImHashMap[] ImHashMapSlots_AddOrUpdate() - { - var map = ImHashMapSlots.CreateWithEmpty(); + return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + } - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key, "a"); + private ImTools.Experiments.RefEqHashMap _refEqHashMap; + public ImTools.Experiments.RefEqHashMap RefEqHashMap_AddOrUpdate() + { + var map = new ImTools.Experiments.RefEqHashMap(5); - map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key, "a"); - private ImTools.V2.Experimental.ImMap> _mapExp; - public ImTools.V2.Experimental.ImMap> Experimental_ImHashMap_AddOrUpdate() - { - var map = ImTools.V2.Experimental.ImMap>.Empty; + map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; + } - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + private ImTools.ImHashMap _mapV4; + public ImTools.ImHashMap V4_ImHashMap_AddOrUpdate() + { + var map = ImTools.ImHashMap.Empty; - return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - } + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - private ImTools.Experiments.RefEqHashMap _refEqHashMap; - public ImTools.Experiments.RefEqHashMap RefEqHashMap_AddOrUpdate() - { - var map = new ImTools.Experiments.RefEqHashMap(5); + return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + } - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key, "a"); + private ImToolsV3.ImHashMap _mapV3; + public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() + { + var map = ImToolsV3.ImHashMap.Empty; - map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - private ImTools.ImHashMap _mapV4; - public ImTools.ImHashMap V4_ImHashMap_AddOrUpdate() - { - var map = ImTools.ImHashMap.Empty; + return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + } - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + private ImTools.ImHashMap[] _partMapV4; + public ImTools.ImHashMap[] V4_PartitionedHashMap_AddOrUpdate() + { + var map = ImTools.PartitionedHashMap.CreateEmpty(); - return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - } + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key.GetHashCode(), key, "a"); - private ImToolsV3.ImHashMap _mapV3; - public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() - { - var map = ImToolsV3.ImHashMap.Empty; + map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + return map; + } - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + private ImToolsV3.ImHashMap[] _partMapV3; + public ImToolsV3.ImHashMap[] V3_PartitionedHashMap_AddOrUpdate() + { + var map = ImToolsV3.PartitionedHashMap.CreateEmpty(); - return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - } + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key.GetHashCode(), key, "a"); - private ImTools.ImHashMap[] _partMapV4; - public ImTools.ImHashMap[] V4_PartitionedHashMap_AddOrUpdate() - { - var map = ImTools.PartitionedHashMap.CreateEmpty(); + map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + return map; + } - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key.GetHashCode(), key, "a"); + private ImToolsV3.ImHashMap[] _partMap23_32; + public ImToolsV3.ImHashMap[] V3_PartitionedHashMap32_AddOrUpdate() + { + var maps = ImToolsV3.PartitionedHashMap.CreateEmpty(32); - map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - return map; - } + foreach (var key in _keys.Take(Count)) + maps.AddOrUpdate(key.GetHashCode(), key, "a", 31); - private ImToolsV3.ImHashMap[] _partMapV3; - public ImToolsV3.ImHashMap[] V3_PartitionedHashMap_AddOrUpdate() - { - var map = ImToolsV3.PartitionedHashMap.CreateEmpty(); + maps.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!", 31); + return maps; + } - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key.GetHashCode(), key, "a"); + public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots32_AddOrUpdate() + { + var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(32); - map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - return map; - } + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a"), 31); - private ImToolsV3.ImHashMap[] _partMap23_32; - public ImToolsV3.ImHashMap[] V3_PartitionedHashMap32_AddOrUpdate() - { - var maps = ImToolsV3.PartitionedHashMap.CreateEmpty(32); + map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!"), 31); + return map; + } - foreach (var key in _keys.Take(Count)) - maps.AddOrUpdate(key.GetHashCode(), key, "a", 31); + private ImTools.V2.Experimental.ImMap>[] _mapSlotsExp32; - maps.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!", 31); - return maps; - } + public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots64_AddOrUpdate() + { + var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(64); - public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots32_AddOrUpdate() - { - var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(32); + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a"), 63); - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a"), 31); + map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!"), 63); + return map; + } - map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!"), 31); - return map; - } + private ImTools.V2.Experimental.ImMap>[] _mapSlotsExp64; - private ImTools.V2.Experimental.ImMap>[] _mapSlotsExp32; + public Dictionary Dict() + { + var map = new Dictionary(); - public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots64_AddOrUpdate() - { - var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(64); + foreach (var key in _keys.Take(Count)) + map.TryAdd(key, "a"); - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a"), 63); + map.TryAdd(typeof(ImHashMapBenchmarks), "!"); - map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!"), 63); - return map; - } + return map; + } - private ImTools.V2.Experimental.ImMap>[] _mapSlotsExp64; + private Dictionary _dict; - public Dictionary Dict() - { - var map = new Dictionary(); + public DictionarySlim DictSlim() + { + var dict = new DictionarySlim(); - foreach (var key in _keys.Take(Count)) - map.TryAdd(key, "a"); + foreach (var key in _keys.Take(Count)) + dict.GetOrAddValueRef(key) = "a"; - map.TryAdd(typeof(ImHashMapBenchmarks), "!"); + dict.GetOrAddValueRef(LookupKey) = "!"; + return dict; + } - return map; - } + private DictionarySlim _dictSlim; - private Dictionary _dict; + public ImTools.Experiments.FHashMap7> FillFHashMap7() + { + var map = new ImTools.Experiments.FHashMap7>(); - public DictionarySlim DictSlim() - { - var dict = new DictionarySlim(); + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key, "a"); - foreach (var key in _keys.Take(Count)) - dict.GetOrAddValueRef(key) = "a"; + map.AddOrUpdate(LookupKey, "!"); + return map; + } - dict.GetOrAddValueRef(LookupKey) = "!"; - return dict; - } + private ImTools.Experiments.FHashMap7> _fHashMap7; - private DictionarySlim _dictSlim; + public ImTools.Experiments.FHashMap9> FillFHashMap9() + { + var map = new ImTools.Experiments.FHashMap9>(); - public ImTools.Experiments.FHashMap7> FillFHashMap7() - { - var map = new ImTools.Experiments.FHashMap7>(); + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key, "a"); - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key, "a"); + map.AddOrUpdate(LookupKey, "!"); + return map; + } - map.AddOrUpdate(LookupKey, "!"); - return map; - } + private ImTools.Experiments.FHashMap9> _fHashMap9; - private ImTools.Experiments.FHashMap7> _fHashMap7; + public FHashMap91TypeString FillFHashMap91() + { + var map = new FHashMap91TypeString(); - public ImTools.Experiments.FHashMap9> FillFHashMap9() - { - var map = new ImTools.Experiments.FHashMap9>(); + foreach (var key in _keys.Take(Count)) + map.GetOrAddValueRef(key) = "a"; - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key, "a"); + map.GetOrAddValueRef(LookupKey) = "!"; + return map; + } - map.AddOrUpdate(LookupKey, "!"); - return map; - } + private FHashMap91TypeString _fHashMap91; - private ImTools.Experiments.FHashMap9> _fHashMap9; + public SmallMapTypeString FillSmallMap() + { + var map = new SmallMapTypeString(); - public FHashMap91TypeString FillFHashMap91() - { - var map = new FHashMap91TypeString(); + foreach (var key in _keys.Take(Count)) + map.GetOrAddValueRef(key) = "a"; - foreach (var key in _keys.Take(Count)) - map.GetOrAddValueRef(key) = "a"; + map.GetOrAddValueRef(LookupKey) = "!"; + return map; + } - map.GetOrAddValueRef(LookupKey) = "!"; - return map; - } + private SmallMapTypeString _smallMap; - private FHashMap91TypeString _fHashMap91; + public FHashMap11> FillFHashMap11() + { + var map = new FHashMap11>(); - public SmallMapTypeString FillSmallMap() - { - var map = new SmallMapTypeString(); + foreach (var key in _keys.Take(Count)) + map.GetOrAddValueRef(key) = "a"; - foreach (var key in _keys.Take(Count)) - map.GetOrAddValueRef(key) = "a"; + map.GetOrAddValueRef(LookupKey) = "!"; + return map; + } - map.GetOrAddValueRef(LookupKey) = "!"; - return map; - } + private FHashMap11> _fHashMap11; - private SmallMapTypeString _smallMap; + public void FillFecHashMap(ref FecSmallMapTypeString map) + { + foreach (var key in _keys.Take(Count)) + map.Map.AddOrUpdate(key, "a"); - public FHashMapTypeString FillFHashMap() - { - var map = new FHashMapTypeString(); + map.Map.AddOrUpdate(LookupKey, "!"); + } - foreach (var key in _keys.Take(Count)) - map.GetOrAddValueRef(key, out _) = "a"; + private FecSmallMapTypeString _fecSmallMap; - map.GetOrAddValueRef(LookupKey, out _) = "!"; - return map; - } + public ConcurrentDictionary ConcurrentDict() + { + var map = new ConcurrentDictionary(); - private FHashMapTypeString _fHashMap; + foreach (var key in _keys.Take(Count)) + map.TryAdd(key, "a"); - public ConcurrentDictionary ConcurrentDict() - { - var map = new ConcurrentDictionary(); + map.TryAdd(typeof(ImHashMapBenchmarks), "!"); + return map; + } - foreach (var key in _keys.Take(Count)) - map.TryAdd(key, "a"); + private ConcurrentDictionary _concurrentDict; - map.TryAdd(typeof(ImHashMapBenchmarks), "!"); - return map; - } + public ImmutableDictionary ImmutableDict() + { + var builder = ImmutableDictionary.CreateBuilder(); - private ConcurrentDictionary _concurrentDict; + foreach (var key in _keys.Take(Count)) + builder.Add(key, "a"); + builder.Add(typeof(ImHashMapBenchmarks), "!"); + return builder.ToImmutable(); + } - public ImmutableDictionary ImmutableDict() - { - var builder = ImmutableDictionary.CreateBuilder(); + private ImmutableDictionary _immutableDict; - foreach (var key in _keys.Take(Count)) - builder.Add(key, "a"); - builder.Add(typeof(ImHashMapBenchmarks), "!"); - return builder.ToImmutable(); - } + public TypeDictionary TypeDictionary_Add() + { + var map = new TypeDictionary(); + + map.Add("a"); + map.Add("a"); + map.Add("a"); + map.Add("a"); + map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + + map.Add("!"); + + return map; + } - private ImmutableDictionary _immutableDict; + private TypeDictionary _typeDict; - public TypeDictionary TypeDictionary_Add() - { - var map = new TypeDictionary(); - - map.Add("a"); - map.Add("a"); - map.Add("a"); - map.Add("a"); - map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - - map.Add("!"); - - return map; - } + #endregion - private TypeDictionary _typeDict; + public static Type LookupKey = typeof(Console); - #endregion + // [Benchmark(Baseline = true)] + public string V2_ImHashMap_AVL_TryFind() + { + _mapV2.TryFind(LookupKey, out var result); + return result; + } - public static Type LookupKey = typeof(Console); + // [Benchmark] + public string ImHashMapSlots32_TryFind() + { + var hash = LookupKey.GetHashCode(); + _mapSlots[hash & ImHashMapSlots.HASH_MASK_TO_FIND_SLOT].TryFind(hash, LookupKey, out var result); + return result; + } - // [Benchmark(Baseline = true)] - public string V2_ImHashMap_AVL_TryFind() - { - _mapV2.TryFind(LookupKey, out var result); - return result; - } + // [Benchmark] + public string V2_ImHashMap_AVLOptimizedForAdd_TryFind() + { + _mapExp.TryFind(LookupKey.GetHashCode(), LookupKey, out var result); + return (string)result; + } - // [Benchmark] - public string ImHashMapSlots32_TryFind() - { - var hash = LookupKey.GetHashCode(); - _mapSlots[hash & ImHashMapSlots.HASH_MASK_TO_FIND_SLOT].TryFind(hash, LookupKey, out var result); - return result; - } + // [Benchmark(Baseline = true)] + public string V4_ImHashMap_TryFind() + { + _mapV4.TryFind(LookupKey.GetHashCode(), LookupKey, out var result); + return result; + } - // [Benchmark] - public string V2_ImHashMap_AVLOptimizedForAdd_TryFind() - { - _mapExp.TryFind(LookupKey.GetHashCode(), LookupKey, out var result); - return (string)result; - } + // [Benchmark] + public string V3_ImHashMap_TryFind() + { + _mapV3.TryFind(LookupKey.GetHashCode(), LookupKey, out var result); + return result; + } - // [Benchmark(Baseline = true)] - public string V4_ImHashMap_TryFind() - { - _mapV4.TryFind(LookupKey.GetHashCode(), LookupKey, out var result); - return result; - } + // [Benchmark] + public string V4_ImMap_GetValueOrDefault() => + _mapV4.GetValueOrDefaultByReferenceEquals(LookupKey.GetHashCode(), LookupKey); + + // [Benchmark] + public string V3_ImHashMap_GetValueOrDefault() => + _mapV3.GetValueOrDefaultByReferenceEquals(LookupKey.GetHashCode(), LookupKey); + + // [Benchmark] + public string V3_PartitionedHashMap_GetValueOrDefault() => + _partMapV3.GetValueOrDefaultByReferenceEquals(LookupKey); + + // [Benchmark] + public string V3_PartitionedHashMap32_GetValueOrDefault() => + _partMap23_32.GetValueOrDefaultByReferenceEquals(LookupKey, 31); + + // [Benchmark] + public string TypeDict_TryFind() => + _typeDict.Get(); + + // [Benchmark] + // public string V3_ImHashMap_23Tree_TryFind_SIMPLIFIED() + // { + // var entry = _map234.GetEntryOrDefault(LookupKey.GetHashCode()); + // if (entry == null) + // return null; + // return ((ImTools.Experimental.ImHashMap234.KeyValueEntry)entry).Value; + // } + + // [Benchmark] + public string V4_PartitionedHashMap_TryFind() + { + var hash = LookupKey.GetHashCode(); + _partMapV4[hash & ImToolsV3.PartitionedHashMap.PARTITION_HASH_MASK].TryFind(hash, LookupKey, out var result); + return result; + } - // [Benchmark] - public string V3_ImHashMap_TryFind() - { - _mapV3.TryFind(LookupKey.GetHashCode(), LookupKey, out var result); - return result; - } + // [Benchmark] + public string V3_PartitionedHashMap_TryFind() + { + var hash = LookupKey.GetHashCode(); + _partMapV3[hash & ImToolsV3.PartitionedHashMap.PARTITION_HASH_MASK].TryFind(hash, LookupKey, out var result); + return result; + } - // [Benchmark] - public string V4_ImMap_GetValueOrDefault() => - _mapV4.GetValueOrDefaultByReferenceEquals(LookupKey.GetHashCode(), LookupKey); - - // [Benchmark] - public string V3_ImHashMap_GetValueOrDefault() => - _mapV3.GetValueOrDefaultByReferenceEquals(LookupKey.GetHashCode(), LookupKey); - - // [Benchmark] - public string V3_PartitionedHashMap_GetValueOrDefault() => - _partMapV3.GetValueOrDefaultByReferenceEquals(LookupKey); - - // [Benchmark] - public string V3_PartitionedHashMap32_GetValueOrDefault() => - _partMap23_32.GetValueOrDefaultByReferenceEquals(LookupKey, 31); - - // [Benchmark] - public string TypeDict_TryFind() => - _typeDict.Get(); - - // [Benchmark] - // public string V3_ImHashMap_23Tree_TryFind_SIMPLIFIED() - // { - // var entry = _map234.GetEntryOrDefault(LookupKey.GetHashCode()); - // if (entry == null) - // return null; - // return ((ImTools.Experimental.ImHashMap234.KeyValueEntry)entry).Value; - // } - - // [Benchmark] - public string V4_PartitionedHashMap_TryFind() - { - var hash = LookupKey.GetHashCode(); - _partMapV4[hash & ImToolsV3.PartitionedHashMap.PARTITION_HASH_MASK].TryFind(hash, LookupKey, out var result); - return result; - } + // [Benchmark] + public string Experimental_ImHashMapSlots32_TryFind() + { + var hash = LookupKey.GetHashCode(); + _mapSlotsExp32[hash & ImHashMapSlots.HASH_MASK_TO_FIND_SLOT].TryFind(hash, LookupKey, out var result); + return (string)result; + } - // [Benchmark] - public string V3_PartitionedHashMap_TryFind() - { - var hash = LookupKey.GetHashCode(); - _partMapV3[hash & ImToolsV3.PartitionedHashMap.PARTITION_HASH_MASK].TryFind(hash, LookupKey, out var result); - return result; - } + // [Benchmark] + public string Experimental_ImHashMapSlots64_TryFind() + { + var hash = LookupKey.GetHashCode(); + _mapSlotsExp64[hash & 63].TryFind(hash, LookupKey, out var result); + return (string)result; + } - // [Benchmark] - public string Experimental_ImHashMapSlots32_TryFind() - { - var hash = LookupKey.GetHashCode(); - _mapSlotsExp32[hash & ImHashMapSlots.HASH_MASK_TO_FIND_SLOT].TryFind(hash, LookupKey, out var result); - return (string)result; - } + // [Benchmark] + // [Benchmark(Baseline = true)] + public string Dictionary_TryGetValue() + { + _dict.TryGetValue(LookupKey, out var result); + return result; + } - // [Benchmark] - public string Experimental_ImHashMapSlots64_TryFind() - { - var hash = LookupKey.GetHashCode(); - _mapSlotsExp64[hash & 63].TryFind(hash, LookupKey, out var result); - return (string)result; - } + // [Benchmark] + [Benchmark(Baseline = true)] + public int DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent() + { + var dict = new DictionarySlim(); - // [Benchmark] - // [Benchmark(Baseline = true)] - public string Dictionary_TryGetValue() - { - _dict.TryGetValue(LookupKey, out var result); - return result; - } + foreach (var key in _presentKeys) + dict.GetOrAddValueRef(key) = "a"; - // [Benchmark] - [Benchmark(Baseline = true)] - public string DictionarySlim_TryGetValue() + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) { - _dictSlim.TryGetValue(LookupKey, out var result); - return result; + if (dict.TryGetValue(_randomPresentKeys[i], out var result)) + count += result.Length; + if (!dict.TryGetValue(_missingKeys[i], out var _)) + --count; } + return count; + } - [Benchmark] - public string SmallMap_TryGetValue() - { - _smallMap.TryGetValue(LookupKey, out var result); - return result; - } + [Benchmark] + public int SmallMap_PopulateThenLookup_HalfMissed_HalfPresent() + { + var map = new SmallMapTypeString(); - [Benchmark] - public string FHashMap_TryGetValue() => - _fHashMap.TryGetValueRef(LookupKey, out _); + foreach (var key in _presentKeys) + map.GetOrAddValueRef(key) = "a"; - // [Benchmark(Baseline = true)] - // [Benchmark] - public string FHashMap7_TryGetValue() + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) { - _fHashMap7.TryGetValue(LookupKey, out var result); - return result; + if (map.TryGetValue(_randomPresentKeys[i], out var result)) + count += result.Length; + if (!map.TryGetValue(_missingKeys[i], out var _)) + --count; } + return count; + } - // [Benchmark] - public string FHashMap9_TryGetValue() + // [Benchmark(Baseline = true)] + [Benchmark] + public int FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent() + { + var m = new FecSmallMapTypeString(); + ref var map = ref m.Map; + + foreach (var key in _presentKeys) + map.AddOrUpdate(key, "a"); + + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) { - _fHashMap9.TryGetValue(LookupKey, out var result); - return result; + ref var result = ref map.TryGetEntryRef(_randomPresentKeys[i], out var found); + if (found) + count += result.Value.Length; + _ = ref map.TryGetEntryRef(_missingKeys[i], out found); + if (!found) + --count; } + return count; + } - // [Benchmark] - public string FHashMap91_TryGetValue() + // [Benchmark] + public int FecHashMap_Init1000_PopulateThenLookup_HalfMissed_HalfPresent() + { + var m = new FecSmallMapTypeString(1024); + ref var map = ref m.Map; + + foreach (var key in _presentKeys) + map.AddOrUpdate(key, "a"); + + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) { - _fHashMap91.TryGetValue(LookupKey, out var result); - return result; + ref var result = ref map.TryGetEntryRef(_randomPresentKeys[i], out var found); + if (found) + count += result.Value.Length; + _ = ref map.TryGetEntryRef(_missingKeys[i], out found); + if (!found) + --count; } + return count; + } + + // [Benchmark] + public int FecHashMap_SingleArrEntries_PopulateThenLookup_HalfMissed_HalfPresent() + { + var m = new FecSmallMapTypeString_SingleArrEntries(); + ref var map = ref m.Map; + + foreach (var key in _presentKeys) + map.AddOrUpdate(key, "a"); - // [Benchmark] - public string ConcurrentDictionary_TryGetValue() + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) { - _concurrentDict.TryGetValue(LookupKey, out var result); - return result; + ref var result = ref map.TryGetEntryRef(_randomPresentKeys[i], out var found); + if (found) + count += result.Value.Length; + _ = ref map.TryGetEntryRef(_missingKeys[i], out found); + if (!found) + --count; } + return count; + } - // [Benchmark] - public string ImmutableDict_TryGet() + // [Benchmark] + public int FHashMap11_TryGetValue() + { + var count = 0; + foreach (var k in _randomPresentKeys) { - _immutableDict.TryGetValue(LookupKey, out var result); - return result; + _fHashMap11.TryGetValue(k, out var result); + count += result.Length; } + + return count; + } + + // [Benchmark(Baseline = true)] + // [Benchmark] + public string FHashMap7_TryGetValue() + { + _fHashMap7.TryGetValue(LookupKey, out var result); + return result; } - [MemoryDiagnoser] - public class Enumerate + // [Benchmark] + public string FHashMap9_TryGetValue() { - /* - ## V2: + _fHashMap9.TryGetValue(LookupKey, out var result); + return result; + } + + // [Benchmark] + public string FHashMap91_TryGetValue() + { + _fHashMap91.TryGetValue(LookupKey, out var result); + return result; + } + + // [Benchmark] + public string ConcurrentDictionary_TryGetValue() + { + _concurrentDict.TryGetValue(LookupKey, out var result); + return result; + } + + // [Benchmark] + public string ImmutableDict_TryGet() + { + _immutableDict.TryGetValue(LookupKey, out var result); + return result; + } + } - BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 - Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=3.0.100 - [Host] : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT - DefaultJob : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT + [MemoryDiagnoser] + public class Enumerate + { + /* + ## V2: + + BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 + Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=3.0.100 + [Host] : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT + DefaultJob : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | |------------------------------ |------ |--------------:|-----------:|-----------:|------:|--------:|--------:|-------:|------:|----------:| @@ -2838,8 +3136,8 @@ public class Enumerate BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.630 (2004/?/20H1) Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores .NET Core SDK=5.0.100 - [Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT - DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT +[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT +DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | @@ -2949,8 +3247,8 @@ public class Enumerate BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 Intel Core i5-8350U CPU 1.70GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores .NET Core SDK=6.0.102 - [Host] : .NET Core 6.0.2 (CoreCLR 6.0.222.6406, CoreFX 6.0.222.6406), X64 RyuJIT - DefaultJob : .NET Core 6.0.2 (CoreCLR 6.0.222.6406, CoreFX 6.0.222.6406), X64 RyuJIT +[Host] : .NET Core 6.0.2 (CoreCLR 6.0.222.6406, CoreFX 6.0.222.6406), X64 RyuJIT +DefaultJob : .NET Core 6.0.2 (CoreCLR 6.0.222.6406, CoreFX 6.0.222.6406), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | @@ -2972,8 +3270,8 @@ public class Enumerate BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores .NET Core SDK=6.0.202 - [Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT - DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT +[Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT +DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | |-------------------------------- |------ |-------------:|-----------:|-----------:|-------------:|------:|--------:|-------:|------:|------:|----------:| @@ -3018,8 +3316,8 @@ public class Enumerate BenchmarkDotNet v0.13.6, Windows 11 (10.0.22621.1992/22H2/2022Update/SunValley2) 11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores .NET SDK 7.0.306 - [Host] : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 +[Host] : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 +DefaultJob : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | |------------------------- |------ |---------:|---------:|---------:|------:|--------:|----------:|------------:| @@ -3037,375 +3335,374 @@ .NET SDK 7.0.306 */ - // [Params(1, 10, 100, 1_000)] - [Params(100)] - public int Count; + // [Params(1, 10, 100, 1_000)] + [Params(10, 100, 1000)] + public int Count; - [GlobalSetup] - public void Populate() - { - _mapV2 = V2_AddOrUpdate(); - // _mapExp = Experimental_ImHashMap_AddOrUpdate(); - _mapV4 = V4_ImMap_AddOrUpdate(); - _mapV3 = V3_ImHashMap_AddOrUpdate(); - _mapPartV4 = V4_PartionedHashMap_AddOrUpdate(); - _mapPartV3 = V3_PartionedHashMap_AddOrUpdate(); - _dict = Dict(); - _dictSlim = DictSlim_GetOrAddValueRef(); - _fHashMap = FHashMap_AddOrUpdate(); - _concurrentDict = ConcurrentDict(); - _immutableDict = ImmutableDict(); - } + [GlobalSetup] + public void Populate() + { + _mapV2 = V2_AddOrUpdate(); + // _mapExp = Experimental_ImHashMap_AddOrUpdate(); + _mapV4 = V4_ImMap_AddOrUpdate(); + _mapV3 = V3_ImHashMap_AddOrUpdate(); + _mapPartV4 = V4_PartionedHashMap_AddOrUpdate(); + _mapPartV3 = V3_PartionedHashMap_AddOrUpdate(); + _dict = Dict(); + _dictSlim = DictSlim_GetOrAddValueRef(); + _fHashMap = FHashMap_AddOrUpdate(); + _concurrentDict = ConcurrentDict(); + _immutableDict = ImmutableDict(); + } - #region Population + #region Population - public ImTools.V2.ImHashMap V2_AddOrUpdate() - { - var map = ImTools.V2.ImHashMap.Empty; + public ImTools.V2.ImHashMap V2_AddOrUpdate() + { + var map = ImTools.V2.ImHashMap.Empty; - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key, "a"); + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key, "a"); - return map; - } + return map; + } - private ImTools.V2.ImHashMap _mapV2; + private ImTools.V2.ImHashMap _mapV2; - private ImTools.V2.Experimental.ImMap> _mapExp; + private ImTools.V2.Experimental.ImMap> _mapExp; - public ImTools.V2.Experimental.ImMap> Experimental_ImHashMap_AddOrUpdate() - { - var map = ImTools.V2.Experimental.ImMap>.Empty; + public ImTools.V2.Experimental.ImMap> Experimental_ImHashMap_AddOrUpdate() + { + var map = ImTools.V2.Experimental.ImMap>.Empty; - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - return map; - } + return map; + } - private ImToolsV3.ImHashMap _mapV3; - public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() - { - var map = ImToolsV3.ImHashMap.Empty; + private ImToolsV3.ImHashMap _mapV3; + public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() + { + var map = ImToolsV3.ImHashMap.Empty; - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - return map; - } + return map; + } - private ImTools.ImHashMap _mapV4; + private ImTools.ImHashMap _mapV4; - public ImTools.ImHashMap V4_ImMap_AddOrUpdate() - { - var map = ImTools.ImHashMap.Empty; + public ImTools.ImHashMap V4_ImMap_AddOrUpdate() + { + var map = ImTools.ImHashMap.Empty; - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - return map; - } + return map; + } - private ImTools.ImHashMap[] _mapPartV4; - public ImTools.ImHashMap[] V4_PartionedHashMap_AddOrUpdate() - { - var map = ImTools.PartitionedHashMap.CreateEmpty(); + private ImTools.ImHashMap[] _mapPartV4; + public ImTools.ImHashMap[] V4_PartionedHashMap_AddOrUpdate() + { + var map = ImTools.PartitionedHashMap.CreateEmpty(); - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key, "a"); + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key, "a"); - return map; - } + return map; + } - private ImToolsV3.ImHashMap[] _mapPartV3; - public ImToolsV3.ImHashMap[] V3_PartionedHashMap_AddOrUpdate() - { - var map = ImToolsV3.PartitionedHashMap.CreateEmpty(); + private ImToolsV3.ImHashMap[] _mapPartV3; + public ImToolsV3.ImHashMap[] V3_PartionedHashMap_AddOrUpdate() + { + var map = ImToolsV3.PartitionedHashMap.CreateEmpty(); - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key, "a"); + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key, "a"); - return map; - } + return map; + } - public Dictionary Dict() - { - var map = new Dictionary(); + public Dictionary Dict() + { + var map = new Dictionary(); - foreach (var key in _keys.Take(Count)) - map.TryAdd(key, "a"); + foreach (var key in _keys.Take(Count)) + map.TryAdd(key, "a"); - return map; - } + return map; + } - private Dictionary _dict; + private Dictionary _dict; - public DictionarySlim DictSlim_GetOrAddValueRef() - { - var dict = new DictionarySlim(); + public DictionarySlim DictSlim_GetOrAddValueRef() + { + var dict = new DictionarySlim(); - foreach (var key in _keys.Take(Count)) - dict.GetOrAddValueRef(key) = "a"; + foreach (var key in _keys.Take(Count)) + dict.GetOrAddValueRef(key) = "a"; - return dict; - } + return dict; + } - private FHashMap91TypeString _fHashMap; + private FHashMap91TypeString _fHashMap; - public FHashMap91TypeString FHashMap_AddOrUpdate() - { - var map = new FHashMap91TypeString(); + public FHashMap91TypeString FHashMap_AddOrUpdate() + { + var map = new FHashMap91TypeString(); - foreach (var key in _keys.Take(Count)) - map.GetOrAddValueRef(key) = "a"; + foreach (var key in _keys.Take(Count)) + map.GetOrAddValueRef(key) = "a"; - return map; - } + return map; + } - private DictionarySlim _dictSlim; + private DictionarySlim _dictSlim; - public ConcurrentDictionary ConcurrentDict() - { - var map = new ConcurrentDictionary(); + public ConcurrentDictionary ConcurrentDict() + { + var map = new ConcurrentDictionary(); - foreach (var key in _keys.Take(Count)) - map.TryAdd(key, "a"); + foreach (var key in _keys.Take(Count)) + map.TryAdd(key, "a"); - return map; - } + return map; + } - private ConcurrentDictionary _concurrentDict; + private ConcurrentDictionary _concurrentDict; - public ImmutableDictionary ImmutableDict() - { - var builder = ImmutableDictionary.CreateBuilder(); + public ImmutableDictionary ImmutableDict() + { + var builder = ImmutableDictionary.CreateBuilder(); - foreach (var key in _keys.Take(Count)) - builder.Add(key, "a"); + foreach (var key in _keys.Take(Count)) + builder.Add(key, "a"); - return builder.ToImmutable(); - } + return builder.ToImmutable(); + } - private ImmutableDictionary _immutableDict; + private ImmutableDictionary _immutableDict; - #endregion + #endregion - // [Benchmark(Baseline = true)] - public object V4_ImHashMap_Enumerate() - { - var s = ""; - foreach (var x in _mapV4.Enumerate()) - s = x.Value; - return s; - } + // [Benchmark(Baseline = true)] + public object V4_ImHashMap_Enumerate() + { + var s = ""; + foreach (var x in _mapV4.Enumerate()) + s = x.Value; + return s; + } - // [Benchmark] - public object V3_ImHashMap_Enumerate() - { - var s = ""; - foreach (var x in _mapV3.Enumerate()) - s = x.Value; - return s; - } + // [Benchmark] + public object V3_ImHashMap_Enumerate() + { + var s = ""; + foreach (var x in _mapV3.Enumerate()) + s = x.Value; + return s; + } - // [Benchmark] - public object V4_PartitionedHashMap_Enumerate() - { - var s = ""; - foreach (var x in _mapPartV4.Enumerate()) - s = x.Value; - return s; - } + // [Benchmark] + public object V4_PartitionedHashMap_Enumerate() + { + var s = ""; + foreach (var x in _mapPartV4.Enumerate()) + s = x.Value; + return s; + } - // [Benchmark] - public object V3_PartitionedHashMap_Enumerate() - { - var s = ""; - foreach (var x in _mapPartV3.Enumerate()) - s = x.Value; - return s; - } + // [Benchmark] + public object V3_PartitionedHashMap_Enumerate() + { + var s = ""; + foreach (var x in _mapPartV3.Enumerate()) + s = x.Value; + return s; + } - // [Benchmark] - public object V2_ImHashMap_Enumerate() - { - var s = ""; - foreach (var x in _mapV2.Enumerate()) - s = x.Value; - return s; - } + // [Benchmark] + public object V2_ImHashMap_Enumerate() + { + var s = ""; + foreach (var x in _mapV2.Enumerate()) + s = x.Value; + return s; + } - [Benchmark(Baseline = true)] - public object DictionarySlim_Enumerate() - { - var s = ""; - foreach (var x in _dictSlim) - s = x.Value; - return s; - } + [Benchmark(Baseline = true)] + public object DictionarySlim_Enumerate() + { + var s = ""; + foreach (var x in _dictSlim) + s = x.Value; + return s; + } - [Benchmark] - public object Dictionary_Enumerate() - { - var s = ""; - foreach (var x in _dict) - s = x.Value; - return s; - } + [Benchmark] + public object Dictionary_Enumerate() + { + var s = ""; + foreach (var x in _dict) + s = x.Value; + return s; + } - [Benchmark] - public object FHashMap91_Enumerate() - { - var s = ""; - foreach (var x in _fHashMap) - s = x.Value; - return s; - } + [Benchmark] + public object FHashMap91_Enumerate() + { + var s = ""; + foreach (var x in _fHashMap) + s = x.Value; + return s; + } - // [Benchmark] - public object ConcurrentDictionary_foreach() - { - var s = ""; - foreach (var x in _concurrentDict) - s = x.Value; - return s; - } + // [Benchmark] + public object ConcurrentDictionary_foreach() + { + var s = ""; + foreach (var x in _concurrentDict) + s = x.Value; + return s; + } - // [Benchmark] - public object ImmutableDict_Enumerate() - { - var s = ""; - foreach (var x in _immutableDict) - s = x.Value; - return s; - } + // [Benchmark] + public object ImmutableDict_Enumerate() + { + var s = ""; + foreach (var x in _immutableDict) + s = x.Value; + return s; } + } - [MemoryDiagnoser] - public class ToArray - { - /* - BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 - Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=5.0.202 - [Host] : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT - DefaultJob : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT - - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |---------------------- |------ |----------:|---------:|----------:|------:|--------:|-------:|------:|------:|----------:| - | UsingLambda | 1 | 29.07 ns | 0.672 ns | 1.722 ns | 1.00 | 0.00 | 0.0051 | - | - | 32 B | - | UsingGenericStruct | 1 | 27.87 ns | 0.657 ns | 1.660 ns | 0.96 | 0.08 | 0.0051 | - | - | 32 B | - | UsingNonGenericStruct | 1 | 15.11 ns | 0.391 ns | 0.930 ns | 0.52 | 0.05 | 0.0051 | - | - | 32 B | - | | | | | | | | | | | | - | UsingLambda | 10 | 159.52 ns | 3.168 ns | 5.379 ns | 1.00 | 0.00 | 0.0293 | - | - | 184 B | - | UsingGenericStruct | 10 | 195.96 ns | 4.683 ns | 13.435 ns | 1.28 | 0.09 | 0.0293 | - | - | 184 B | - | UsingNonGenericStruct | 10 | 134.08 ns | 2.785 ns | 7.578 ns | 0.85 | 0.06 | 0.0293 | - | - | 184 B | - */ - [Params(1, 10)]//, 100, 1_000)] - public int Count; - - [GlobalSetup] - public void Populate() - { - _mapV3 = V3_ImHashMap_AddOrUpdate(); - } + [MemoryDiagnoser] + public class ToArray + { + /* + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 + Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=5.0.202 + [Host] : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT + DefaultJob : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT + + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |---------------------- |------ |----------:|---------:|----------:|------:|--------:|-------:|------:|------:|----------:| + | UsingLambda | 1 | 29.07 ns | 0.672 ns | 1.722 ns | 1.00 | 0.00 | 0.0051 | - | - | 32 B | + | UsingGenericStruct | 1 | 27.87 ns | 0.657 ns | 1.660 ns | 0.96 | 0.08 | 0.0051 | - | - | 32 B | + | UsingNonGenericStruct | 1 | 15.11 ns | 0.391 ns | 0.930 ns | 0.52 | 0.05 | 0.0051 | - | - | 32 B | + | | | | | | | | | | | | + | UsingLambda | 10 | 159.52 ns | 3.168 ns | 5.379 ns | 1.00 | 0.00 | 0.0293 | - | - | 184 B | + | UsingGenericStruct | 10 | 195.96 ns | 4.683 ns | 13.435 ns | 1.28 | 0.09 | 0.0293 | - | - | 184 B | + | UsingNonGenericStruct | 10 | 134.08 ns | 2.785 ns | 7.578 ns | 0.85 | 0.06 | 0.0293 | - | - | 184 B | + */ + [Params(1, 10)]//, 100, 1_000)] + public int Count; + + [GlobalSetup] + public void Populate() + { + _mapV3 = V3_ImHashMap_AddOrUpdate(); + } - [Benchmark(Baseline = true)] - public object UsingLambda() => _mapV3.ToArray(); + [Benchmark(Baseline = true)] + public object UsingLambda() => _mapV3.ToArray(); - private ImToolsV3.ImHashMap _mapV3; - public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() - { - var map = ImToolsV3.ImHashMap.Empty; + private ImToolsV3.ImHashMap _mapV3; + public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() + { + var map = ImToolsV3.ImHashMap.Empty; - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - return map; - } + return map; } + } - [MemoryDiagnoser] - public class GetAndUpdate_vs_AddOrGetAndReplace - { - /* - BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 - Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=5.0.202 - [Host] : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT - DefaultJob : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT - - ## Initial results - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |-------------------------------------------------- |------ |---------:|---------:|---------:|------:|--------:|-------:|------:|------:|----------:| - | Get_then_AddOrUpdate | 1 | 29.34 ns | 0.397 ns | 0.371 ns | 1.00 | 0.00 | 0.0114 | - | - | 72 B | - | AddOrGet_then_Replace | 1 | 25.17 ns | 0.568 ns | 0.504 ns | 0.86 | 0.02 | 0.0114 | - | - | 72 B | - | AddOrGet_then_Replace_inlined_without_lambda_cost | 1 | 24.05 ns | 0.316 ns | 0.296 ns | 0.82 | 0.01 | 0.0115 | - | - | 72 B | - | | | | | | | | | | | | - | Get_then_AddOrUpdate | 5 | 32.54 ns | 0.725 ns | 0.917 ns | 1.00 | 0.00 | 0.0114 | - | - | 72 B | - | AddOrGet_then_Replace | 5 | 26.16 ns | 0.584 ns | 0.546 ns | 0.80 | 0.03 | 0.0114 | - | - | 72 B | - | AddOrGet_then_Replace_inlined_without_lambda_cost | 5 | 25.00 ns | 0.266 ns | 0.236 ns | 0.76 | 0.02 | 0.0114 | - | - | 72 B | - | | | | | | | | | | | | - | Get_then_AddOrUpdate | 10 | 51.65 ns | 1.085 ns | 0.962 ns | 1.00 | 0.00 | 0.0178 | - | - | 112 B | - | AddOrGet_then_Replace | 10 | 48.35 ns | 0.521 ns | 0.487 ns | 0.94 | 0.02 | 0.0178 | - | - | 112 B | - | AddOrGet_then_Replace_inlined_without_lambda_cost | 10 | 38.43 ns | 0.861 ns | 2.144 ns | 0.79 | 0.05 | 0.0178 | - | - | 112 B | - | | | | | | | | | | | | - | Get_then_AddOrUpdate | 50 | 91.46 ns | 1.870 ns | 2.079 ns | 1.00 | 0.00 | 0.0370 | - | - | 232 B | - | AddOrGet_then_Replace | 50 | 85.13 ns | 1.755 ns | 2.573 ns | 0.94 | 0.03 | 0.0370 | - | - | 232 B | - | AddOrGet_then_Replace_inlined_without_lambda_cost | 50 | 85.02 ns | 1.775 ns | 1.823 ns | 0.93 | 0.03 | 0.0370 | - | - | 232 B | - - */ - [Params(1, 5, 10)]//, 50, 100, 1_000)] - public int Count; - - [GlobalSetup] - public void Populate() - { - _map = V3_ImHashMap_AddOrUpdate(); - } + [MemoryDiagnoser] + public class GetAndUpdate_vs_AddOrGetAndReplace + { + /* + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 + Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=5.0.202 + [Host] : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT + DefaultJob : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT + + ## Initial results + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |-------------------------------------------------- |------ |---------:|---------:|---------:|------:|--------:|-------:|------:|------:|----------:| + | Get_then_AddOrUpdate | 1 | 29.34 ns | 0.397 ns | 0.371 ns | 1.00 | 0.00 | 0.0114 | - | - | 72 B | + | AddOrGet_then_Replace | 1 | 25.17 ns | 0.568 ns | 0.504 ns | 0.86 | 0.02 | 0.0114 | - | - | 72 B | + | AddOrGet_then_Replace_inlined_without_lambda_cost | 1 | 24.05 ns | 0.316 ns | 0.296 ns | 0.82 | 0.01 | 0.0115 | - | - | 72 B | + | | | | | | | | | | | | + | Get_then_AddOrUpdate | 5 | 32.54 ns | 0.725 ns | 0.917 ns | 1.00 | 0.00 | 0.0114 | - | - | 72 B | + | AddOrGet_then_Replace | 5 | 26.16 ns | 0.584 ns | 0.546 ns | 0.80 | 0.03 | 0.0114 | - | - | 72 B | + | AddOrGet_then_Replace_inlined_without_lambda_cost | 5 | 25.00 ns | 0.266 ns | 0.236 ns | 0.76 | 0.02 | 0.0114 | - | - | 72 B | + | | | | | | | | | | | | + | Get_then_AddOrUpdate | 10 | 51.65 ns | 1.085 ns | 0.962 ns | 1.00 | 0.00 | 0.0178 | - | - | 112 B | + | AddOrGet_then_Replace | 10 | 48.35 ns | 0.521 ns | 0.487 ns | 0.94 | 0.02 | 0.0178 | - | - | 112 B | + | AddOrGet_then_Replace_inlined_without_lambda_cost | 10 | 38.43 ns | 0.861 ns | 2.144 ns | 0.79 | 0.05 | 0.0178 | - | - | 112 B | + | | | | | | | | | | | | + | Get_then_AddOrUpdate | 50 | 91.46 ns | 1.870 ns | 2.079 ns | 1.00 | 0.00 | 0.0370 | - | - | 232 B | + | AddOrGet_then_Replace | 50 | 85.13 ns | 1.755 ns | 2.573 ns | 0.94 | 0.03 | 0.0370 | - | - | 232 B | + | AddOrGet_then_Replace_inlined_without_lambda_cost | 50 | 85.02 ns | 1.775 ns | 1.823 ns | 0.93 | 0.03 | 0.0370 | - | - | 232 B | + + */ + [Params(1, 5, 10)]//, 50, 100, 1_000)] + public int Count; + + [GlobalSetup] + public void Populate() + { + _map = V3_ImHashMap_AddOrUpdate(); + } - [Benchmark(Baseline = true)] - public object Get_then_AddOrUpdate() - { - var key = typeof(GlobalSetupAttribute); - var hash = key.GetHashCode(); - var val = "!"; - var m = _map; - var s = m.GetValueOrDefault(hash, key); - if (s != null) - s = Handle(s, val); - else - s = val; - - return m.AddOrUpdate(hash, key, s); - } + [Benchmark(Baseline = true)] + public object Get_then_AddOrUpdate() + { + var key = typeof(GlobalSetupAttribute); + var hash = key.GetHashCode(); + var val = "!"; + var m = _map; + var s = m.GetValueOrDefault(hash, key); + if (s != null) + s = Handle(s, val); + else + s = val; + + return m.AddOrUpdate(hash, key, s); + } - [Benchmark] - public object AddOrGet_then_Replace() - { - var key = typeof(GlobalSetupAttribute); - var hash = key.GetHashCode(); - var val = "!"; + [Benchmark] + public object AddOrGet_then_Replace() + { + var key = typeof(GlobalSetupAttribute); + var hash = key.GetHashCode(); + var val = "!"; - return _map.AddOrUpdate(hash, key, val, (_, o, n) => Handle(o, n)); - } + return _map.AddOrUpdate(hash, key, val, (_, o, n) => Handle(o, n)); + } - [MethodImpl(MethodImplOptions.NoInlining)] - static string Handle(string a, string b) => a + b; + [MethodImpl(MethodImplOptions.NoInlining)] + static string Handle(string a, string b) => a + b; - private ImToolsV3.ImHashMap _map; - public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() - { - var map = ImToolsV3.ImHashMap.Empty; + private ImToolsV3.ImHashMap _map; + public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() + { + var map = ImToolsV3.ImHashMap.Empty; - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - return map; - } + return map; } } } diff --git a/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj b/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj index 68a3269a..aa4214c6 100644 --- a/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj +++ b/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net11.0;net10.0 x64 @@ -18,11 +18,11 @@ - - + + - - + + diff --git a/playground/ImTools.Benchmarks/MemoryOwnerVsHashMap.cs b/playground/ImTools.Benchmarks/MemoryOwnerVsHashMap.cs index b3909963..fa3a7b78 100644 --- a/playground/ImTools.Benchmarks/MemoryOwnerVsHashMap.cs +++ b/playground/ImTools.Benchmarks/MemoryOwnerVsHashMap.cs @@ -44,7 +44,7 @@ public class MemoryOwnerVsHashMap MemoryOwner MemoryOwner; ImHashMap ImHashMap; - SmallMap> SmallMap; + HSmallMap> SmallMap; [GlobalSetup] public void Setup() @@ -84,7 +84,7 @@ public object ImTools_populate() [Benchmark] public object SmallMap_populate() { - var imt = ImTools.SmallMap.New(); + var imt = ImTools.HSmallMap.New(); for (int i = 0; i < Count; i++) { // todo: @wip add AddSureNotPresent (use FHashMap for the reference) diff --git a/playground/ImTools.Benchmarks/Program.cs b/playground/ImTools.Benchmarks/Program.cs index 89a6ea8d..b76b82e5 100644 --- a/playground/ImTools.Benchmarks/Program.cs +++ b/playground/ImTools.Benchmarks/Program.cs @@ -1,50 +1,68 @@ -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Running; -namespace ImTools.Benchmarks +namespace ImTools.Benchmarks; + +class Program { - class Program + static void Main(string[] args) { - static void Main() - { - BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); + // With args: full BenchmarkSwitcher (filters, jobs, etc.) + // dotnet run -c Release -f net10.0 -- --filter *SmallGrrVsSmallList* + // Without args: default baseline benchmark for the hybrid list work. + // if (args is { Length: > 0 }) + // { + // BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + // return; + // } + + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + + // new SIO.Program().Run(); + + //var x = new ImMapBenchmarks.Populate { Count = 10 }; + //x.ImMapArray_AddOrUpdate(); + + //var b = new ImMapBenchmarks.Populate { Count = 10 }; + //b.ImMap_FixedData4(); + + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + + // BenchmarkRunner.Run(); - // new SIO.Program().Run(); + // BenchmarkRunner.Run(); + BenchmarkRunner.Run(); - //var x = new ImMapBenchmarks.Populate { Count = 10 }; - //x.ImMapArray_AddOrUpdate(); - //var b = new ImMapBenchmarks.Populate { Count = 10 }; - //b.ImMap_FixedData4(); + // var bm = new Playground.ImHashMapBenchmarks.Lookup(); + // bm.Count = 1000; + // bm.Populate(); + // var count = bm.FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent(); + // Console.WriteLine(count); - // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); + //BenchmarkRunner.Run(); - //BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + //BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - //BenchmarkRunner.Run(); + //BenchmarkRunner.Run(); - //BenchmarkRunner.Run(); - - //BenchmarkRunner.Run(); - //BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); + //BenchmarkRunner.Run(); + //BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); - //BenchmarkRunner.Run(); + //BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - } + // BenchmarkRunner.Run(); } } diff --git a/playground/ImTools.Benchmarks/SecretTetrisGame.html b/playground/ImTools.Benchmarks/SecretTetrisGame.html new file mode 100644 index 00000000..0b6661b1 --- /dev/null +++ b/playground/ImTools.Benchmarks/SecretTetrisGame.html @@ -0,0 +1,577 @@ + + + + + + Tetris + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + (Alt+r)
+ + (Alt+p)
+ + (Alt+w)
+ + (Alt+q)
+ + (Alt+e)
+ + (Alt+s)
+
+ + + + + \ No newline at end of file diff --git a/playground/ImTools.Benchmarks/SmallGrrVsSmallListBenchmarks.cs b/playground/ImTools.Benchmarks/SmallGrrVsSmallListBenchmarks.cs new file mode 100644 index 00000000..e695bf73 --- /dev/null +++ b/playground/ImTools.Benchmarks/SmallGrrVsSmallListBenchmarks.cs @@ -0,0 +1,65 @@ +using System; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; +using FastExpressionCompiler.ImTools; + +namespace ImTools.Benchmarks; + +/// +/// Random-access benchmark: SmallList (stack + single growing array) +/// vs SmallGrr (stack + stable segmented Grr) once enough items spill to heap. +/// Run under net10.0 host (project TFM). Heap spill starts after Stack8 capacity (8). +/// +[MemoryDiagnoser] +[SimpleJob(RunStrategy.Throughput, launchCount: 1, warmupCount: 3, iterationCount: 8)] +public class SmallGrrVsSmallListBenchmarks +{ + // Sizes where Rest/Grr is on the hot path (well past stack capacity). + [Params(256, 1024, 4096)] + public int Count; + + private int[] _indices = null!; + private SmallList> _smallList; + private SmallGrrPowerOf2, Stack8> _smallGrr; + + [GlobalSetup] + public void Setup() + { + _smallList = default; + _smallGrr = default; + + for (var i = 0; i < Count; i++) + { + _smallList.Add(i); + _smallGrr.Add(i); + } + + // Deterministic pseudo-random access pattern (mostly heap indices) + _indices = new int[Count * 4]; + var rng = new Random(42); + for (var i = 0; i < _indices.Length; i++) + _indices[i] = rng.Next(Count); + } + + [Benchmark(Baseline = true)] + public int SmallList_RandomAccess() + { + var sum = 0; + var idxs = _indices; + ref var list = ref _smallList; + for (var i = 0; i < idxs.Length; i++) + sum += list.GetSurePresentRef(idxs[i]); + return sum; + } + + [Benchmark] + public int SmallGrr_RandomAccess() + { + var sum = 0; + var idxs = _indices; + ref var list = ref _smallGrr; + for (var i = 0; i < idxs.Length; i++) + sum += list.GetSurePresentRef(idxs[i]); + return sum; + } +} diff --git a/playground/ImTools.Benchmarks/SmallMapVsHSmallMapBenchmarks.cs b/playground/ImTools.Benchmarks/SmallMapVsHSmallMapBenchmarks.cs new file mode 100644 index 00000000..81c28462 --- /dev/null +++ b/playground/ImTools.Benchmarks/SmallMapVsHSmallMapBenchmarks.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; +using Fec = FastExpressionCompiler.ImTools; +using Im = ImTools; + +namespace ImTools.Benchmarks; + +/// +/// FEC SmallMap (stack entries + Grr or single-array heap overflow) vs ImTools HSmallMap and Dictionary. +/// Run: dotnet run -c Release -f net10.0 -- --filter *SmallMapVsHSmallMap* [--runtimes net10.0 net11.0 net8.0] +/// +[MemoryDiagnoser] +[SimpleJob(RunStrategy.Throughput, launchCount: 1, warmupCount: 3, iterationCount: 10)] +[GroupBenchmarksBy(BenchmarkDotNet.Configs.BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class SmallMapVsHSmallMapBenchmarks +{ + // 4/8/16 are the stack-only zone of SmallMap16, 32+ spill to the heap and the metadata table. + [Params(4, 8, 16, 32, 128, 1024)] + public int Count; + + private int[] _keys = null!; + private int[] _missingKeys = null!; + + private Im.HSmallMap> _hMap; + private Fec.SmallMap16 _grr16; + private Fec.SmallMap16_SingleArrEntries _single16; + private Fec.SmallMap8 _grr8; + private Dictionary _dict = null!; + + [GlobalSetup] + public void Setup() + { + var rng = new Random(42); + var seen = new HashSet(); + _keys = new int[Count]; + for (var i = 0; i < Count;) + if (seen.Add(_keys[i] = rng.Next(1, int.MaxValue))) + ++i; + + _missingKeys = new int[Count]; + for (var i = 0; i < Count;) + { + var k = rng.Next(1, int.MaxValue); + if (seen.Add(k)) + _missingKeys[i++] = k; + } + + _hMap = Im.HSmallMap.New(); + _grr16 = new Fec.SmallMap16(); + _single16 = new Fec.SmallMap16_SingleArrEntries(); + _grr8 = new Fec.SmallMap8(); + _dict = new Dictionary(); + foreach (var k in _keys) + { + _hMap.AddOrUpdate(k, k); + Fec.SmallMap.AddOrUpdate(ref _grr16.Map, k, k); + Fec.SmallMap.AddOrUpdate(ref _single16.Map, k, k); + Fec.SmallMap.AddOrUpdate(ref _grr8.Map, k, k); + _dict[k] = k; + } + } + + #region Populate + + [BenchmarkCategory("Populate"), Benchmark(Baseline = true)] + public int Populate_HSmallMap() + { + var m = Im.HSmallMap.New(); + foreach (var k in _keys) + m.AddOrUpdate(k, k); + return m.Count; + } + + [BenchmarkCategory("Populate"), Benchmark] + public int Populate_SmallMap16_Grr() + { + var m = new Fec.SmallMap16(); + foreach (var k in _keys) + Fec.SmallMap.AddOrUpdate(ref m.Map, k, k); + return m.Map.Count; + } + + [BenchmarkCategory("Populate"), Benchmark] + public int Populate_SmallMap16_SingleArray() + { + var m = new Fec.SmallMap16_SingleArrEntries(); + foreach (var k in _keys) + Fec.SmallMap.AddOrUpdate(ref m.Map, k, k); + return m.Map.Count; + } + + [BenchmarkCategory("Populate"), Benchmark] + public int Populate_SmallMap8_Grr() + { + var m = new Fec.SmallMap8(); + foreach (var k in _keys) + Fec.SmallMap.AddOrUpdate(ref m.Map, k, k); + return m.Map.Count; + } + + [BenchmarkCategory("Populate"), Benchmark] + public int Populate_Dictionary() + { + var m = new Dictionary(); + foreach (var k in _keys) + m[k] = k; + return m.Count; + } + + #endregion + + #region Lookup hit + + [BenchmarkCategory("LookupHit"), Benchmark(Baseline = true)] + public int LookupHit_HSmallMap() + { + var sum = 0; + foreach (var k in _keys) + if (_hMap.TryGetValue(k, out var v)) sum += v; + return sum; + } + + [BenchmarkCategory("LookupHit"), Benchmark] + public int LookupHit_SmallMap16_Grr() + { + var sum = 0; + foreach (var k in _keys) + sum += Fec.SmallMap.GetValueOrDefault(ref _grr16.Map, k, -1); + return sum; + } + + [BenchmarkCategory("LookupHit"), Benchmark] + public int LookupHit_SmallMap16_SingleArray() + { + var sum = 0; + foreach (var k in _keys) + sum += Fec.SmallMap.GetValueOrDefault(ref _single16.Map, k, -1); + return sum; + } + + [BenchmarkCategory("LookupHit"), Benchmark] + public int LookupHit_SmallMap8_Grr() + { + var sum = 0; + foreach (var k in _keys) + sum += Fec.SmallMap.GetValueOrDefault(ref _grr8.Map, k, -1); + return sum; + } + + [BenchmarkCategory("LookupHit"), Benchmark] + public int LookupHit_Dictionary() + { + var sum = 0; + foreach (var k in _keys) + if (_dict.TryGetValue(k, out var v)) sum += v; + return sum; + } + + #endregion + + #region Lookup miss + + [BenchmarkCategory("LookupMiss"), Benchmark(Baseline = true)] + public int LookupMiss_HSmallMap() + { + var sum = 0; + foreach (var k in _missingKeys) + if (_hMap.TryGetValue(k, out var v)) sum += v; + return sum; + } + + [BenchmarkCategory("LookupMiss"), Benchmark] + public int LookupMiss_SmallMap16_Grr() + { + var sum = 0; + foreach (var k in _missingKeys) + sum += Fec.SmallMap.GetValueOrDefault(ref _grr16.Map, k, -1); + return sum; + } + + [BenchmarkCategory("LookupMiss"), Benchmark] + public int LookupMiss_SmallMap16_SingleArray() + { + var sum = 0; + foreach (var k in _missingKeys) + sum += Fec.SmallMap.GetValueOrDefault(ref _single16.Map, k, -1); + return sum; + } + + [BenchmarkCategory("LookupMiss"), Benchmark] + public int LookupMiss_SmallMap8_Grr() + { + var sum = 0; + foreach (var k in _missingKeys) + sum += Fec.SmallMap.GetValueOrDefault(ref _grr8.Map, k, -1); + return sum; + } + + [BenchmarkCategory("LookupMiss"), Benchmark] + public int LookupMiss_Dictionary() + { + var sum = 0; + foreach (var k in _missingKeys) + if (_dict.TryGetValue(k, out var v)) sum += v; + return sum; + } + + #endregion + + #region Enumerate by insertion order + + [BenchmarkCategory("Enumerate"), Benchmark(Baseline = true)] + public int Enumerate_HSmallMap() + { + var sum = 0; + var e = _hMap.Entries; + for (var i = 0; i < e.GetCount(); ++i) + sum += e.GetSurePresentEntryRef(i).Value; + return sum; + } + + [BenchmarkCategory("Enumerate"), Benchmark] + public int Enumerate_SmallMap16_Grr() + { + var sum = 0; + ref var m = ref _grr16.Map; + for (var i = 0; i < m.Count; ++i) + sum += m.GetSurePresentEntryRef(i).Value; + return sum; + } + + [BenchmarkCategory("Enumerate"), Benchmark] + public int Enumerate_SmallMap16_SingleArray() + { + var sum = 0; + ref var m = ref _single16.Map; + for (var i = 0; i < m.Count; ++i) + sum += m.GetSurePresentEntryRef(i).Value; + return sum; + } + + #endregion +} diff --git a/playground/ImTools.SimpleDIPlayground/ImTools.SimpleDIPlayground.csproj b/playground/ImTools.SimpleDIPlayground/ImTools.SimpleDIPlayground.csproj index 6d99787a..84b004a3 100644 --- a/playground/ImTools.SimpleDIPlayground/ImTools.SimpleDIPlayground.csproj +++ b/playground/ImTools.SimpleDIPlayground/ImTools.SimpleDIPlayground.csproj @@ -2,7 +2,8 @@ Exe - net8.0 + net11.0;net10.0;net8.0 + net11.0;net10.0;net8.0;net472 diff --git a/playground/ImTools.UnionCsVsFsBenchmarks/ImTools.UnionCsVsFsBenchmarks.fsproj b/playground/ImTools.UnionCsVsFsBenchmarks/ImTools.UnionCsVsFsBenchmarks.fsproj index 6782ec12..9cdc1311 100644 --- a/playground/ImTools.UnionCsVsFsBenchmarks/ImTools.UnionCsVsFsBenchmarks.fsproj +++ b/playground/ImTools.UnionCsVsFsBenchmarks/ImTools.UnionCsVsFsBenchmarks.fsproj @@ -2,7 +2,8 @@ Exe - net6.0 + net11.0;net10.0;net8.0 + net11.0;net10.0;net8.0;net472 diff --git a/playground/ImTools.UnionPlayground/ImTools.UnionPlayground.csproj b/playground/ImTools.UnionPlayground/ImTools.UnionPlayground.csproj index ffa088bc..c6c41996 100644 --- a/playground/ImTools.UnionPlayground/ImTools.UnionPlayground.csproj +++ b/playground/ImTools.UnionPlayground/ImTools.UnionPlayground.csproj @@ -2,8 +2,8 @@ Exe - net6.0 - net7.0 + net11.0;net10.0;net8.0 + net11.0;net10.0;net8.0;net472 diff --git a/playground/ImTools.UnionPlayground/Program.cs b/playground/ImTools.UnionPlayground/Program.cs index 26730758..9fb36077 100644 --- a/playground/ImTools.UnionPlayground/Program.cs +++ b/playground/ImTools.UnionPlayground/Program.cs @@ -10,18 +10,18 @@ class Program { static void Main() { - // Unnamed (anonymous) union is fast to declare and use + // Unnamed (anonymous) un is fast to declare and use var i = U.Of(42); var s = U.Of("hey"); WriteLine(i); WriteLine(s); - // You may create the union case directly via constructor, helpful for the cases like `U` or `U` + // You may create the un case directly via constructor, helpful for the cases like `U` or `U` var s2 = new U.case1("hsss"); WriteLine(s2); - // Typed union (check the definition below) is different from the unnamed `U`, e.g. `s = name;` won't compile + // Typed un (check the definition below) is different from the unnamed `U`, e.g. `s = name;` won't compile var name = BoolOrString.Of("Bob"); var flag = BoolOrString.Of(false); @@ -29,7 +29,7 @@ static void Main() WriteLine(SwitchOnCases(flag)); WriteLine(name.Match(f => "" + f, n => n)); - // Typed union with the Typed cases so you can pattern match on `case I name` or `I flag` + // Typed un with the Typed cases so you can pattern match on `case I name` or `I flag` var name2 = FlagOrName.Of(Name.Of("Alice")); var flag2 = FlagOrName.Of(Flag.Of(true)); @@ -48,13 +48,13 @@ static void Main() } } - // One-liner named union definition + // One-liner named un definition public sealed class BoolOrString : Union { } // A different type from the NamedBoolOrString public sealed class OtherBoolOrString : Union { } - // Typed union with the typed cases - now you can pattern match via `I` and `I` + // Typed un with the typed cases - now you can pattern match via `I` and `I` public sealed class FlagOrName : Union { } public sealed class Flag : Item { } public sealed class Name : Item { } @@ -62,7 +62,7 @@ public sealed class Name : Item { } public static class Usage { // note: Using T with constraint instead of FlagOrName.I interface improves the performance by avoiding boxing. - public static string SwitchOnCases(T x) where T : BoolOrString.union + public static string SwitchOnCases(T x) where T : BoolOrString.un { switch (x) { @@ -72,7 +72,7 @@ public static string SwitchOnCases(T x) where T : BoolOrString.union } } - public static string SwitchOnTypedItems(FlagOrName.union x) + public static string SwitchOnTypedItems(FlagOrName.un x) { // Refactoring friendly Named cases, with some performance price due the boxing - likely is not important for your case, // except you are designing a performance oriented data structure or being used in performance sensitive spot context. @@ -125,34 +125,34 @@ public sealed class Decrement : Item { } public sealed class MyList : Union, Unit, MyList.NonEmptyList> { public static readonly case1 Empty = new case1(Unit.unit); - public static case2 NonEmpty(T head, union tail) => new case2(new NonEmptyList(head, tail)); + public static case2 NonEmpty(T head, un tail) => new case2(new NonEmptyList(head, tail)); public readonly struct NonEmptyList { public readonly T Head; - public readonly union Tail; - public NonEmptyList(T head, union tail) => (Head, Tail) = (head, tail); + public readonly un Tail; + public NonEmptyList(T head, un tail) => (Head, Tail) = (head, tail); public override string ToString() => Head + "::" + Tail; } } public static class MyList { - public static MyList.case2 Push(this MyList.union list, T head) => MyList.NonEmpty(head, list); + public static MyList.case2 Push(this MyList.un list, T head) => MyList.NonEmpty(head, list); } // Less efficient, but less boilerplate recursive type - requires one heap reference per recursive type usage. public sealed class MyTree : Union, Unit, MyTree.NonEmptyTree> { - public sealed class NonEmptyTree : Box { } + public sealed class NonEmptyTree : Box { } public static readonly case1 Empty = new case1(Unit.unit); } public static class MyTree { - public static MyTree.union Of(MyTree.union left, T leaf, MyTree.union right) => + public static MyTree.un Of(MyTree.un left, T leaf, MyTree.un right) => MyTree.Of(MyTree.NonEmptyTree.Of((left, leaf, right))); - public static MyTree.union Leaf(T leaf) => Of(MyTree.Empty, leaf, MyTree.Empty); + public static MyTree.un Leaf(T leaf) => Of(MyTree.Empty, leaf, MyTree.Empty); } } diff --git a/playground/ImTools.UnionPlayground/Union.cs b/playground/ImTools.UnionPlayground/Union.cs index 0984a2d7..fc0f2d2d 100644 --- a/playground/ImTools.UnionPlayground/Union.cs +++ b/playground/ImTools.UnionPlayground/Union.cs @@ -162,10 +162,10 @@ public bool Equals(Box other) => public override string ToString() => UnionTools.ToString(Value, "box("); } -/// Unnamed discriminated union (with Empty name), shorter name for simplified inline usage +/// Unnamed discriminated un (with Empty name), shorter name for simplified inline usage public class U : Union { } -/// Discriminated union +/// Discriminated un public abstract class Union { /// To tag the cases with enum value for efficient pattern matching of required - @@ -182,23 +182,23 @@ public enum Tag : byte /// The naming is selected to start from the lower letter, cause we need to use the nested type. /// It is an unusual case, that's why using the __union__ will be fine to highlight this. // ReSharper disable once InconsistentNaming - public interface union + public interface un { /// The tag Tag Tag { get; } - /// Matches the union cases to the R value + /// Matches the un cases to the R value R Match(Func map1, Func map2); } /// Creates the respective case - public static union Of(T1 x) => new case1(x); + public static un Of(T1 x) => new case1(x); /// Creates the respective case - public static union Of(T2 x) => new case2(x); + public static un Of(T2 x) => new case2(x); /// Wraps the respective case - public readonly struct case1 : union, IEquatable, I + public readonly struct case1 : un, IEquatable, I { /// Implicit conversion public static implicit operator case1(T1 x) => new case1(x); @@ -232,7 +232,7 @@ public interface union } /// Wraps the respective case - public readonly struct case2 : union, IEquatable, I + public readonly struct case2 : un, IEquatable, I { /// Conversion public static implicit operator case2(T2 x) => new case2(x); @@ -273,17 +273,17 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -303,7 +303,7 @@ public struct case1 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -321,7 +321,7 @@ public struct case2 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -345,18 +345,18 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3, Case4 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3, Func map4); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); - public static union Of(T4 x) => new case4(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); + public static un Of(T4 x) => new case4(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -376,7 +376,7 @@ public struct case1 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -394,7 +394,7 @@ public struct case2 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -412,7 +412,7 @@ public struct case3 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case4 : union, IEquatable, I + public struct case4 : un, IEquatable, I { public static implicit operator case4(T4 x) => new case4(x); @@ -436,19 +436,19 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3, Case4, Case5 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3, Func map4, Func map5); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); - public static union Of(T4 x) => new case4(x); - public static union Of(T5 x) => new case5(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); + public static un Of(T4 x) => new case4(x); + public static un Of(T5 x) => new case5(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -468,7 +468,7 @@ public struct case1 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -486,7 +486,7 @@ public struct case2 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -504,7 +504,7 @@ public struct case3 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case4 : union, IEquatable, I + public struct case4 : un, IEquatable, I { public static implicit operator case4(T4 x) => new case4(x); @@ -522,7 +522,7 @@ public struct case4 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case5 : union, IEquatable, I + public struct case5 : un, IEquatable, I { public static implicit operator case5(T5 x) => new case5(x); @@ -546,20 +546,20 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3, Case4, Case5, Case6 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3, Func map4, Func map5, Func map6); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); - public static union Of(T4 x) => new case4(x); - public static union Of(T5 x) => new case5(x); - public static union Of(T6 x) => new case6(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); + public static un Of(T4 x) => new case4(x); + public static un Of(T5 x) => new case5(x); + public static un Of(T6 x) => new case6(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -579,7 +579,7 @@ public struct case1 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -597,7 +597,7 @@ public struct case2 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -615,7 +615,7 @@ public struct case3 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case4 : union, IEquatable, I + public struct case4 : un, IEquatable, I { public static implicit operator case4(T4 x) => new case4(x); @@ -633,7 +633,7 @@ public struct case4 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case5 : union, IEquatable, I + public struct case5 : un, IEquatable, I { public static implicit operator case5(T5 x) => new case5(x); @@ -651,7 +651,7 @@ public struct case5 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case6 : union, IEquatable, I + public struct case6 : un, IEquatable, I { public static implicit operator case6(T6 x) => new case6(x); @@ -675,21 +675,21 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3, Case4, Case5, Case6, Case7 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3, Func map4, Func map5, Func map6, Func map7); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); - public static union Of(T4 x) => new case4(x); - public static union Of(T5 x) => new case5(x); - public static union Of(T6 x) => new case6(x); - public static union Of(T7 x) => new case7(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); + public static un Of(T4 x) => new case4(x); + public static un Of(T5 x) => new case5(x); + public static un Of(T6 x) => new case6(x); + public static un Of(T7 x) => new case7(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -709,7 +709,7 @@ public struct case1 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -727,7 +727,7 @@ public struct case2 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -745,7 +745,7 @@ public struct case3 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case4 : union, IEquatable, I + public struct case4 : un, IEquatable, I { public static implicit operator case4(T4 x) => new case4(x); @@ -763,7 +763,7 @@ public struct case4 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case5 : union, IEquatable, I + public struct case5 : un, IEquatable, I { public static implicit operator case5(T5 x) => new case5(x); @@ -781,7 +781,7 @@ public struct case5 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case6 : union, IEquatable, I + public struct case6 : un, IEquatable, I { public static implicit operator case6(T6 x) => new case6(x); @@ -799,7 +799,7 @@ public struct case6 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case7 : union, IEquatable, I + public struct case7 : un, IEquatable, I { public static implicit operator case7(T7 x) => new case7(x); @@ -822,22 +822,22 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3, Case4, Case5, Case6, Case7, Case8 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3, Func map4, Func map5, Func map6, Func map7, Func map8); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); - public static union Of(T4 x) => new case4(x); - public static union Of(T5 x) => new case5(x); - public static union Of(T6 x) => new case6(x); - public static union Of(T7 x) => new case7(x); - public static union Of(T8 x) => new case8(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); + public static un Of(T4 x) => new case4(x); + public static un Of(T5 x) => new case5(x); + public static un Of(T6 x) => new case6(x); + public static un Of(T7 x) => new case7(x); + public static un Of(T8 x) => new case8(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -857,7 +857,7 @@ public struct case1 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -875,7 +875,7 @@ public struct case2 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -893,7 +893,7 @@ public struct case3 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case4 : union, IEquatable, I + public struct case4 : un, IEquatable, I { public static implicit operator case4(T4 x) => new case4(x); @@ -911,7 +911,7 @@ public struct case4 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case5 : union, IEquatable, I + public struct case5 : un, IEquatable, I { public static implicit operator case5(T5 x) => new case5(x); @@ -929,7 +929,7 @@ public struct case5 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case6 : union, IEquatable, I + public struct case6 : un, IEquatable, I { public static implicit operator case6(T6 x) => new case6(x); @@ -947,7 +947,7 @@ public struct case6 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case7 : union, IEquatable, I + public struct case7 : un, IEquatable, I { public static implicit operator case7(T7 x) => new case7(x); @@ -965,7 +965,7 @@ public struct case7 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case8 : union, IEquatable, I + public struct case8 : un, IEquatable, I { public static implicit operator case8(T8 x) => new case8(x); @@ -988,24 +988,24 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3, Case4, Case5, Case6, Case7, Case8, Case9 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3, Func map4, Func map5, Func map6, Func map7, Func map8, Func map9); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); - public static union Of(T4 x) => new case4(x); - public static union Of(T5 x) => new case5(x); - public static union Of(T6 x) => new case6(x); - public static union Of(T7 x) => new case7(x); - public static union Of(T8 x) => new case8(x); - public static union Of(T9 x) => new case9(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); + public static un Of(T4 x) => new case4(x); + public static un Of(T5 x) => new case5(x); + public static un Of(T6 x) => new case6(x); + public static un Of(T7 x) => new case7(x); + public static un Of(T8 x) => new case8(x); + public static un Of(T9 x) => new case9(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -1026,7 +1026,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -1045,7 +1045,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -1064,7 +1064,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case4 : union, IEquatable, I + public struct case4 : un, IEquatable, I { public static implicit operator case4(T4 x) => new case4(x); @@ -1083,7 +1083,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case5 : union, IEquatable, I + public struct case5 : un, IEquatable, I { public static implicit operator case5(T5 x) => new case5(x); @@ -1102,7 +1102,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case6 : union, IEquatable, I + public struct case6 : un, IEquatable, I { public static implicit operator case6(T6 x) => new case6(x); @@ -1121,7 +1121,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case7 : union, IEquatable, I + public struct case7 : un, IEquatable, I { public static implicit operator case7(T7 x) => new case7(x); @@ -1140,7 +1140,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case8 : union, IEquatable, I + public struct case8 : un, IEquatable, I { public static implicit operator case8(T8 x) => new case8(x); @@ -1159,7 +1159,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case9 : union, IEquatable, I + public struct case9 : un, IEquatable, I { public static implicit operator case9(T9 x) => new case9(x); @@ -1183,25 +1183,25 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3, Case4, Case5, Case6, Case7, Case8, Case9, Case10 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3, Func map4, Func map5, Func map6, Func map7, Func map8, Func map9, Func map10); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); - public static union Of(T4 x) => new case4(x); - public static union Of(T5 x) => new case5(x); - public static union Of(T6 x) => new case6(x); - public static union Of(T7 x) => new case7(x); - public static union Of(T8 x) => new case8(x); - public static union Of(T9 x) => new case9(x); - public static union Of(T10 x) => new case10(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); + public static un Of(T4 x) => new case4(x); + public static un Of(T5 x) => new case5(x); + public static un Of(T6 x) => new case6(x); + public static un Of(T7 x) => new case7(x); + public static un Of(T8 x) => new case8(x); + public static un Of(T9 x) => new case9(x); + public static un Of(T10 x) => new case10(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -1222,7 +1222,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -1241,7 +1241,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -1260,7 +1260,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case4 : union, IEquatable, I + public struct case4 : un, IEquatable, I { public static implicit operator case4(T4 x) => new case4(x); @@ -1279,7 +1279,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case5 : union, IEquatable, I + public struct case5 : un, IEquatable, I { public static implicit operator case5(T5 x) => new case5(x); @@ -1298,7 +1298,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case6 : union, IEquatable, I + public struct case6 : un, IEquatable, I { public static implicit operator case6(T6 x) => new case6(x); @@ -1317,7 +1317,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case7 : union, IEquatable, I + public struct case7 : un, IEquatable, I { public static implicit operator case7(T7 x) => new case7(x); @@ -1336,7 +1336,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case8 : union, IEquatable, I + public struct case8 : un, IEquatable, I { public static implicit operator case8(T8 x) => new case8(x); @@ -1355,7 +1355,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case9 : union, IEquatable, I + public struct case9 : un, IEquatable, I { public static implicit operator case9(T9 x) => new case9(x); @@ -1374,7 +1374,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case10 : union, IEquatable, I + public struct case10 : un, IEquatable, I { public static implicit operator case10(T10 x) => new case10(x); diff --git a/src/ImTools/ImTools.cs b/src/ImTools/ImTools.cs index 2d3400c5..343badcb 100644 --- a/src/ImTools/ImTools.cs +++ b/src/ImTools/ImTools.cs @@ -37,10 +37,12 @@ namespace ImTools; using System.Runtime.CompilerServices; // For [MethodImpl(AggressiveInlining)] #if NET7_0_OR_GREATER +using System.Numerics; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; #endif -using static SmallMap; +// using static SmallMap; #nullable disable @@ -2002,7 +2004,7 @@ public static void ForEach(this ImZipper z, Action effect) /// Entry containing the Value in addition to the Hash public abstract class ImHashMapEntry : ImHashMap.Entry { - /// The value. Maybe modified if you need the Ref{Value} semantics. + /// The value. Maybe modified if you need the Ref{Value} semantics. /// You may add the entry with the default Value to the map, and calculate and set it later (e.g. using the CAS). public V Value; @@ -6830,7 +6832,7 @@ public static S ForEach(this ImHashMap[] parts, S state, ActionConfiguration and the tools for the SmallMap and friends -public static class SmallMap +public static class HSmallMap { internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) internal const byte MinCapacityBits = 3; // 1 << 3 == 8 @@ -6840,18 +6842,19 @@ public static class SmallMap internal const byte MaxProbeCount = (1 << MaxProbeBits) - 1; internal const byte ProbeCountShift = 32 - MaxProbeBits; internal const int HashAndIndexMask = ~(MaxProbeCount << ProbeCountShift); + internal const int HashAndIndexMaskWithIndex = HashAndIndexMask | MaxProbeCount; /// Creates the map with the storage [MethodImpl((MethodImplOptions)256)] - public static SmallMap> New(byte capacityBitShift = 0) + public static HSmallMap> New(byte capacityBitShift = 0) where TEq : struct, IEq => - new SmallMap>(capacityBitShift); + new HSmallMap>(capacityBitShift); /// Creates the map with the storage [MethodImpl((MethodImplOptions)256)] - public static SmallMap> NewChunked(byte capacityBitShift = 0) + public static HSmallMap> NewChunked(byte capacityBitShift = 0) where TEq : struct, IEq => - new SmallMap>(capacityBitShift); + new HSmallMap>(capacityBitShift); /// Holds a single entry consisting of key and value. /// Value may be set or changed but the key is set in stone (by construction). @@ -6873,7 +6876,7 @@ public Entry(K key, V value) } /// Converts the packed hashes and entries into the human readable info for debugging visualization - public static DebugHashItem[] Explain(this SmallMap map) + public static DebugHashItem[] Explain(this HSmallMap map) where TEq : struct, IEq where TEntries : struct, IEntries { @@ -6907,16 +6910,16 @@ public static DebugHashItem[] Explain(this SmallMap ref Unsafe.Add(ref start, distance); + internal static ref int GetItemRef(ref int start, int distance) => ref Unsafe.Add(ref start, distance); #else - internal static ref int GetHashRef(ref int[] start, int distance) => ref start[distance]; + internal static ref int GetItemRef(ref int[] start, int distance) => ref start[distance]; #endif [MethodImpl((MethodImplOptions)256)] #if NET7_0_OR_GREATER - internal static int GetHash(ref int start, int distance) => Unsafe.Add(ref start, distance); + internal static int GetItem(ref int start, int distance) => Unsafe.Add(ref start, distance); #else - internal static int GetHash(ref int[] start, int distance) => start[distance]; + internal static int GetItem(ref int[] start, int distance) => start[distance]; #endif /// Uses Fibonacci hashing by multiplying the integer on the factor derived from the GoldenRatio @@ -7025,7 +7028,12 @@ public void SetTombstoneOrRemoveSurePresentEntry(int index) // 0b11111111 == 255, so the mask for the capacity is 255 internal const int ChunkCapacityMask = ChunkCapacity - 1; - // todo: @perf research on the similar growable indexed collection with append-to-end semantics + // todo: @perf recently (07-2025) a similar data structure was presented on BestSoftwareConference and + // has name Xar (extendible growable array with the stable references). + // I can implement O(1) random access by precalculating starting offsets of each chunk then using SIMD to compare the input index against offsets, + // e.g. `var offsets = Vector256.Create(16, 16+32, 16+32+64, 16+32+64+128,...); + // var matches = Vector256.ExtractMostSignificantBits(Vector256.Greater(Vector256.Create(Index), offsets)); + // var chunkIndex = BitOperations.TrailingZeroCount(matches);` /// The array of array buckets, where bucket is the fixed size. /// It enables adding the new bucket without for the new entries without reallocating the existing data. /// It may allow to drop the empty bucket as well, reclaiming the memory after remove. @@ -7214,8 +7222,8 @@ public class DebugProxy where TEq : struct, IEq where TEntries : struct, IEntries { - private readonly SmallMap _map; - internal DebugProxy(SmallMap map) => _map = map; + private readonly HSmallMap _map; + internal DebugProxy(HSmallMap map) => _map = map; public DebugHashItem[] PackedHashes => _map.Explain(); public TEntries Entries => _map.Entries; } @@ -7241,14 +7249,14 @@ public class DebugProxy /// For instance, for the `RefEq` the tombstone is . You may redefine it in the `IEq{K}.GetTombstone()` implementation. /// /// -[DebuggerTypeProxy(typeof(DebugProxy<,,,>))] +[DebuggerTypeProxy(typeof(HSmallMap.DebugProxy<,,,>))] [DebuggerDisplay("Count={Count}")] -public struct SmallMap : IReadOnlyCollection> +public struct HSmallMap : IReadOnlyCollection> where TEq : struct, IEq - where TEntries : struct, IEntries + where TEntries : struct, HSmallMap.IEntries { #if DEBUG - ProbesTracker _dbg; + HSmallMap.ProbesTracker _dbg; #endif private int _indexMask; @@ -7261,7 +7269,9 @@ public struct SmallMap : IReadOnlyCollection> private int[] _packedHashesAndIndexes; #pragma warning disable IDE0044 // it tries to make the _entries readonly but they should stay modifiable to prevent its defensive struct copying +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value private TEntries _entries; +#pragma warning restore CS0649 #pragma warning restore IDE0044 /// The capacity @@ -7277,17 +7287,120 @@ public struct SmallMap : IReadOnlyCollection> public TEntries Entries => _entries; /// Capacity calculates as `1 leftShift capacityBitShift` - public SmallMap(byte capacityBitShift) + public HSmallMap(byte capacityBitShift) { + capacityBitShift = capacityBitShift < HSmallMap.MinCapacityBits ? HSmallMap.MinCapacityBits : capacityBitShift; _indexMask = (1 << capacityBitShift) - 1; // the overflow tail to the hashes is the size of log2N where N==capacityBitShift, // it is probably fine to have the check for the overflow of capacity because it will be mis-predicted only once at the end of loop (it even rarely for the lookup) _packedHashesAndIndexes = new int[1 << capacityBitShift]; - _entries = default; _entries.Init(capacityBitShift); } +#if NET7_0_OR_GREATER + internal static readonly Vector256 InitialProbesVec = Vector256.Create(1, 2, 3, 4, 5, 6, 7, 8); + internal static readonly Vector256 VectorSizeVec = Vector256.Create(8); +#endif + + // todo: @wip @remove + /// Lookup for the key and get the associated value if the key is found + [MethodImpl((MethodImplOptions)256)] + public bool TryGetValue_SIMD(K key, out V value) + { + if (_packedHashesAndIndexes != null) + { + var hash = default(TEq).GetHashCode(key); + + var indexMask = _indexMask; + var hashMiddleMask = HSmallMap.HashAndIndexMask & ~indexMask; + var hashMiddle = hash & hashMiddleMask; + var hashIndex = hash & indexMask; + +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + var hashesAndIndexesVec = MemoryMarshal.Cast>(_packedHashesAndIndexes.AsSpan()); + + var vIndexMask = indexMask >> 3; + var vIndex = hashIndex >> 3; + + // Adjust probes if starting index is not aligned to 8 + // e.g. if hashIndex = 1 -> V(0, 1, 2, 3, 4, 5, 6, 7) to V(-1, 0, 1, 2, 3, 4, 5, 6) + var expectedProbeVec = Vector256.Subtract(InitialProbesVec, Vector256.Create(hashIndex & 7)); + + while (true) + { + var hVec = hashesAndIndexesVec[vIndex++ & vIndexMask]; + + // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions + var hProbeVec = Vector256.ShiftRightLogical(hVec, HSmallMap.ProbeCountShift); + var probeDistanceVec = Vector256.Subtract(hProbeVec, expectedProbeVec); + + for (var i = 0; i < Vector256.Count; i++) + { + var dist = probeDistanceVec.GetElement(i); + if (dist == 0) + { + if ((hVec.GetElement(i) & hashMiddleMask) == hashMiddle) + { + ref var e = ref _entries.GetSurePresentEntryRef(hVec.GetElement(i) & indexMask); + if (default(TEq).Equals(e.Key, key)) + { + value = e.Value; + return true; + } + } + } + else if (dist < 0) + { + value = default; + return false; + } + } + + // e.g. V(-1, 0, 1, 2, 3, 4, 5, 6) + 8 -> V(7, 8, 9, 10, 11, 12, 13, 14) + expectedProbeVec = Vector256.Add(expectedProbeVec, VectorSizeVec); + } + } +#endif +#if NET7_0_OR_GREATER + ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); +#else + var hashesAndIndexes = _packedHashesAndIndexes; +#endif + + var h = HSmallMap.GetItem(ref hashesAndIndexes, hashIndex); + + // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions + var probe = 1; + while (true) + { + var probeDistance = (h >>> HSmallMap.ProbeCountShift) - probe; + if (probeDistance == 0) + { + if ((h & hashMiddleMask) == hashMiddle) + { + ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); + if (default(TEq).Equals(e.Key, key)) + { + value = e.Value; + return true; + } + } + } + else if (probeDistance < 0) + break; + + h = HSmallMap.GetItem(ref hashesAndIndexes, ++hashIndex & indexMask); + ++probe; + } + } + + value = default; + return false; + } + /// Lookup for the key and get the associated value if the key is found [MethodImpl((MethodImplOptions)256)] public bool TryGetValue(K key, out V value) @@ -7297,7 +7410,7 @@ public bool TryGetValue(K key, out V value) var hash = default(TEq).GetHashCode(key); var indexMask = _indexMask; - var hashMiddleMask = HashAndIndexMask & ~indexMask; + var hashMiddleMask = HSmallMap.HashAndIndexMask & ~indexMask; var hashMiddle = hash & hashMiddleMask; var hashIndex = hash & indexMask; @@ -7307,25 +7420,30 @@ public bool TryGetValue(K key, out V value) var hashesAndIndexes = _packedHashesAndIndexes; #endif - var h = GetHash(ref hashesAndIndexes, hashIndex); + var h = HSmallMap.GetItem(ref hashesAndIndexes, hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - var probes = 1; - while ((h >>> ProbeCountShift) >= probes) + var probe = 1; + while (true) { - // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) + var probeDistance = (h >>> HSmallMap.ProbeCountShift) - probe; + if (probeDistance == 0) { - ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); - if (default(TEq).Equals(e.Key, key)) + if ((h & hashMiddleMask) == hashMiddle) { - value = e.Value; - return true; + ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); + if (default(TEq).Equals(e.Key, key)) + { + value = e.Value; + return true; + } } } + else if (probeDistance < 0) + break; - h = GetHash(ref hashesAndIndexes, ++hashIndex & indexMask); - ++probes; + h = HSmallMap.GetItem(ref hashesAndIndexes, ++hashIndex & indexMask); + ++probe; } } @@ -7349,7 +7467,7 @@ public int GetEntryIndex(K key) var hash = default(TEq).GetHashCode(key); var indexMask = _indexMask; - var hashMiddleMask = HashAndIndexMask & ~indexMask; + var hashMiddleMask = HSmallMap.HashAndIndexMask & ~indexMask; var hashMiddle = hash & hashMiddleMask; var hashIndex = hash & indexMask; @@ -7359,21 +7477,21 @@ public int GetEntryIndex(K key) var hashesAndIndexes = _packedHashesAndIndexes; #endif - var h = GetHash(ref hashesAndIndexes, hashIndex); + var h = HSmallMap.GetItem(ref hashesAndIndexes, hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var probes = 1; - while ((h >>> ProbeCountShift) >= probes) + while ((h >>> HSmallMap.ProbeCountShift) >= probes) { // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) + if (((h >>> HSmallMap.ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) { ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); if (default(TEq).Equals(e.Key, key)) return h & indexMask; } - h = GetHash(ref hashesAndIndexes, ++hashIndex & indexMask); + h = HSmallMap.GetItem(ref hashesAndIndexes, ++hashIndex & indexMask); ++probes; } } @@ -7399,10 +7517,10 @@ public ref V GetOrAddValueRef(K key) var entryCount = _entries.GetCount(); // if the free space is less than 1/8 of capacity (12.5%) then Resize - if (indexMask - entryCount <= (indexMask >>> MinFreeCapacityShift)) + if (indexMask - entryCount <= (indexMask >>> HSmallMap.MinFreeCapacityShift)) indexMask = ResizeHashes(indexMask); - var hashMiddleMask = HashAndIndexMask & ~indexMask; // todo: @perf put in its own field to avoid the redundant calculation + var hashMiddleMask = HSmallMap.HashAndIndexMask & ~indexMask; // todo: @perf put in its own field to avoid the redundant calculation var hashMiddle = hash & hashMiddleMask; var hashIndex = hash & indexMask; @@ -7411,14 +7529,14 @@ public ref V GetOrAddValueRef(K key) #else var hashesAndIndexes = _packedHashesAndIndexes; #endif - ref var h = ref GetHashRef(ref hashesAndIndexes, hashIndex); + ref var h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var probes = 1; - while ((h >>> ProbeCountShift) >= probes) + while ((h >>> HSmallMap.ProbeCountShift) >= probes) { // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) + if (((h >>> HSmallMap.ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) { ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); #if DEBUG @@ -7427,33 +7545,34 @@ public ref V GetOrAddValueRef(K key) if (default(TEq).Equals(e.Key, key)) return ref e.Value; } - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, ++hashIndex & indexMask); ++probes; } // 3. We did not find the hash and therefore the key, so insert the new entry var hRobinHooded = h; - h = (probes << ProbeCountShift) | hashMiddle | entryCount; + h = (probes << HSmallMap.ProbeCountShift) | hashMiddle | entryCount; // old entryCount is the new index of the added hash #if DEBUG _dbg.DebugCollectAndOutputProbes(probes, "Add"); #endif // 4. If the robin hooded hash is empty then we stop // 5. Otherwise we steal the slot with the smaller probes - probes = hRobinHooded >>> ProbeCountShift; + probes = hRobinHooded >>> HSmallMap.ProbeCountShift; while (hRobinHooded != 0) { - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); - if ((h >>> ProbeCountShift) < ++probes) + h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, ++hashIndex & indexMask); + if ((h >>> HSmallMap.ProbeCountShift) < ++probes) { #if DEBUG if (h != 0) - _dbg.RemoveProbes(h >>> ProbeCountShift); + _dbg.RemoveProbes(h >>> HSmallMap.ProbeCountShift); _dbg.DebugCollectAndOutputProbes(probes, "Add-RH"); #endif var tmp = h; - h = (probes << ProbeCountShift) | (hRobinHooded & HashAndIndexMask); + h = (probes << HSmallMap.ProbeCountShift) | (hRobinHooded & HSmallMap.HashAndIndexMask); + hRobinHooded = tmp; - probes = hRobinHooded >>> ProbeCountShift; + probes = hRobinHooded >>> HSmallMap.ProbeCountShift; } } return ref _entries.AppendEntryAndGetValueRef(key); @@ -7471,7 +7590,7 @@ public bool TryRemove(K key) var hash = default(TEq).GetHashCode(key); var indexMask = _indexMask; - var hashMiddleMask = ~indexMask & HashAndIndexMask; + var hashMiddleMask = ~indexMask & HSmallMap.HashAndIndexMask; var hashMiddle = hash & hashMiddleMask; var hashIndex = hash & indexMask; @@ -7480,16 +7599,16 @@ public bool TryRemove(K key) #else var hashesAndIndexes = _packedHashesAndIndexes; #endif - ref var h = ref GetHashRef(ref hashesAndIndexes, hashIndex); + ref var h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, hashIndex); var removed = false; // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var probes = 1; - while ((h >>> ProbeCountShift) >= probes) + while ((h >>> HSmallMap.ProbeCountShift) >= probes) { // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) + if (((h >>> HSmallMap.ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) { ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); if (default(TEq).Equals(e.Key, key)) @@ -7503,24 +7622,26 @@ public bool TryRemove(K key) break; } } - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, ++hashIndex & indexMask); ++probes; } if (!removed) return false; + var emptiedIndex = hashIndex & indexMask; ref var emptied = ref h; - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, ++hashIndex & indexMask); // move the next hash into the emptied slot until the next hash is empty or ideally positioned (hash is 0 or probe is 1) - while ((h >>> ProbeCountShift) > 1) + while ((h >>> HSmallMap.ProbeCountShift) > 1) { - emptied = (((h >>> ProbeCountShift) - 1) << ProbeCountShift) | (h & HashAndIndexMask); // decrease the probe count by one cause we moving the hash closer to the ideal index + // Decrease the probe count by one cause we moving the hash closer to the ideal index + emptied = (((h >>> HSmallMap.ProbeCountShift) - 1) << HSmallMap.ProbeCountShift) | (h & HSmallMap.HashAndIndexMask); h = 0; emptied = ref h; - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, ++hashIndex & indexMask); } return true; } @@ -7529,16 +7650,16 @@ internal int ResizeHashes(int indexMask) { if (indexMask == 0) { - _indexMask = (1 << MinCapacityBits) - 1; - _packedHashesAndIndexes = new int[1 << MinCapacityBits]; + _indexMask = (1 << HSmallMap.MinCapacityBits) - 1; + _packedHashesAndIndexes = new int[1 << HSmallMap.MinCapacityBits]; #if DEBUG - Debug.WriteLine($"[ResizeHashes] new empty hashes {1} -> {_packedHashesAndIndexes.Length}"); + Debug.WriteLine($"[ResizeHashes] new empty hashes {_indexMask + 1} -> {_packedHashesAndIndexes.Length}"); #endif - return (1 << MinCapacityBits) - 1; + return _indexMask; } var oldCapacity = indexMask + 1; - var newHashAndIndexMask = HashAndIndexMask & ~oldCapacity; + var newHashAndIndexMask = HSmallMap.HashAndIndexMask & ~oldCapacity; var newIndexMask = (indexMask << 1) | 1; var newHashesAndIndexes = new int[oldCapacity << 1]; @@ -7555,8 +7676,8 @@ internal int ResizeHashes(int indexMask) // Overflow segment is wrapped-around hashes // and! the hashes at the beginning robin hooded by the wrapped-around hashes var i = 0; - while ((oldHash >>> ProbeCountShift) > 1) - oldHash = GetHash(ref oldHashes, ++i); + while ((oldHash >>> HSmallMap.ProbeCountShift) > 1) + oldHash = HSmallMap.GetItem(ref oldHashes, ++i); var oldCapacityWithOverflowSegment = i + oldCapacity; while (true) @@ -7564,29 +7685,30 @@ internal int ResizeHashes(int indexMask) if (oldHash != 0) { // get the new hash index from the old one with the next bit equal to the `oldCapacity` - var indexWithNextBit = (oldHash & oldCapacity) | (((i + 1) - (oldHash >>> ProbeCountShift)) & indexMask); + var indexWithNextBit = (oldHash & oldCapacity) | (((i + 1) - (oldHash >>> HSmallMap.ProbeCountShift)) & indexMask); // no need for robin-hooding because we already did it for the old hashes and // now just sparcing the hashes into the new array which are already in order var probes = 1; - ref var newHash = ref GetHashRef(ref newHashes, indexWithNextBit); + ref var newHash = ref HSmallMap.GetItemRef(ref newHashes, indexWithNextBit); while (newHash != 0) { - newHash = ref GetHashRef(ref newHashes, ++indexWithNextBit & newIndexMask); + newHash = ref HSmallMap.GetItemRef(ref newHashes, ++indexWithNextBit & newIndexMask); ++probes; } - newHash = (probes << ProbeCountShift) | (oldHash & newHashAndIndexMask); + newHash = (probes << HSmallMap.ProbeCountShift) | (oldHash & newHashAndIndexMask); } if (++i >= oldCapacityWithOverflowSegment) break; - oldHash = GetHash(ref oldHashes, i & indexMask); + oldHash = HSmallMap.GetItem(ref oldHashes, i & indexMask); } #if DEBUG Debug.WriteLine($"[ResizeHashes] {oldCapacity} -> {newHashesAndIndexes.Length}"); _dbg.DebugReCollectAndOutputProbes(newHashesAndIndexes); #endif + _indexMask = _indexMask << 1 | 1; _packedHashesAndIndexes = newHashesAndIndexes; return newIndexMask; @@ -7597,16 +7719,16 @@ internal int ResizeHashes(int indexMask) public Enumerator GetEnumerator() => new(_entries); // prevents the boxing of the enumerator struct /// - IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// Enumerator of the entries in the order of their addition to the map - public struct Enumerator : IEnumerator> + public struct Enumerator : IEnumerator> { private int _index; - private Entry _current; + private HSmallMap.Entry _current; private readonly TEntries _entries; private int _countIncludingRemoved; internal Enumerator(TEntries entries) @@ -7638,7 +7760,7 @@ public bool MoveNext() } /// - public Entry Current => _current; + public HSmallMap.Entry Current => _current; object IEnumerator.Current => _current; void IEnumerator.Reset() diff --git a/src/ImTools/ImTools.csproj b/src/ImTools/ImTools.csproj index 4f0baaf4..e5631430 100644 --- a/src/ImTools/ImTools.csproj +++ b/src/ImTools/ImTools.csproj @@ -1,8 +1,7 @@ - net8.0;net6.0 - net8.0;net6.0;net45;netstandard2.0 - net7.0 + net11.0;net10.0;net8.0 + net11.0;net10.0;net8.0;net472;netstandard2.1 ImTools 5.0.0 diff --git a/test/ImTools.UnitTests/GrrTests.cs b/test/ImTools.UnitTests/GrrTests.cs new file mode 100644 index 00000000..e634363d --- /dev/null +++ b/test/ImTools.UnitTests/GrrTests.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; + +#if LIGHT_EXPRESSION +using FastExpressionCompiler.LightExpression.ImTools; +#else +using FastExpressionCompiler.ImTools; +#endif + +namespace ImTools.Experiments.UnitTests; + +[TestFixture] +public class GrrTests +{ + private static GrrPowerOf2> NewGrr(uint capacity = 0) + { + var g = default(GrrPowerOf2>); + g.Init(capacity); + return g; + } + + [Test] + public void Init_uses_min_capacity_16() + { + var g = NewGrr(0); + Assert.AreEqual(16, g.Capacity); + Assert.AreEqual(1, g.SegmentCount); + g.GetSurePresentRef(0) = 42; + Assert.AreEqual(42, g.GetSurePresentRef(0)); + } + + [Test] + public void Init_rounds_up_to_power_of_two() + { + var g = NewGrr(20); + Assert.AreEqual(32, g.Capacity); + } + + [Test] + public void DoubleCapacity_keeps_existing_values_and_refs_stable() + { + var g = NewGrr(16); + for (var i = 0; i < 16; i++) + g.GetSurePresentRef(i) = i + 1; + + ref var first = ref g.GetSurePresentRef(0); + var before = first; + + g.DoubleCapacity(); + Assert.AreEqual(32, g.Capacity); + Assert.AreEqual(2, g.SegmentCount); + + for (var i = 0; i < 16; i++) + Assert.AreEqual(i + 1, g.GetSurePresentRef(i)); + + Assert.AreEqual(before, first); + first = 99; + Assert.AreEqual(99, g.GetSurePresentRef(0)); + } + + [Test] + public void GrowCapacity_across_multiple_segments_and_random_access() + { + var g = NewGrr(16); + g.GrowCapacity(1000); + Assert.GreaterOrEqual(g.Capacity, 1000); + Assert.Greater(g.SegmentCount, 1); + + for (var i = 0; i < 1000; i++) + g.GetSurePresentRef(i) = i * 3; + + for (var i = 999; i >= 0; i -= 7) + Assert.AreEqual(i * 3, g.GetSurePresentRef(i)); + } + + [Test] + public void Indexing_matches_segment_layout_for_various_first_sizes() + { + foreach (var firstReq in new uint[] { 1, 16, 17, 32, 64, 100 }) + { + var g = NewGrr(firstReq); + var first = g.Capacity; + Assert.GreaterOrEqual(first, 16); + Assert.AreEqual(0, first & (first - 1)); // power of two + + while (g.Capacity < first * 16) + g.DoubleCapacity(); + + var n = g.Capacity; + for (var i = 0; i < n; i++) + g.GetSurePresentRef(i) = i; + + for (var i = 0; i < n; i++) + Assert.AreEqual(i, g.GetSurePresentRef(i), $"firstReq={firstReq}, i={i}, cap={n}"); + } + } + + [Test] + public void Re_Init_resets_segments() + { + var g = NewGrr(16); + g.DoubleCapacity(); + g.DoubleCapacity(); + Assert.Greater(g.SegmentCount, 1); + + g.Init(16); + Assert.AreEqual(16, g.Capacity); + Assert.AreEqual(1, g.SegmentCount); + } + + [Test] + public void Enumerator_visits_all_capacity_slots() + { + var g = NewGrr(16); + g.DoubleCapacity(); // capacity 32 + for (var i = 0; i < 32; i++) + g.GetSurePresentRef(i) = i; + + var list = new List(); + foreach (var x in g) + list.Add(x); + + Assert.AreEqual(32, list.Count); + for (var i = 0; i < 32; i++) + Assert.AreEqual(i, list[i]); + } + + [Test] + public void Enumerator_Reset_restarts() + { + var g = NewGrr(16); + for (var i = 0; i < 16; i++) + g.GetSurePresentRef(i) = i + 10; + + var e = g.GetEnumerator(); + Assert.IsTrue(e.MoveNext()); + Assert.AreEqual(10, e.Current); + e.Reset(); + Assert.IsTrue(e.MoveNext()); + Assert.AreEqual(10, e.Current); + } +} + +[TestFixture] +public class SmallGrrTests +{ + private static SmallGrrPowerOf2, Stack8> New() => default; + + [Test] + public void Add_stays_on_stack_until_capacity() + { + var s = New(); + for (var i = 0; i < 8; i++) + Assert.AreEqual(i, s.Add(i * 10)); + + Assert.AreEqual(8, s.Count); + Assert.AreEqual(0, s.Rest.Capacity); // never grew rest + + for (var i = 0; i < 8; i++) + Assert.AreEqual(i * 10, s.GetSurePresentRef(i)); + } + + [Test] + public void Add_overflow_goes_to_Grr_and_keeps_stack_stable() + { + var s = New(); + for (var i = 0; i < 8; i++) + s.Add(i); + + ref var stack0 = ref s.GetSurePresentRef(0); + for (var i = 8; i < 100; i++) + s.Add(i); + + Assert.AreEqual(100, s.Count); + Assert.GreaterOrEqual(s.Rest.Capacity, 100 - 8); + + for (var i = 0; i < 100; i++) + Assert.AreEqual(i, s[i]); + + // stack item ref still valid after heap growth + Assert.AreEqual(0, stack0); + stack0 = 777; + Assert.AreEqual(777, s.GetSurePresentRef(0)); + } + + [Test] + public void Heap_item_refs_remain_stable_across_Rest_growth() + { + var s = New(); + for (var i = 0; i < 20; i++) + s.Add(i * 2); + + ref var heapItem = ref s.GetSurePresentRef(10); // past stack + Assert.AreEqual(20, heapItem); + + // force more rest growth + for (var i = 20; i < 500; i++) + s.Add(i); + + Assert.AreEqual(20, heapItem); + heapItem = -1; + Assert.AreEqual(-1, s.GetSurePresentRef(10)); + } + + [Test] + public void Random_access_matches_SmallList_for_heap_heavy_counts() + { + var grr = New(); + var list = default(SmallList>); + + const int n = 1024; + for (var i = 0; i < n; i++) + { + grr.Add(i ^ 0x5a5a); + list.Add(i ^ 0x5a5a); + } + + var rng = new Random(123); + for (var t = 0; t < 5000; t++) + { + var i = rng.Next(n); + Assert.AreEqual(list.GetSurePresentRef(i), grr.GetSurePresentRef(i)); + } + } + + [Test] + public void Clear_and_reuse() + { + var s = New(); + for (var i = 0; i < 40; i++) + s.Add(i); + var restCap = s.Rest.Capacity; + s.Clear(); + Assert.AreEqual(0, s.Count); + Assert.AreEqual(restCap, s.Rest.Capacity); // segments kept + + s.Add(5); + Assert.AreEqual(1, s.Count); + Assert.AreEqual(5, s[0]); + } + + [Test] + public void Enumerator_and_TryGetIndex() + { + var s = New(); + for (var i = 0; i < 30; i++) + s.Add(i * 3); + + var sum = 0; + foreach (var x in s) + sum += x; + Assert.AreEqual(3 * 29 * 30 / 2, sum); + + Assert.AreEqual(10, s.TryGetIndex(30, default(FastExpressionCompiler.ImTools.IntEq))); + Assert.AreEqual(-1, s.TryGetIndex(1, default(FastExpressionCompiler.ImTools.IntEq))); + Assert.AreEqual(30, s.GetIndexOrAdd(999, default(FastExpressionCompiler.ImTools.IntEq))); + } + + [Test] + public void RemoveLast() + { + var s = New(); + s.Add(1); + s.Add(2); + s.RemoveLastSurePresentItem(); + Assert.AreEqual(1, s.Count); + Assert.AreEqual(1, s.GetLastSurePresentItem()); + } +} diff --git a/test/ImTools.UnitTests/ImTools.UnitTests.csproj b/test/ImTools.UnitTests/ImTools.UnitTests.csproj index 3110412e..40024d40 100644 --- a/test/ImTools.UnitTests/ImTools.UnitTests.csproj +++ b/test/ImTools.UnitTests/ImTools.UnitTests.csproj @@ -1,9 +1,8 @@ - net8.0 - net8.0;net6.0;net472 - net7.0 + net11.0;net10.0;net8.0 + net11.0;net10.0;net8.0;net472 diff --git a/test/ImTools.UnitTests/Playground/FHashMap11Tests.cs b/test/ImTools.UnitTests/Playground/FHashMap11Tests.cs new file mode 100644 index 00000000..065cde52 --- /dev/null +++ b/test/ImTools.UnitTests/Playground/FHashMap11Tests.cs @@ -0,0 +1,414 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; + +namespace ImTools.Experiments.UnitTests; + +using static FHashMap11; + +[TestFixture] +public class FHashMap11Tests +{ + internal static void Verify(FHashMap11 map, IEnumerable expectedKeys) + where TEq : struct, IEq + { + map.VerifyHashesAndKeysEq(Assert.True); + map.VerifyProbesAreFitRobinHood(Assert.Fail); + map.VerifyNoDuplicateKeys(key => Assert.Fail($"Duplicate key: {key}")); + if (expectedKeys != null) + map.VerifyContainAllKeys(expectedKeys, (contains, key) => Assert.True(contains, $"Key not found:`{key}`")); + } + + [Test] + public void Real_world_test_AddOrUpdate() + { + var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(100).ToArray(); + + var map = new FHashMap11>(); + + foreach (var key in types) + map.AddOrUpdate(key, "a"); + + map.AddOrUpdate(typeof(Console), "!"); + + var found = map.TryGetValue(typeof(Console), out var value); + Assert.IsTrue(found); + Assert.AreEqual("!", value); + + Verify(map, types); + } + + [Test] + public void Real_world_test_AddOrUpdate_NO_Resize() + { + var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(100).ToArray(); + + var map = new FHashMap11>(8); + + foreach (var key in types) + map.AddOrUpdate(key, "a"); + + map.AddOrUpdate(typeof(FHashMap11Tests), "!"); + + Assert.AreEqual(101, map.Count); + + Verify(map, types); + } + + // [Test] + // public void Real_world_test_with_TryRemove_from_1000_items() + // { + // var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(1000).ToArray(); + + // var map = new FHashMap11>(); + + // foreach (var key in types) + // map.AddOrUpdate(key, "a"); + + // map.AddOrUpdate(typeof(FHashMap11Tests), "!"); + // Assert.AreEqual(1001, map.Count); + + // Assert.IsTrue(map.TryRemove(typeof(FHashMap11Tests))); + // Assert.AreEqual(1000, map.Count); + + // Verify(map, types); + // } + + // [Test] + // public void Real_world_test_with_Enumerator_and_TryRemove_the_entries() + // { + // var count = 1000; + // var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(count).ToList(); + // Assert.AreEqual(count, types.Count); + + // var map = new FHashMap11>(); + + // foreach (var key in types) + // map.AddOrUpdate(key, "a"); + + // var keys = map.Select(kv => kv.Key).ToList(); + // CollectionAssert.AreEquivalent(types, keys); + + // Assert.IsTrue(map.TryRemove(types[0])); + // Assert.IsTrue(map.TryRemove(types[999])); + // Assert.IsTrue(map.TryRemove(types[377])); + // Assert.IsTrue(map.TryRemove(types[733])); + // Assert.AreEqual(count - 4, map.Count); + + // // remove in the reverse order to keep the correct index in regard to map + // types.RemoveAt(999); + // types.RemoveAt(733); + // types.RemoveAt(377); + // types.RemoveAt(0); + // Assert.AreEqual(count - 4, types.Count); + + // // Check the second enumeration is working + // var keys2 = map.Select(kv => kv.Key).ToList(); + // CollectionAssert.AreEquivalent(types, keys2); + + // Verify(map, types); + // } + + // [Test] + // public void Simplified_test_with_equal_hashes_RefEq() + // { + // var map = new FHashMap11>(); + + // var keys = new[] { typeof(Tuple<>), typeof(Tuple<,>), typeof(Tuple<,,>) }; + // var i = 1; + // foreach (var k in keys) + // map.AddOrUpdate(k, "" + i++); + + // Assert.AreEqual(3, map.Count); + + // Assert.IsTrue(map.TryRemove(typeof(Tuple<,,>))); + // Assert.AreEqual(2, map.Count); + + // Verify(map, new[] { typeof(Tuple<>), typeof(Tuple<,>) }); + // } + + [Test] + public void Can_store_and_retrieve_value_from_map() + { + var map = new FHashMap11(2); + + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(42 + 32, "2"); + map.AddOrUpdate(42 + 32 + 32, "3"); + + // interrupt the keys with ne key + map.AddOrUpdate(43, "a"); + map.AddOrUpdate(43 + 32, "b"); + map.AddOrUpdate(43 + 32 + 32, "c"); + + map.AddOrUpdate(42 + 32 + 32 + 32, "4"); + + // insert 3rd variety of the keys + map.AddOrUpdate(44, "*"); + + map.AddOrUpdate(42 + 32 + 32 + 32 + 32, "5"); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32, "6"); + map.AddOrUpdate(43 + 32 + 32 + 32, "d"); + + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32, "7"); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, "8"); + + // check for the missing key + Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32)); + + // check for the strange key + Assert.AreEqual("*", map.GetValueOrDefault(44)); + + Assert.AreEqual("1", map.GetValueOrDefault(42)); + Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); + Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); + Assert.AreEqual("4", map.GetValueOrDefault(42 + 32 + 32 + 32)); + Assert.AreEqual("5", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32)); + Assert.AreEqual("6", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32)); + Assert.AreEqual("7", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32)); + Assert.AreEqual("8", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32)); + + Assert.AreEqual("a", map.GetValueOrDefault(43)); + Assert.AreEqual("b", map.GetValueOrDefault(43 + 32)); + Assert.AreEqual("c", map.GetValueOrDefault(43 + 32 + 32)); + Assert.AreEqual("d", map.GetValueOrDefault(43 + 32 + 32 + 32)); + + Assert.AreEqual(13, map.Count); + + Verify(map, null); + } + + /* + ## Debug output example + + ### IntEq + + [AddOrUpdate] Probes abs max=2, max=2, all=[1: 1, 2: 1]; first 4 probes are 2 out of 2 + [AddOrUpdate] Probes abs max=3, max=3, all=[1: 1, 2: 1, 3: 1]; first 4 probes are 3 out of 3 + [AllocateEntries] Resize entries: 2 -> 4 + [ResizeHashes] 4 -> 8 + [ResizeHashes] Probes abs max=3, max=3, all=[1: 1, 2: 1, 3: 1]; first 4 probes are 3 out of 3 + [AddOrUpdate] Probes abs max=4, max=4, all=[1: 1, 2: 1, 3: 2, 4: 1]; first 4 probes are 5 out of 5 + [AllocateEntries] Resize entries: 4 -> 8 + [AddOrUpdate] Probes abs max=5, max=5, all=[1: 1, 2: 1, 3: 2, 4: 1, 5: 1]; first 4 probes are 5 out of 6 + [AddOrUpdate-RH] Probes abs max=6, max=6, all=[1: 1, 2: 1, 3: 2, 4: 2, 5: 1, 6: 1]; first 4 probes are 6 out of 8 + [ResizeHashes] 8 -> 16 + [ResizeHashes] Probes abs max=6, max=6, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 1, 6: 1]; first 4 probes are 5 out of 7 + [AllocateEntries] Resize entries: 8 -> 16 + [AddOrUpdate-RH] Probes abs max=7, max=7, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 1, 7: 1]; first 4 probes are 5 out of 9 + [AddOrUpdate-RH] Probes abs max=8, max=8, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 1, 8: 1]; first 4 probes are 5 out of 11 + [AddOrUpdate] Probes abs max=9, max=9, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 1, 8: 2, 9: 1]; first 4 probes are 5 out of 13 + [AddOrUpdate-RH] Probes abs max=10, max=10, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 2, 8: 2, 9: 1, 10: 1]; first 4 probes are 5 out of 15 + [AddOrUpdate-RH] Probes abs max=11, max=11, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 2, 8: 3, 9: 1, 10: 1, 11: 1]; first 4 probes are 5 out of 17 + + ### GoldenIntEq + + [AddOrUpdate] Probes abs max=2, max=2, all=[1: 1, 2: 1]; first 4 probes are 2 out of 2 + [AllocateEntries] Resize entries: 2 -> 4 + [ResizeHashes] 4 -> 8 + [ResizeHashes] Probes abs max=2, max=1, all=[1: 3]; first 4 probes are 3 out of 3 + [AllocateEntries] Resize entries: 4 -> 8 + [AddOrUpdate] Probes abs max=2, max=2, all=[1: 5, 2: 1]; first 4 probes are 6 out of 6 + [ResizeHashes] 8 -> 16 + [ResizeHashes] Probes abs max=2, max=2, all=[1: 6, 2: 1]; first 4 probes are 7 out of 7 + [AllocateEntries] Resize entries: 8 -> 16 + + */ + [Test] + public void Can_store_and_retrieve_value_from_map_Golden() + { + var map = new FHashMap11(2); + // var map = new FHashMap11(2); + + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(42 + 32, "2"); + map.AddOrUpdate(42 + 32 + 32, "3"); + + // interrupt the keys with new key + map.AddOrUpdate(43, "a"); + map.AddOrUpdate(43 + 32, "b"); + map.AddOrUpdate(43 + 32 + 32, "c"); + + map.AddOrUpdate(42 + 32 + 32 + 32, "4"); + + // insert 3rd variety of the keys + map.AddOrUpdate(44, "*"); + + map.AddOrUpdate(42 + 32 + 32 + 32 + 32, "5"); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32, "6"); + map.AddOrUpdate(43 + 32 + 32 + 32, "d"); + + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32, "7"); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, "8"); + + // check for the missing key + Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32)); + + // check for the strange key + Assert.AreEqual("*", map.GetValueOrDefault(44)); + + Assert.AreEqual("1", map.GetValueOrDefault(42)); + Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); + Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); + Assert.AreEqual("4", map.GetValueOrDefault(42 + 32 + 32 + 32)); + Assert.AreEqual("5", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32)); + Assert.AreEqual("6", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32)); + Assert.AreEqual("7", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32)); + Assert.AreEqual("8", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32)); + + Assert.AreEqual("a", map.GetValueOrDefault(43)); + Assert.AreEqual("b", map.GetValueOrDefault(43 + 32)); + Assert.AreEqual("c", map.GetValueOrDefault(43 + 32 + 32)); + Assert.AreEqual("d", map.GetValueOrDefault(43 + 32 + 32 + 32)); + + Assert.AreEqual(13, map.Count); + + Verify(map, null); + } + + [Test] + public void Can_lookup_the_default_map_without_error() + { + FHashMap11 map = default; + + Assert.IsFalse(map.TryGetValue(42, out _)); + } + + [Test] + public void Can_store_and_retrieve_value_from_map_with_Expand_in_the_middle() + { + var map = new FHashMap11(1); + + Assert.IsFalse(map.TryGetValue(42, out _)); + + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(42 + 32, "2"); + + // interrupt the keys with new key + map.AddOrUpdate(43, "a"); + map.AddOrUpdate(43 + 32, "b"); + + map.AddOrUpdate(42 + 32 + 32, "3"); + + Assert.AreEqual("1", map.GetValueOrDefault(42)); + Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); + Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); + Assert.AreEqual(null, map.GetValueOrDefault(42 + 32 + 32 + 32)); + Assert.AreEqual("a", map.GetValueOrDefault(43)); + + map.AddOrUpdate(43, "a!"); + Assert.AreEqual("a!", map.GetValueOrDefault(43)); + + map.AddOrUpdate(47, "x"); + map.AddOrUpdate(53, "y"); + Assert.AreEqual("x", map.GetValueOrDefault(47)); + Assert.AreEqual("y", map.GetValueOrDefault(53)); + + map.AddOrUpdate(47 + 16, "x!"); + map.AddOrUpdate(53 + 16, "y!"); + Assert.AreEqual("x!", map.GetValueOrDefault(47 + 16)); + Assert.AreEqual("y!", map.GetValueOrDefault(53 + 16)); + + Verify(map, null); + } + + [Test] + public void Can_resize_without_moving() + { + var map = new FHashMap11(2); + + map.AddOrUpdate(0, "0"); + map.AddOrUpdate(1, "1"); + map.AddOrUpdate(9, "9"); + + // resize goes here + map.AddOrUpdate(3, "3"); + + map.AddOrUpdate(5, "5"); + + Verify(map, new[] { 0, 1, 3, 5, 9 }); + } + + [Test] + public void Can_store_and_get_stored_item_count() + { + var map = new FHashMap11(); + + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(42 + 32 + 32, "3"); + + Assert.AreEqual(2, map.Count); + Verify(map, new[] { 42, 42 + 32 + 32 }); + } + + [Test] + public void Can_update_a_stored_item_with_new_value() + { + var map = new FHashMap11(); + + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(42, "3"); + + Assert.AreEqual("3", map.GetValueOrDefault(42)); + Assert.AreEqual(1, map.Count); + Verify(map, new[] { 42 }); + } + + [Test] + public void Can_add_key_with_0_hash_code() + { + var map = new FHashMap11(); + + map.AddOrUpdate(0, "aaa"); + map.AddOrUpdate(0 + 32, "2"); + map.AddOrUpdate(0 + 32 + 32, "3"); + Verify(map, new[] { 0, 0 + 32, 0 + 32 + 32 }); + + string value; + Assert.IsTrue(map.TryGetValue(0, out value)); + + Assert.AreEqual("aaa", value); + } + + [Test] + public void Can_quickly_find_the_scattered_items_with_the_same_cache() + { + var map = new FHashMap11(); + + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(43, "a"); + map.AddOrUpdate(42 + 32, "2"); + map.AddOrUpdate(45, "b"); + map.AddOrUpdate(46, "c"); + map.AddOrUpdate(42 + 32 + 32, "3"); + Verify(map, new[] { 42, 43, 42 + 32, 45, 46, 42 + 32 + 32 }); + + string value; + Assert.IsTrue(map.TryGetValue(42 + 32, out value)); + Assert.AreEqual("2", value); + + Assert.IsTrue(map.TryGetValue(42 + 32 + 32, out value)); + Assert.AreEqual("3", value); + } + + // [Test] + // public void Can_remove_the_stored_item() + // { + // var map = new FHashMap11(2); + + // map.AddOrUpdate(42, "1"); + // map.AddOrUpdate(42 + 32, "2"); + // map.AddOrUpdate(42 + 32 + 32, "3"); + + // Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); + // var r = map.TryRemove(42 + 32); + // Assert.IsTrue(r); + + // Assert.AreEqual(2, map.Count); + // Assert.AreEqual("1", map.GetValueOrDefault(42)); + // Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); + // Verify(map, null); + // } +} diff --git a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs new file mode 100644 index 00000000..f886da3b --- /dev/null +++ b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs @@ -0,0 +1,441 @@ +#define VERIFY_MAP +#if NET6_0_OR_GREATER +#define CS_CHECK +#endif + +namespace FastExpressionCompiler.ImTools.UnitTests; + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using static SmallMap; +#if CS_CHECK +using CsCheck; +#endif + +/// +/// Baseline tests for the hybrid SmallMap (stack entries + Grr/SingleArray heap + Grr metadata). +/// Removal/TryRemove is intentionally not covered yet - not implemented in this baseline. +/// +[TestFixture] +public class FecSmallMapTests +{ + private static readonly Type[] _allKeys = typeof(Dictionary<,>).Assembly.GetTypes().Take(2000).ToArray(); + + private static void AssertVerifyKeys(ref TMap map, IEnumerable expectedKeys = null) + where TMap : struct, IMap, IMapImpl + { +#if VERIFY_MAP + map.Verify(static (cond, msg) => Assert.IsTrue(cond, msg), expectedKeys, Pass.It); +#endif + } + + [Test] + public void Zero_0_hash_test() + { + var m = new SmallMap16(); + ref var map = ref m.Map; + map.AddOrUpdate(1, 1); + Assert.AreEqual(1, map.Count); + + Assert.False(map.ContainsKey(0)); + map.AddOrUpdate(0, 0); + Assert.AreEqual(2, map.Count); + Assert.True(map.ContainsKey(0)); + Assert.AreEqual(0, map.GetValueOrDefault(0, 0)); + } + + [Test] + public void Can_store_and_retrieve_value_from_map() + { + var m = new SmallMap4(); + ref var map = ref m.Map; + + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(42 + 32, "2"); + map.AddOrUpdate(42 + 32 + 32, "3"); + + map.AddOrUpdate(43, "a"); + map.AddOrUpdate(43 + 32, "b"); + map.AddOrUpdate(43 + 32 + 32, "c"); + + map.AddOrUpdate(42 + 32 + 32 + 32, "4"); + map.AddOrUpdate(44, "*"); + + map.AddOrUpdate(42 + 32 + 32 + 32 + 32, "5"); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32, "6"); + map.AddOrUpdate(43 + 32 + 32 + 32, "d"); + + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32, "7"); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, "8"); + + AssertVerifyKeys(ref map, (IEnumerable)null); + + Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32, default(string))); + Assert.AreEqual("*", map.GetValueOrDefault(44, default(string))); + + Assert.AreEqual("1", map.GetValueOrDefault(42, default(string))); + Assert.AreEqual("2", map.GetValueOrDefault(42 + 32, default(string))); + Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32, default(string))); + Assert.AreEqual("4", map.GetValueOrDefault(42 + 32 + 32 + 32, default(string))); + Assert.AreEqual("5", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32, default(string))); + Assert.AreEqual("6", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32, default(string))); + Assert.AreEqual("7", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32, default(string))); + Assert.AreEqual("8", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, default(string))); + + Assert.AreEqual("a", map.GetValueOrDefault(43, default(string))); + Assert.AreEqual("b", map.GetValueOrDefault(43 + 32, default(string))); + Assert.AreEqual("c", map.GetValueOrDefault(43 + 32 + 32, default(string))); + Assert.AreEqual("d", map.GetValueOrDefault(43 + 32 + 32 + 32, default(string))); + + Assert.AreEqual(13, map.Count); + } + + [Test] + public void Update_existing_key_keeps_count() + { + var m = new SmallMap8(); + ref var map = ref m.Map; + + Assert.IsFalse(map.AddOrUpdate(7, "a")); + Assert.IsTrue(map.AddOrUpdate(7, "b")); + Assert.AreEqual(1, map.Count); + Assert.AreEqual("b", map.GetValueOrDefault(7, default(string))); + AssertVerifyKeys(ref map, new[] { 7 }); + } + + [Test] + public void Default_map_lookups_are_safe() + { + // Truly default (uninitialized) map: Count/Capacity 0, no heap metadata yet. + var m = default(SmallMap16); + ref var map = ref m.Map; + Assert.AreEqual(0, map.Count); + Assert.IsFalse(map.ContainsKey(42)); + Assert.AreEqual(null, map.GetValueOrDefault(42, default(string))); + map.TryGetEntryRef(42, out var found); + Assert.IsFalse(found); + // Do not dereference the returned ref when found is false (null ref). + } + + [Test] + public void Stack_to_heap_migration_and_entry_order() + { + var m = new SmallMap4(); + ref var map = ref m.Map; + + const int n = 40; + for (var i = 0; i < n; i++) + map.AddOrUpdate(i, i * 10); + + Assert.AreEqual(n, map.Count); + Assert.Greater(map.Capacity, 0); + Assert.Greater(map.HeapEntries.Capacity, 0); + + for (var i = 0; i < n; i++) + { + Assert.AreEqual(i, map.GetSurePresentKey(i)); + Assert.AreEqual(i * 10, map.GetSurePresentEntryRef(i).Value); + Assert.AreEqual(i * 10, map.GetValueOrDefault(i, 0)); + } + + AssertVerifyKeys(ref map, Enumerable.Range(0, n)); + } + + [Test] + public void Heap_entry_refs_remain_stable_with_Grr_backing() + { + var m = new SmallMap4(); + ref var map = ref m.Map; + + for (var i = 0; i < 8; i++) + map.AddOrUpdate(i, i); + + ref var heapEntry = ref map.GetSurePresentEntryRef(4); + Assert.AreEqual(4, heapEntry.Key); + Assert.AreEqual(4, heapEntry.Value); + + for (var i = 8; i < 200; i++) + map.AddOrUpdate(i, i); + + Assert.AreEqual(4, heapEntry.Key); + Assert.AreEqual(4, heapEntry.Value); + heapEntry.Value = -1; + Assert.AreEqual(-1, map.GetSurePresentEntryRef(4).Value); + Assert.AreEqual(-1, map.GetValueOrDefault(4, 0)); + + AssertVerifyKeys(ref map, (IEnumerable)null); + } + + [Test] + public void SingleBackingArray_map_behaves_the_same_for_lookup() + { + var m = new SmallMap16_SingleArrEntries(); + ref var map = ref m.Map; + + for (var i = 0; i < 100; i++) + map.AddOrUpdate(i, "v" + i); + + for (var i = 0; i < 100; i++) + Assert.AreEqual("v" + i, map.GetValueOrDefault(i, default(string))); + + Assert.IsFalse(map.ContainsKey(1000)); + AssertVerifyKeys(ref map, Enumerable.Range(0, 100)); + } + + [Test] + public void AddSureAbsent_and_AddOrGet_paths() + { + var m = new SmallMap8(); + ref var map = ref m.Map; + + map.AddSureAbsentDefaultEntryAndGetRef(1).Value = 10; + map.AddSureAbsentDefaultEntryAndGetRef(2).Value = 20; + Assert.AreEqual(2, map.Count); + + ref var existing = ref map.AddOrGetEntryRef(1, out var found); + Assert.IsTrue(found); + Assert.AreEqual(10, existing.Value); + existing.Value = 11; + + ref var added = ref map.AddOrGetEntryRef(3, out found); + Assert.IsFalse(found); + added.Value = 30; + + Assert.AreEqual(3, map.Count); + Assert.AreEqual(11, map.GetValueOrDefault(1, 0)); + Assert.AreEqual(30, map.GetValueOrDefault(3, 0)); + AssertVerifyKeys(ref map, new[] { 1, 2, 3 }); + } + + [Test] + public void Constructor_capacity_is_honored_as_power_of_two_floor() + { + var m = new SmallMap16(100); + ref var map = ref m.Map; + Assert.GreaterOrEqual(map.Capacity, 16); + Assert.AreEqual(0, map.Capacity & (map.Capacity - 1)); + + for (var i = 0; i < 50; i++) + map.AddOrUpdate(i, i); + + Assert.AreEqual(50, map.Count); + AssertVerifyKeys(ref map, Enumerable.Range(0, 50)); + } + + [Test] + public void Set_stores_keys_only() + { + var s = new SmallSet8(); + ref var set = ref s.Set; + + set.AddOrGetEntryRef(5, out var found); + Assert.IsFalse(found); + set.AddOrGetEntryRef(5, out found); + Assert.IsTrue(found); + Assert.IsTrue(set.ContainsKey(5)); + Assert.IsFalse(set.ContainsKey(6)); + Assert.AreEqual(1, set.Count); + } + + [Test] + public void Benchmark_test_with_Add_and_Lookup_10_items() + { + const int Count = 10; + var m = new SmallMap16>(); + ref var map = ref m.Map; + + var presentKeys = _allKeys.Take(Count).ToArray(); + var missingKeys = _allKeys.Skip(1000).Take(Count).ToArray(); + var seed = new Random(42); + var randomPresentKeys = presentKeys.OrderBy(_ => seed.Next()).ToArray(); + + foreach (var key in presentKeys) + map.AddOrUpdate(key, "a"); + + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) + { + ref var entry = ref map.TryGetEntryRef(randomPresentKeys[i], out var found); + if (found) + count += entry.Value.Length; + _ = map.TryGetEntryRef(missingKeys[i], out found); + if (!found) + --count; + } + + Assert.AreEqual(0, count); + AssertVerifyKeys(ref map, presentKeys); + } + + [Test] + public void Benchmark_test_with_Add_and_Lookup_100_items() + { + const int Count = 100; + var m = new SmallMap16>(); + ref var map = ref m.Map; + + var presentTypes = _allKeys.Take(Count).ToArray(); + var missingKeys = _allKeys.Skip(1000).Take(Count).ToArray(); + var seed = new Random(42); + var randomPresentKeys = presentTypes.OrderBy(_ => seed.Next()).ToArray(); + + foreach (var type in presentTypes) + map.AddOrUpdate(type, "a"); + + AssertVerifyKeys(ref map, presentTypes); + + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) + { + ref var result = ref map.TryGetEntryRef(randomPresentKeys[i], out var found); + if (found) + count += result.Value.Length; + _ = ref map.TryGetEntryRef(missingKeys[i], out found); + if (!found) + --count; + } + Assert.AreEqual(0, count); + } + + [Test] + public void Benchmark_test_with_Add_and_Lookup_1000_items() + { + const int Count = 1000; + var m = new SmallMap16>(); + ref var map = ref m.Map; + + var presentTypes = _allKeys.Take(Count).ToArray(); + var missingKeys = _allKeys.Skip(1000).Take(Count).ToArray(); + var seed = new Random(42); + var randomPresentKeys = presentTypes.OrderBy(_ => seed.Next()).ToArray(); + + foreach (var type in presentTypes) + map.AddOrUpdate(type, "a"); + + AssertVerifyKeys(ref map, presentTypes); + + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) + { + ref var result = ref map.TryGetEntryRef(randomPresentKeys[i], out var found); + if (found) + count += result.Value.Length; + _ = ref map.TryGetEntryRef(missingKeys[i], out found); + if (!found) + --count; + } + Assert.AreEqual(0, count); + } + + [Test] + public void Colliding_hashes_still_roundtrip() + { + var m = new SmallMap8(); + ref var map = ref m.Map; + + var keys = new[] { 1, 1 + 16, 1 + 32, 1 + 48, 1 + 64, 1 + 80, 1 + 96, 1 + 112, 1 + 128, 1 + 144 }; + for (var i = 0; i < keys.Length; i++) + map.AddOrUpdate(keys[i], "v" + i); + + for (var i = 0; i < keys.Length; i++) + Assert.AreEqual("v" + i, map.GetValueOrDefault(keys[i], default(string))); + + Assert.AreEqual(keys.Length, map.Count); + AssertVerifyKeys(ref map, keys); + } + + [Test] + public void Multiple_resizes_keep_all_keys_findable() + { + // Force several metadata resizes (threshold ~7/8 full) past stack migration. + var m = new SmallMap4(); + ref var map = ref m.Map; + const int n = 5000; + for (var i = 0; i < n; i++) + map.AddOrUpdate(i, i); + + Assert.AreEqual(n, map.Count); + Assert.Greater(map.Capacity, 16); + for (var i = 0; i < n; i++) + Assert.AreEqual(i, map.GetValueOrDefault(i, -1)); + Assert.AreEqual(-1, map.GetValueOrDefault(n, -1)); + AssertVerifyKeys(ref map, Enumerable.Range(0, n)); + } + +#if CS_CHECK + [Test] + public void Check_AddOrUpdate_random_items_and_verify_all_added() + { + const int upperBound = 100_000; + Gen.Int[0, upperBound].Array.Sample(items => + { + var m = new SmallMap16(); + ref var map = ref m.Map; + foreach (var n in items) + { + map.AddOrUpdate(n, n); + Assert.AreEqual(n, map.GetValueOrDefault(n, -1)); + } + + foreach (var n in items) + Assert.AreEqual(n, map.GetValueOrDefault(n, -1)); + + Assert.AreEqual(-1, map.GetValueOrDefault(upperBound + 1, -1)); + Assert.AreEqual(-1, map.GetValueOrDefault(-1, -1)); + AssertVerifyKeys(ref map, items.Distinct()); + }, + iter: 2000); + } + + [Test] + public void Check_AddOrUpdate_unique_keys_count_and_lookup() + { + Gen.Int[0, 50_000].ArrayUnique.Sample(keys => + { + var m = new SmallMap8(); + ref var map = ref m.Map; + for (var i = 0; i < keys.Length; i++) + map.AddOrUpdate(keys[i], i); + + Assert.AreEqual(keys.Length, map.Count); + for (var i = 0; i < keys.Length; i++) + Assert.AreEqual(i, map.GetValueOrDefault(keys[i], -1)); + + AssertVerifyKeys(ref map, keys); + }, + iter: 1500); + } + + [Test] + public void Check_colliding_int_keys_across_resizes() + { + // Same low bits (ideal index) force long RH chains and wrap cases before/after resize. + Gen.Int[1, 64].SelectMany(baseKey => + Gen.Int[8, 400].Select(n => (baseKey, n))) + .Sample(t => + { + var (baseKey, n) = t; + var m = new SmallMap4(); + ref var map = ref m.Map; + var keys = new int[n]; + for (var i = 0; i < n; i++) + { + // Step by growing powers of two so ideals collide under successive masks. + keys[i] = baseKey + i * 16; + map.AddOrUpdate(keys[i], i); + } + + Assert.AreEqual(n, map.Count); + for (var i = 0; i < n; i++) + Assert.AreEqual(i, map.GetValueOrDefault(keys[i], -1)); + AssertVerifyKeys(ref map, keys); + }, + iter: 1000); + } +#endif +} diff --git a/test/ImTools.UnitTests/Playground/MapSlimTests.cs b/test/ImTools.UnitTests/Playground/MapSlimTests.cs index 415931bb..ff3d700d 100644 --- a/test/ImTools.UnitTests/Playground/MapSlimTests.cs +++ b/test/ImTools.UnitTests/Playground/MapSlimTests.cs @@ -126,7 +126,7 @@ public void MapSlim_Performance_IndexOf() public void MapSlim_Performance_Increment() { Gen.Int[0, 255].Array - .Select(a => (a, SmallMap.New(), new Dictionary())) + .Select(a => (a, HSmallMap.New(), new Dictionary())) .Faster( (items, map, _) => { diff --git a/test/ImTools.UnitTests/SmallMapTests.cs b/test/ImTools.UnitTests/SmallMapTests.cs index 0486c8a4..3294f317 100644 --- a/test/ImTools.UnitTests/SmallMapTests.cs +++ b/test/ImTools.UnitTests/SmallMapTests.cs @@ -10,31 +10,17 @@ namespace ImTools.Experiments.UnitTests; -using static SmallMap; +using static HSmallMap; [TestFixture] public class SmallMapTests { -#if NET7_0_OR_GREATER - [Test] - public void Test_stackalloc_for_entries() - { - Span arr = stackalloc int[8]; - - Foo(in arr); - - Assert.AreEqual(42, arr[2]); - } - - static void Foo(in Span arr) => arr[2] = 42; -#endif - [Test] public void Real_world_test_AddOrUpdate() { var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(100).ToArray(); - var map = SmallMap.New>(); + var map = HSmallMap.New>(); foreach (var key in types) map.AddOrUpdate(key, "a"); @@ -52,7 +38,7 @@ public void Real_world_test_AddOrUpdate_NO_Resize() { var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(100).ToArray(); - var map = SmallMap.New>(8); + var map = HSmallMap.New>(8); foreach (var key in types) map.AddOrUpdate(key, "a"); @@ -69,7 +55,7 @@ public void Real_world_test_with_TryRemove_from_1000_items() { var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(1000).ToArray(); - var map = SmallMap.New>(); + var map = HSmallMap.New>(); foreach (var key in types) map.AddOrUpdate(key, "a"); @@ -88,7 +74,7 @@ public void Real_world_test_with_TryRemove_from_1000_items_TypeEq() { var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(1000).ToArray(); - var map = SmallMap.NewChunked>(); + var map = HSmallMap.NewChunked>(); foreach (var key in types) map.AddOrUpdate(key, "a"); @@ -109,7 +95,7 @@ public void Real_world_test_with_Enumerator_and_TryRemove_the_entries() var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(count).ToList(); Assert.AreEqual(count, types.Count); - var map = SmallMap.New>(); + var map = HSmallMap.New>(); foreach (var key in types) map.AddOrUpdate(key, "a"); @@ -144,7 +130,7 @@ public void Real_world_test_with_Enumerator_and_TryRemove_the_entries_ChunkedEnt var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(count).ToList(); Assert.AreEqual(count, types.Count); - var map = SmallMap.NewChunked>(); + var map = HSmallMap.NewChunked>(); foreach (var key in types) map.AddOrUpdate(key, "a"); @@ -175,7 +161,7 @@ public void Real_world_test_with_Enumerator_and_TryRemove_the_entries_ChunkedEnt [Test] public void Simplified_test_with_equal_hashes_RefEq() { - var map = SmallMap.New>(); + var map = HSmallMap.New>(); var keys = new[] { typeof(Tuple<>), typeof(Tuple<,>), typeof(Tuple<,,>) }; var i = 1; @@ -193,7 +179,7 @@ public void Simplified_test_with_equal_hashes_RefEq() [Test] public void Can_store_and_retrieve_value_from_map() { - var map = SmallMap.New(2); + var map = HSmallMap.New(2); map.AddOrUpdate(42, "1"); map.AddOrUpdate(42 + 32, "2"); @@ -280,7 +266,7 @@ [ResizeHashes] 8 -> 16 [Test] public void Can_store_and_retrieve_value_from_map_Golden() { - var map = SmallMap.New(2); + var map = HSmallMap.New(2); // var map = SmallMap.New(2); map.AddOrUpdate(42, "1"); @@ -332,7 +318,7 @@ public void Can_store_and_retrieve_value_from_map_Golden() [Test] public void Can_lookup_the_default_map_without_error() { - SmallMap> map = default; + HSmallMap> map = default; Assert.IsFalse(map.TryGetValue(42, out _)); } @@ -340,7 +326,7 @@ public void Can_lookup_the_default_map_without_error() [Test] public void Can_store_and_retrieve_value_from_map_with_Expand_in_the_middle() { - var map = SmallMap.New(1); + var map = HSmallMap.New(1); Assert.IsFalse(map.TryGetValue(42, out _)); @@ -378,7 +364,7 @@ public void Can_store_and_retrieve_value_from_map_with_Expand_in_the_middle() [Test] public void Can_resize_without_moving() { - var map = SmallMap.New(2); + var map = HSmallMap.New(2); map.AddOrUpdate(0, "0"); map.AddOrUpdate(1, "1"); @@ -395,7 +381,7 @@ public void Can_resize_without_moving() [Test] public void Can_store_and_get_stored_item_count() { - var map = SmallMap.New(); + var map = HSmallMap.New(); map.AddOrUpdate(42, "1"); map.AddOrUpdate(42 + 32 + 32, "3"); @@ -407,7 +393,7 @@ public void Can_store_and_get_stored_item_count() [Test] public void Can_update_a_stored_item_with_new_value() { - var map = SmallMap.New(); + var map = HSmallMap.New(); map.AddOrUpdate(42, "1"); map.AddOrUpdate(42, "3"); @@ -420,7 +406,7 @@ public void Can_update_a_stored_item_with_new_value() [Test] public void Can_add_key_with_0_hash_code() { - var map = SmallMap.New(); + var map = HSmallMap.New(); map.AddOrUpdate(0, "aaa"); map.AddOrUpdate(0 + 32, "2"); @@ -436,7 +422,7 @@ public void Can_add_key_with_0_hash_code() [Test] public void Can_quickly_find_the_scattered_items_with_the_same_cache() { - var map = SmallMap.New(); + var map = HSmallMap.New(); map.AddOrUpdate(42, "1"); map.AddOrUpdate(43, "a"); @@ -457,7 +443,7 @@ public void Can_quickly_find_the_scattered_items_with_the_same_cache() [Test] public void Can_remove_the_stored_item() { - var map = SmallMap.New(2); + var map = HSmallMap.New(2); map.AddOrUpdate(42, "1"); map.AddOrUpdate(42 + 32, "2"); @@ -480,7 +466,7 @@ public void Check_AddOrUpdate_random_items_and_verify_all_added() const int upperBound = 100000; Gen.Int[0, upperBound].Array.Sample(items => { - var m = SmallMap.New>(); + var m = HSmallMap.New>(); foreach (var n in items) { var k = "" + n; @@ -505,7 +491,7 @@ public void Check_AddOrUpdate_random_items_and_verify_all_added() // } static Gen<( - SmallMap, SmallMap.SingleArrayEntries>>, + HSmallMap, HSmallMap.SingleArrayEntries>>, string[])> GenImMap(int upperBound) => Gen.Int[0, upperBound].ArrayUnique.SelectMany(keys => @@ -514,7 +500,7 @@ public void Check_AddOrUpdate_random_items_and_verify_all_added() var keyArray = keys.Select(x => x.ToString()).ToArray(); var valArray = values.Select(x => x.ToString()).ToArray(); - var m = SmallMap.New>(); + var m = HSmallMap.New>(); for (var i = 0; i < keyArray.Length; i++) m.AddOrUpdate(keyArray[i], valArray[i]); return (map: m, keys: keyArray); @@ -533,8 +519,8 @@ public void Check_AddOrUpdate_metamorphic() // todo: @wip add the Copy method // copy things to the new maps - var m1 = SmallMap.New>(); - var m2 = SmallMap.New>(); + var m1 = HSmallMap.New>(); + var m2 = HSmallMap.New>(); foreach (var (k, v) in m.Select(x => (x.Key, x.Value))) { m1.AddOrUpdate(k, v); @@ -588,8 +574,8 @@ public void Check_Remove_metamorphic() var ((m, _), k1, v1, k2, v2) = t; var (sk1, sv1, sk2, sv2) = ("" + k1, "" + v1, "" + k2, "" + v2); - var m1 = SmallMap.New>(); - var m2 = SmallMap.New>(); + var m1 = HSmallMap.New>(); + var m2 = HSmallMap.New>(); foreach (var (k, v) in m.Select(x => (x.Key, x.Value))) { m1.AddOrUpdate(k, v); @@ -646,7 +632,7 @@ public void Check_Remove_ModelBased() public static class SmallMapTestTools { - internal static void Verify(this SmallMap map, IEnumerable expectedKeys = null) + internal static void Verify(this HSmallMap map, IEnumerable expectedKeys = null) where TEq : struct, IEq where TEntries : struct, IEntries { @@ -658,7 +644,7 @@ internal static void Verify(this SmallMapVerifies that the hashes correspond to the keys stored in the entries. May be called from the tests. - public static void VerifyHashesAndKeysEq(this SmallMap map) + public static void VerifyHashesAndKeysEq(this HSmallMap map) where TEq : struct, IEq where TEntries : struct, IEntries { @@ -669,7 +655,7 @@ public static void VerifyHashesAndKeysEq(this SmallMapVerifies that there is no duplicate keys stored in hashes -> entries. May be called from the tests. - public static void VerifyNoDuplicateKeys(this SmallMap map) + public static void VerifyNoDuplicateKeys(this HSmallMap map) where TEq : struct, IEq where TEntries : struct, IEntries { @@ -678,7 +664,7 @@ public static void VerifyNoDuplicateKeys(this SmallMap(this SmallMapVerifies that the probes are consistently increasing - public static void VerifyProbesAreFitRobinHood(this SmallMap map) + public static void VerifyProbesAreFitRobinHood(this HSmallMap map) where TEq : struct, IEq where TEntries : struct, IEntries { @@ -701,7 +687,7 @@ public static void VerifyProbesAreFitRobinHood(this SmallMa var indexMask = capacity - 1; var prevProbes = -1; const int ProbeCountShift = 32 - MaxProbeBits; - for (var i = 0; i < hashes.Length; i++) + for (var i = 0; i < hashes.Length - 8; i++) { var h = hashes[i]; var probes = h >>> ProbeCountShift; @@ -712,7 +698,7 @@ public static void VerifyProbesAreFitRobinHood(this SmallMa } /// Verifies that the map contains all passed keys. May be called from the tests. - public static void VerifyContainAllKeys(this SmallMap map, IEnumerable expectedKeys) + public static void VerifyContainAllKeys(this HSmallMap map, IEnumerable expectedKeys) where TEq : struct, IEq where TEntries : struct, IEntries {