//----------------------------------------------------------------------- // ----------------------------------------------------------------------- // file="SwissBenchmark" // Project: StgSharp // AuthorGroup: Nitload // Copyright (c) Nitload. All rights reserved. // // 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 in 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. // // ----------------------------------------------------------------------- // ----------------------------------------------------------------------- using BenchmarkDotNet.Attributes; using StgSharp.Collections; using System; using System.Collections.Generic; namespace StgSharp.Benchmark { /// /// Benchmark comparing SwissTable (SSE2 SIMD probing, zero-GC, native memory) against for the L4 cache address mapping workload: concentrated /// small-key insert/lookup/remove cycles. SwissTable.Create() performs a 1 MB memset /// (65536 × 16 control bytes), which is separated into IterationSetup to avoid polluting /// insert timings. /// [MemoryDiagnoser] public class SwissTableBench { private const int LookupCount = 4096; private nint[] _lookupKeys = null!; // ── Pre-generated key arrays ── private nint[] _preloadedKeys = null!; private Dictionary _dictLookup = null!; private Dictionary _freshDict = null!; // ── Fresh instances for insert/mixed (rebuilt each iteration) ── private SwissTable _freshSwiss = null!; // ── Lookup targets (pre-filled in GlobalSetup) ── private SwissTable _swissLookup = null!; [Params(500, 2000, 5000)] public int KeyCount; [GlobalCleanup] public void Cleanup() { _dictLookup.Clear(); _dictLookup.TrimExcess(); } // ══════════════════════════════════════ // Insert: pure write throughput // Empty table → fill N keys // Create() cost excluded via IterationSetup // ══════════════════════════════════════ [Benchmark] public void Dictionary_Insert() { Dictionary dict = _freshDict; nint[] keys = _preloadedKeys; for (int i = 0; i < keys.Length; i++) { dict[keys[i]] = keys[i] * 2; } } // ══════════════════════════════════════ // Lookup: pure read throughput // Pre-filled table, random key access // ══════════════════════════════════════ [Benchmark(Baseline = true)] public nint Dictionary_Lookup() { nint sum = 0; nint[] keys = _lookupKeys; Dictionary dict = _dictLookup; for (int i = 0; i < keys.Length; i++) { if (dict.TryGetValue(keys[i], out nint value)) { sum += value; } } return sum; } // ══════════════════════════════════════ // Mixed: insert all → lookup half → remove half → lookup all // Simulates L4 cache lifecycle: // load working set → compute → evict → recompute // ══════════════════════════════════════ [Benchmark] public nint Dictionary_Mixed() { Dictionary dict = _freshDict; nint[] keys = _preloadedKeys; nint sum = 0; // insert all for (int i = 0; i < keys.Length; i++) { dict[keys[i]] = keys[i] * 2; } // lookup first half for (int i = 0; i < keys.Length / 2; i++) { if (dict.TryGetValue(keys[i], out nint value)) { sum += value; } } // remove first half (eviction) for (int i = 0; i < keys.Length / 2; i++) { dict.Remove(keys[i]); } // lookup all (half hit, half miss) for (int i = 0; i < keys.Length; i++) { if (dict.TryGetValue(keys[i], out nint value)) { sum += value; } } return sum; } [IterationSetup(Targets = [ nameof(SwissTable_Insert), nameof(SwissTable_Mixed), nameof(Dictionary_Insert), nameof(Dictionary_Mixed) ])] public void IterationSetup() { _freshSwiss = SwissTable.Create(); _freshDict = new Dictionary(KeyCount); } // ── Setup / Teardown ── [GlobalSetup] public void Setup() { _preloadedKeys = new nint[KeyCount]; _lookupKeys = new nint[LookupCount]; _swissLookup = SwissTable.Create(); _dictLookup = new Dictionary(KeyCount); for (int i = 0; i < KeyCount; i++) { _preloadedKeys[i] = (nint)i; _swissLookup.TryAddOrSet((nint)i, (nint)(i * 2), out _); _dictLookup[(nint)i] = (nint)(i * 2); } Random rng = new Random(42); for (int i = 0; i < LookupCount; i++) { _lookupKeys[i] = (nint)rng.Next(0, KeyCount); } } [Benchmark] public void SwissTable_Insert() { SwissTable swiss = _freshSwiss; nint[] keys = _preloadedKeys; for (int i = 0; i < keys.Length; i++) { swiss.TryAddOrSet(keys[i], keys[i] * 2, out _); } } [Benchmark] public nint SwissTable_Lookup() { nint sum = 0; nint[] keys = _lookupKeys; SwissTable swiss = _swissLookup; for (int i = 0; i < keys.Length; i++) { if (swiss.TryGet(keys[i], out nint value)) { sum += value; } } return sum; } [Benchmark] public nint SwissTable_Mixed() { SwissTable swiss = _freshSwiss; nint[] keys = _preloadedKeys; nint sum = 0; // insert all for (int i = 0; i < keys.Length; i++) { swiss.TryAddOrSet(keys[i], keys[i] * 2, out _); } // lookup first half for (int i = 0; i < keys.Length / 2; i++) { if (swiss.TryGet(keys[i], out nint value)) { sum += value; } } // remove first half (eviction) for (int i = 0; i < keys.Length / 2; i++) { swiss.TryRemove(keys[i], out _); } // lookup all (half hit, half miss) for (int i = 0; i < keys.Length; i++) { if (swiss.TryGet(keys[i], out nint value)) { sum += value; } } return sum; } } }