diff --git a/StgSharp.Benchmark/SwissBenchmark.cs b/StgSharp.Benchmark/SwissBenchmark.cs index b5437d64..187c86be 100644 --- a/StgSharp.Benchmark/SwissBenchmark.cs +++ b/StgSharp.Benchmark/SwissBenchmark.cs @@ -29,41 +29,127 @@ using BenchmarkDotNet.Attributes; using StgSharp.Collections; using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; 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. + /// Benchmark comparing SwissTable against + /// under realistic L4 software cache address-mapping workloads. + /// + /// L4 scenario characteristics: + /// - Keys are HLSF/malloc pointer addresses (large, 1024-byte aligned) + /// - Access pattern is Zipfian-skewed (20% hot cache lines receive 80% of lookups) + /// - Steady state: high hit-rate lookups dominate, with rare evict+refill on miss + /// - Working set: 512–4096 active cache line mappings + /// /// [MemoryDiagnoser] public class SwissTableBench { - private const int LookupCount = 4096; + /// L4 cache line size in bytes (per L4_design.md). + private const int CacheLineSize = 1024; - private nint[] _lookupKeys = null!; + /// Simulated heap base address for pointer-like keys. + private const long HeapBase = 0x7FFF_0000_0000L; - // ── Pre-generated key arrays ── + /// Number of lookup operations per Lookup benchmark call. + private const int LookupIterations = 8192; - private nint[] _preloadedKeys = null!; + /// + /// Number of steady-state operations per call. + /// Each op is either a lookup (hit) or an evict+insert+lookup (miss). + /// + private const int SteadyStateOps = 4096; + + /// Fraction of steady-state ops that are cache misses. + private const double MissRate = 0.05; + + // ── Pre-generated key arrays (pointer-like, 1024-byte aligned) ── + + /// Keys for initial table fill (simulates cached addresses). + private nint[] _residentKeys = null!; + + /// Additional keys not in the table, for triggering misses. + private nint[] _externalKeys = null!; + + /// Zipfian-skewed lookup sequence (hot 20% accessed 80% of time). + private nint[] _zipfianLookups = null!; + + /// Uniform random lookup sequence (baseline comparison). + private nint[] _uniformLookups = null!; + + /// + /// Pre-built steady-state operation script. + /// Each entry: positive index → lookup _residentKeys[idx], + /// negative value → cache miss trigger (evict + insert from _externalKeys). + /// + private int[] _steadyStateScript = null!; + + /// Indices into _residentKeys for eviction targets (cold entries, tail region). + private int[] _evictOrder = null!; + + // ── Pre-filled lookup targets ── + + private SwissTable _swissLookup = null!; private Dictionary _dictLookup = null!; - private Dictionary _freshDict = null!; // ── Fresh instances for insert/mixed (rebuilt each iteration) ── private SwissTable _freshSwiss = null!; + private Dictionary _freshDict = null!; - // ── Lookup targets (pre-filled in GlobalSetup) ── - - private SwissTable _swissLookup = null!; - - [Params(500, 2000, 5000)] + /// + /// Active cache line count. Matches L4 capacity range: + /// 5 MB / 1024B ≈ 5120, 10 MB / 1024B ≈ 10240. + /// 512 and 1024 test low-occupancy; 4096 tests near-capacity. + /// + [Params(512, 1024, 4096)] public int KeyCount; + // ════════════════════════════════════════════════════════════ + // Setup / Teardown + // ════════════════════════════════════════════════════════════ + + [GlobalSetup] + public void Setup() + { + Random rng = new Random(42); + + // ── Generate pointer-like keys (HeapBase + random_offset * 1024) ── + _residentKeys = GeneratePointerKeys(KeyCount, rng); + _externalKeys = GeneratePointerKeys(KeyCount, rng); // disjoint set for miss simulation + + // ── Build lookup sequences ── + _zipfianLookups = GenerateZipfianLookups(_residentKeys, LookupIterations, rng); + _uniformLookups = GenerateUniformLookups(_residentKeys, LookupIterations, rng); + + // ── Build steady-state operation script ── + _steadyStateScript = GenerateSteadyStateScript(KeyCount, SteadyStateOps, MissRate, rng); + + // ── Build eviction order (cold tail entries first) ── + _evictOrder = new int[KeyCount]; + for (int i = 0; i < KeyCount; i++) + { + _evictOrder[i] = i; + } + // Shuffle so eviction isn't just sequential + Shuffle(_evictOrder, rng); + + // ── Pre-fill lookup tables ── + _swissLookup = SwissTable.Create(); + _dictLookup = new Dictionary(KeyCount); + + for (int i = 0; i < KeyCount; i++) + { + nint key = _residentKeys[i]; + nint val = key ^ unchecked((nint)0xDEAD_BEEF); // simulated cache pointer + _swissLookup.TryAddOrSet(key, val, out _); + _dictLookup[key] = val; + } + } + [GlobalCleanup] public void Cleanup() { @@ -71,119 +157,114 @@ namespace StgSharp.Benchmark _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(SwissTable_SteadyState), nameof(Dictionary_Insert), - nameof(Dictionary_Mixed) + nameof(Dictionary_SteadyState) ])] 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); + // Pre-fill for steady-state benchmarks 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); + nint key = _residentKeys[i]; + nint val = key ^ unchecked((nint)0xDEAD_BEEF); + _freshSwiss.TryAddOrSet(key, val, out _); + _freshDict[key] = val; } + } - Random rng = new Random(42); - for (int i = 0; i < LookupCount; i++) { - _lookupKeys[i] = (nint)rng.Next(0, KeyCount); + // ════════════════════════════════════════════════════════════ + // Lookup — Zipfian (L4 hot path, skewed access) + // 20% of cache lines receive 80% of accesses. + // This is the most representative L4 benchmark. + // ════════════════════════════════════════════════════════════ + + [Benchmark(Baseline = true)] + public nint Dictionary_Lookup_Zipfian() + { + nint sum = 0; + nint[] keys = _zipfianLookups; + Dictionary dict = _dictLookup; + for (int i = 0; i < keys.Length; i++) + { + if (dict.TryGetValue(keys[i], out nint value)) + { + sum += value; + } + } + return sum; + } + + [Benchmark] + public nint SwissTable_Lookup_Zipfian() + { + nint sum = 0; + nint[] keys = _zipfianLookups; + SwissTable swiss = _swissLookup; + for (int i = 0; i < keys.Length; i++) + { + if (swiss.TryGet(keys[i], out nint value)) + { + sum += value; + } + } + return sum; + } + + // ════════════════════════════════════════════════════════════ + // Lookup — Uniform (baseline comparison) + // ════════════════════════════════════════════════════════════ + + [Benchmark] + public nint Dictionary_Lookup_Uniform() + { + nint sum = 0; + nint[] keys = _uniformLookups; + Dictionary dict = _dictLookup; + for (int i = 0; i < keys.Length; i++) + { + if (dict.TryGetValue(keys[i], out nint value)) + { + sum += value; + } + } + return sum; + } + + [Benchmark] + public nint SwissTable_Lookup_Uniform() + { + nint sum = 0; + nint[] keys = _uniformLookups; + SwissTable swiss = _swissLookup; + for (int i = 0; i < keys.Length; i++) + { + if (swiss.TryGet(keys[i], out nint value)) + { + sum += value; + } + } + return sum; + } + + // ════════════════════════════════════════════════════════════ + // Insert — bulk fill with scattered pointer addresses + // Simulates L4 initial cache warm-up phase. + // ════════════════════════════════════════════════════════════ + + [Benchmark] + public void Dictionary_Insert() + { + Dictionary dict = _freshDict; + nint[] keys = _residentKeys; + for (int i = 0; i < keys.Length; i++) + { + dict[keys[i]] = keys[i] ^ unchecked((nint)0xDEAD_BEEF); } } @@ -191,64 +272,235 @@ namespace StgSharp.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 _); + nint[] keys = _residentKeys; + for (int i = 0; i < keys.Length; i++) + { + swiss.TryAddOrSet(keys[i], keys[i] ^ unchecked((nint)0xDEAD_BEEF), out _); } } + // ════════════════════════════════════════════════════════════ + // SteadyState — the KEY L4 benchmark + // Pre-filled table at capacity, then 4096 operations: + // ~95% are lookups (cache hits) + // ~5% are miss → evict cold entry → insert new entry → lookup new + // This directly simulates L4's main runtime loop. + // ════════════════════════════════════════════════════════════ + [Benchmark] - public nint SwissTable_Lookup() + public nint Dictionary_SteadyState() { nint sum = 0; - nint[] keys = _lookupKeys; - SwissTable swiss = _swissLookup; - for (int i = 0; i < keys.Length; i++) + Dictionary dict = _freshDict; + nint[] resident = _residentKeys; + nint[] external = _externalKeys; + int[] script = _steadyStateScript; + int[] evict = _evictOrder; + int evictCursor = 0; + int externalCursor = 0; + + for (int i = 0; i < script.Length; i++) { - if (swiss.TryGet(keys[i], out nint value)) { - sum += value; + int op = script[i]; + if (op >= 0) + { + // Cache hit: lookup existing entry + if (dict.TryGetValue(resident[op], out nint value)) + { + sum += value; + } + } + else + { + // Cache miss: evict a cold entry, insert a new one, then lookup + int evictIdx = evict[evictCursor % evict.Length]; + evictCursor++; + dict.Remove(resident[evictIdx]); + + int extIdx = externalCursor % external.Length; + externalCursor++; + nint newKey = external[extIdx]; + nint newVal = newKey ^ unchecked((nint)0xDEAD_BEEF); + dict[newKey] = newVal; + + if (dict.TryGetValue(newKey, out nint v)) + { + sum += v; + } } } return sum; } [Benchmark] - public nint SwissTable_Mixed() + public nint SwissTable_SteadyState() { + nint sum = 0; SwissTable swiss = _freshSwiss; - nint[] keys = _preloadedKeys; - nint sum = 0; + nint[] resident = _residentKeys; + nint[] external = _externalKeys; + int[] script = _steadyStateScript; + int[] evict = _evictOrder; + int evictCursor = 0; + int externalCursor = 0; - // insert all - for (int i = 0; i < keys.Length; i++) + for (int i = 0; i < script.Length; i++) { - swiss.TryAddOrSet(keys[i], keys[i] * 2, out _); - } + int op = script[i]; + if (op >= 0) + { + // Cache hit: lookup existing entry + if (swiss.TryGet(resident[op], out nint value)) + { + sum += value; + } + } + else + { + // Cache miss: evict a cold entry, insert a new one, then lookup + int evictIdx = evict[evictCursor % evict.Length]; + evictCursor++; + swiss.TryRemove(resident[evictIdx], out _); - // lookup first half - for (int i = 0; i < keys.Length / 2; i++) - { - if (swiss.TryGet(keys[i], out nint value)) { - sum += value; + int extIdx = externalCursor % external.Length; + externalCursor++; + nint newKey = external[extIdx]; + nint newVal = newKey ^ unchecked((nint)0xDEAD_BEEF); + swiss.TryAddOrSet(newKey, newVal, out _); + + if (swiss.TryGet(newKey, out nint v)) + { + sum += v; + } } } - - // 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; } + // ════════════════════════════════════════════════════════════ + // Data generation helpers + // ════════════════════════════════════════════════════════════ + + /// + /// Generate unique pointer-like keys. + /// Each key = HeapBase + randomOffset * CacheLineSize, + /// simulating HLSF-allocated 1024-byte aligned addresses + /// scattered across a ~1 GB virtual address range. + /// + private static nint[] GeneratePointerKeys( + int count, + Random rng + ) + { + nint[] keys = new nint[count]; + HashSet used = new HashSet(count); + for (int i = 0; i < count; i++) + { + long offset; + do + { + // Random offset within ~1M cache lines (1 GB range) + offset = (long)rng.Next(0, 1 << 20) * CacheLineSize; + } while (!used.Add(offset)); + keys[i] = (nint)(HeapBase + offset); + } + return keys; + } + + /// + /// Generate Zipfian-skewed lookup sequence: + /// 80% of accesses hit the hottest 20% of keys (sorted by index as "hotness proxy"), + /// 20% of accesses hit the remaining 80%. + /// This models matrix computation where a small panel working set is reused heavily. + /// + private static nint[] GenerateZipfianLookups( + nint[] sourceKeys, + int count, + Random rng + ) + { + int hotSetSize = Math.Max(1, sourceKeys.Length / 5); + nint[] lookups = new nint[count]; + for (int i = 0; i < count; i++) + { + if (rng.NextDouble() < 0.8) + { + lookups[i] = sourceKeys[rng.Next(0, hotSetSize)]; + } + else + { + lookups[i] = sourceKeys[rng.Next(hotSetSize, sourceKeys.Length)]; + } + } + return lookups; + } + + /// + /// Generate uniform random lookup sequence (baseline for comparison with Zipfian). + /// + private static nint[] GenerateUniformLookups( + nint[] sourceKeys, + int count, + Random rng + ) + { + nint[] lookups = new nint[count]; + for (int i = 0; i < count; i++) + { + lookups[i] = sourceKeys[rng.Next(0, sourceKeys.Length)]; + } + return lookups; + } + + /// + /// Generate a steady-state operation script. + /// Each entry: non-negative value = index into _residentKeys (lookup/hit), + /// negative value (-1) = cache miss trigger (evict → insert → lookup). + /// Lookup indices are Zipfian-skewed. + /// + private static int[] GenerateSteadyStateScript( + int keyCount, + int opCount, + double missRate, + Random rng + ) + { + int hotSetSize = Math.Max(1, keyCount / 5); + int[] script = new int[opCount]; + for (int i = 0; i < opCount; i++) + { + if (rng.NextDouble() < missRate) + { + script[i] = -1; // miss + } + else + { + // Zipfian hit + if (rng.NextDouble() < 0.8) + { + script[i] = rng.Next(0, hotSetSize); + } + else + { + script[i] = rng.Next(hotSetSize, keyCount); + } + } + } + return script; + } + + /// Fisher-Yates shuffle. + private static void Shuffle( + int[] array, + Random rng + ) + { + for (int i = array.Length - 1; i > 0; i--) + { + int j = rng.Next(0, i + 1); + (array[i], array[j]) = (array[j], array[i]); + } + } + } } \ No newline at end of file diff --git a/StgSharp.Common/Collections/SwissTable.cs b/StgSharp.Common/Collections/SwissTable.cs index 844839dd..96e41d46 100644 --- a/StgSharp.Common/Collections/SwissTable.cs +++ b/StgSharp.Common/Collections/SwissTable.cs @@ -2,8 +2,8 @@ // ----------------------------------------------------------------------- // file="SwissTable" // Project: StgSharp -// AuthorGroup: Nitload -// Copyright (c) Nitload. All rights reserved. +// AuthorGroup: Nitload Space +// Copyright (c) Nitload Space. 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 @@ -43,14 +43,12 @@ namespace StgSharp.Collections private const int BucketCount = 1 << EntryMaskWidth; private const int BucketSize = 16; private const uint CrcMagic = 0xffff9961; // Magic number for CRC32C, used in hash computation - private const int EntryMaskWidth = 16; // 65536 entries total, 16 per bucket + private const int EntryMaskWidth = 10; // 1024 entries total, 16 per bucket public const int MaxCapacity = BucketCount * BucketSize; // 256 entries per bucket, 1024 buckets private readonly Entry* _entries; - private SwissTable( - Entry* buffer - ) + private SwissTable(Entry* buffer) { _entries = buffer; } @@ -66,11 +64,7 @@ namespace StgSharp.Collections return new SwissTable(buffer); } - public bool TryAddOrSet( - nint key, - nint value, - out bool isNewKey - ) + public bool TryAddOrSet(nint key, nint value, out bool isNewKey) { uint hash = BitOperations.Crc32C(CrcMagic, (ulong)key); ushort entry = (ushort)(hash & (BucketCount - 1)); @@ -78,10 +72,7 @@ namespace StgSharp.Collections return IBucketScanner.Current.TrySet(key, hash, bucket, value, out isNewKey); } - public bool TryGet( - nint key, - out nint value - ) + public bool TryGet(nint key, out nint value) { uint hash = BitOperations.Crc32C(CrcMagic, (ulong)key); ushort entry = (ushort)(hash & (BucketCount - 1)); @@ -96,10 +87,7 @@ namespace StgSharp.Collections */ } - public bool TryRemove( - nint key, - out nint value - ) + public bool TryRemove(nint key, out nint value) { uint hash = BitOperations.Crc32C(CrcMagic, (ulong)key); ushort entry = (ushort)(hash & (BucketCount - 1)); @@ -134,27 +122,11 @@ namespace StgSharp.Collections static IBucketScanner Current { get; } = Create(); - bool TryFind( - nint key, - uint Hash, - Bucket* bucket, - out nint value - ); + bool TryFind(nint key, uint Hash, Bucket* bucket, out nint value); - unsafe bool TryRemove( - nint key, - uint hash, - Bucket* bucket, - out nint value - ); + unsafe bool TryRemove(nint key, uint hash, Bucket* bucket, out nint value); - bool TrySet( - nint key, - uint hash, - Bucket* bucket, - nint value, - out bool isNewKey - ); + bool TrySet(nint key, uint hash, Bucket* bucket, nint value, out bool isNewKey); private static IBucketScanner Create() { @@ -197,4 +169,4 @@ namespace StgSharp.Collections } } -} \ No newline at end of file +} diff --git a/StgSharp.Common/HighPerformance/Memory/L4.cs b/StgSharp.Common/HighPerformance/Memory/L4.cs new file mode 100644 index 00000000..7d951ea9 --- /dev/null +++ b/StgSharp.Common/HighPerformance/Memory/L4.cs @@ -0,0 +1,59 @@ +//----------------------------------------------------------------------- +// ----------------------------------------------------------------------- +// file="L4" +// Project: StgSharp +// AuthorGroup: Nitload Space +// Copyright (c) Nitload Space. 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 StgSharp.Collections; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StgSharp.HighPerformance.Memory +{ + public unsafe class L4 + { + + private readonly byte* _buffer; + private readonly SwissTable _map; + + public nint MapMemory(nint address) + { + if (_map.TryGet(address, out nint ptr)) + { + return ptr; + } else + { + /* + * register a request for memory mapping, + * and return a pointer to the mapped memory. + */ + return 0; + } + } + + } +} diff --git a/StgSharp.Common/HighPerformance/Memory/L4_design.md b/StgSharp.Common/HighPerformance/Memory/L4_design.md index 103c87e4..9464b8ee 100644 --- a/StgSharp.Common/HighPerformance/Memory/L4_design.md +++ b/StgSharp.Common/HighPerformance/Memory/L4_design.md @@ -77,8 +77,8 @@ L4 缓存器是一个纯软件实现的**线程本地数据缓存层**, | 参数 | 值 | 说明 | |-------------------|-----------------------|-----------------------------------| | 缓存区总容量 | 5-10 MB | 线程本地连续内存 | -| 缓存行大小 | 1024 Bytes | = 8 个 `double MAT_KERNEL` 的大小 | -| 映射表类型 | Swiss Table | 1024 桶 × 256 槽,SIMD 加速 | +| 缓存行大小 | 8192 Bytes | = 64 个 `double MAT_KERNEL` 的大小 | +| 映射表类型 | Swiss Table | 65536 桶 × 16 槽,SIMD 加速 | | 缓存行存储池 | Slab Allocator | 非并发、固定容量、不自动扩容 | | 逐出策略 | 4 级热度环形链表 | 基于访问频率的近似 LFU | | 写回策略 | 无(只读缓存) | 逐出时直接丢弃,不写回远端 |