cad.net 最快的字典SwissTable
SwissTable
各种策略
1,主要参考了谷歌的SwissTable,它是在开放寻址法上面的一种极端优化.
我没有看过谷歌的代码,只是凭借网络文章自己用手机敲的C#版本,可能实现得有点不一样.
是用在线NET80环境验证的,
不过只是用到Sse,应该可以向下扩展.
2,SwissTable数据结构
它描述了A数组(table/分片)链接着B数组(group),B组是定长是SIMD宽度,
也就是它本质上是一个二维数组结构.
为什么不直接用数组呢?
有没有可能我们只是对数组封装了规则?嘿嘿.
不过更多时候其实是为了实现rehash这种东西.
B组不要用Vector256,因为权衡了这个128长度.128/16=8bit.
因此我们选用Vector128.
8bit表示用byte类型作为一个短hash,叫做h2.
这样SIMD一次性可以比较16个h2,以此实现加速.
16个表示说明解决冲突足够了,
如果不满足的话,会直接++处理下一个分组,分组可以SIMD预取.
其中byte最高位1bit被占用作为byte符号位,最高位是1,
所以其实只是用了7bit.
3,申请内存和清理.
C++编程习惯是无根持有堆对象,没有GC去回收,
清理的时候可以只清空meta数组,不需要清理key,value数组.
C#虽然也能做到无根持有,无非就是非托管堆申请内存.
但是习惯是要容器持有对象强引用,不然KV两个对象会被GC回收.
也就是我们需要Clear key,value数组,否则不就是移除了对象还在持有对象强引用了?
这样一搞速度就低了啊,还是蛮麻烦的.
_table使用了alloc进行申请非托管内存,并且不清零策略.
非托管堆还没有GC扫描它,没有大对象扫描,使得可以减少STW时间.
不过无法在非安全上下文中使用yield来构造迭代器,所以要用传统的枚举器模式.
4,扩容
rehash不能用realloc,因为存在新旧搬迁.
absl::flat_hash_map是动态重新求hashcode,
储存在h1Array有储存成本...并且可变类存在致命问题.
例如string的hashcode缓存其实是对的,不过通常是实现类上面进行,而非容器进行.
所以我们也要动态求hashcode
没实现渐进式扩容,毕竟它不是并发容器.
还没做...
5,go语言存在缓存失效问题,
https://cloud.tencent.com/developer/article/2523101
我设计本来就是KV分离数组
6,微软在net每个进程都是做了随机值做base,防止别人攻击.
并且提供默认配置可关闭,因为分布式求hash要相同才行.
还没做...
7,为了省电,分组跳转上面我们不用除法器%,而是保证2次方的数组长度,用&来进行取余数
但用素数长度可以增加稀疏性,就要用%了?.
Google的测试表明在良好哈希函数下,素数优势不明显,
位运算的收益明显大于素数带来的分布改善.
9,缩容
因为字典通常都是不缩容的,可以得到更好的性能.
所以像google实现的是手动缩容.
我实现了配置参数上面决定手动和自动缩容.
10,惰性分配
首次是1个组(16个槽位),并没有首次0组.
11,居然被AI耍了,把hashcode | 0x80,害我以为不能写key == 0.
瑞士表的数据结构
结构体数组只需要一次解引用(我实现的).
类数组就需要两次解引用.
_table (Group*)
│
├─ _table[0] (Group实例,结构体)
│ ├─ Metadata → byte[16] 16字节
│ ├─ Keys* → 指针,独立分配的 TKey[16] 8字节
│ ├─ Values* → 指针,独立分配的 TValue[16] 8字节
│ └─ H1Array → 内联的 long[16] 128字节 (可变类型需要每次重新求,废弃)
│
├─ _table[1] (Group实例)
│ ├─ Metadata → ...
│ └─ ...
└─ ...
为什么 SwissTable 可以不使用 unmanaged 约束
潜在问题
当执行 map.Add("string", 1) 时,字符串"string"在托管堆上分配
指向该key的字符串的指针被存储在非托管内存中
GC会跟踪这个引用,确保字符串不会被错误回收.
但是它会存在风险.
对于引用类型(类)获取其指针需要特殊处理,
因为类的实例存储在托管堆上,并且可能被垃圾回收器移动.
以下是几种获取类实例指针的方法:
1. 使用 GCHandle 固定对象(推荐方法)
private static unsafe IntPtr GetClassPtr<T>(T obj) where T : class { GCHandle handle = GCHandle.Alloc(obj, GCHandleType.Pinned); try { return handle.AddrOfPinnedObject(); } finally { handle.Free(); } }
如果同一个对象每次调用 GCHandle.Alloc() 都返回不同的 GCHandle ,
那么直接用 GCHandle 作为键会导致哈希表无法正确查找,
因为相同的对象会生成不同的键(GCHandle)导致 Get 操作失败.
2. 使用 fixed 语句(仅适用于包含非托管类型的类)
public unsafe class UnmanagedContainer { public int Value; public static IntPtr GetPointer(UnmanagedContainer obj) { fixed (void* p = &obj.Value) { return (IntPtr)p; } } }
3. 使用 Unsafe 类(高级用法,需谨慎,它是即用即弃,也就是临时)
private static unsafe IntPtr GetClassPtrUnsafe<T>(T obj) where T : class { return (IntPtr)Unsafe.AsPointer(ref obj); }
重要注意事项:
固定对象会影响垃圾回收器性能,应尽快释放
获取的指针只在对象被固定期间有效
修改指针指向的内存可能破坏类型安全性
对于纯托管类,无法直接获取其内部结构的指针
对于大多数情况,GCHandle 是最安全可靠的方法.
如果确实需要频繁操作类实例的指针,考虑使用非托管内存或值类型.
不如直接用Dictionary<>持有托管类型,然后做一层转发...
GC 压缩与非托管内存中的托管引用
你提出了一个非常关键的问题!确实,GC 的压缩(compaction)会移动托管对象并更新所有引用,那么为什么存储在非托管内存中的托管引用不会出问题呢?让我们深入分析:
GC 如何处理非托管内存中的托管引用
.NET 运行时确实有机制来处理这种情况,主要通过以下方式:
a) GCHandle 机制(虽然这里没有显式使用)
虽然这个实现没有显式使用 GCHandle,但类似的概念在底层工作。当托管对象被非托管代码引用时,GC 需要知道这些"外部引用"。
b) 关键点:非托管内存本身被 GC 跟踪
在这个实现中,整个 SwissTable 类是一个托管对象,它持有非托管内存的指针(_table)。GC 知道:
- 这个托管对象(SwissTable 实例)是存活的
- 这个托管对象"拥有"某些非托管内存
- 这些非托管内存中可能包含托管引用
为什么不会出问题
具体到这个实现:
非托管内存中的托管引用被 GC 视为根(root):
- 因为 SwissTable 是托管对象,它内部指针指向的非托管内存被视为"可能包含托管引用"
- GC 会扫描这些区域,查找看起来像托管引用的指针
GC 压缩时的处理:
当 GC 压缩堆并移动对象时,它会:
- 更新所有托管代码中的引用
- 扫描非托管内存中的可能引用并更新它们
这是通过 GC 的"固定"(pinning)机制实现的
潜在风险与限制
虽然这种设计可以工作,但有重要限制:
性能影响
- GC 必须扫描非托管内存中的可能引用,这会增加 GC 负担
- 比纯 unmanaged 类型或显式使用 GCHandle 效率低
正确性依赖
- 要求非托管内存中的托管引用必须"看起来像"真正的托管引用
- 如果内存被错误解释(如整数被误认为引用),会导致问题
跨平台考虑
在某些特殊环境(如某些嵌入式系统)中,这种隐式行为可能不被支持
更安全的替代方案
生产代码通常会更明确地处理这种边界:
使用 GCHandle 显式固定
GCHandle handle = GCHandle.Alloc(obj, GCHandleType.Normal);
IntPtr ptr = GCHandle.ToIntPtr(handle); // 存储 ptr 到非托管内存
使用 unmanaged 约束(如果可能)
public sealed unsafe class SwissTable<TKey, TValue> where TKey : unmanaged where TValue : unmanaged
使用 Span 和 MemoryMarshal 进行安全转换
为什么这个示例能工作,这个示例能工作是因为:
- GC 的保守性:.NET GC 会保守地扫描非托管内存中的可能引用
- 实现细节:键和值存储为指针,GC 能识别这些是有效的引用
- 生命周期管理:SwissTable 是托管对象,确保非托管内存中的引用在表存活期间有效
结论
你完全正确地指出了 GC 压缩的问题。虽然这个实现可以工作(得益于 GC 的保守扫描),但在生产环境中:
- 对于性能关键代码,应该使用 unmanaged 约束
- 对于需要存储任意类型的场景,应该显式使用 GCHandle
这种"隐式"依赖 GC 行为的设计虽然方便,但不够健壮。
这种设计在教育示例中可以接受,但在生产代码中需要更严谨的处理方式。
代码
最少代码改为托管类型
public unsafe struct Group<TKey, TValue> {
public Vector128<byte> Metadata;
private GCHandle _keysHandle; // 托管数组的GC句柄,被GC扫描
private GCHandle _valuesHandle;
public TKey* Keys {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => (TKey*)_keysHandle.AddrOfPinnedObject();
}
public TValue* Values {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => (TValue*)_valuesHandle.AddrOfPinnedObject();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Group<TKey, TValue>* Create() {
var group = (Group<TKey, TValue>*)NativeMemory.Alloc(
(nuint)sizeof(Group<TKey, TValue>), (nuint)16);
// 初始化固定住的托管数组
group->_keysHandle = GCHandle.Alloc(
new TKey[SwissConfig.GroupSize],
GCHandleType.Pinned);
group->_valuesHandle = GCHandle.Alloc(
new TValue[SwissConfig.GroupSize],
GCHandleType.Pinned);
group->Metadata = Vector128.Create(SwissConfig.Empty);
return group;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Destroy() {
_keysHandle.Free(); // 释放托管引用
_valuesHandle.Free();
NativeMemory.Free(this);
}
}
SwissTable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using System.Threading.Tasks;
using System.Diagnostics;
public static class SwissConfig {
public const byte Empty = 0x80;
public const byte Tombstone = 0xFF;
public const float MinCap = 0.25f;
public const float MaxCap = 0.8f;
public const int GroupSize = 16; // 其实就是 Vector128<byte>.Count 为了内联直接写常量 //
}
[StructLayout(LayoutKind.Sequential, Pack = 16)]
public unsafe struct Group<TKey, TValue> {
// 热路径在第一位
public Vector128<byte> Metadata;
public TKey* Keys;
public TValue* Values;
// public fixed long H1Array[16]; 容器不要缓存此对象,
// 因为rehash要重计算才对,否则可变对象缓存是致命的,
// 不可变对象应该实现类中用字段进行缓存hashcode
// 创建并初始化一个新的Group
public static Group<TKey, TValue>* Create() {
Console.WriteLine("创建新Group");
var group = (Group<TKey, TValue>*)NativeMemory.Alloc(
(nuint)sizeof(Group<TKey, TValue>), (nuint)16);
if (group == null)
throw new OutOfMemoryException("Failed to allocate memory for Group");
group->Keys = (TKey*)NativeMemory.Alloc((nuint)(SwissConfig.GroupSize * sizeof(TKey)), (nuint)sizeof(TKey));
if (group->Keys == null) {
NativeMemory.Free(group);
throw new OutOfMemoryException("Failed to allocate memory for Keys");
}
group->Values = (TValue*)NativeMemory.Alloc((nuint)(SwissConfig.GroupSize * sizeof(TValue)), (nuint)sizeof(TValue));
if (group->Values == null) {
NativeMemory.Free(group->Keys);
group->Keys = null;
NativeMemory.Free(group);
throw new OutOfMemoryException("Failed to allocate memory for Values");
}
group->Metadata = Vector128.Create(SwissConfig.Empty);
/*
long* h1s = group->H1Array;
for (int i = 0; i < SwissConfig.GroupSize; i += 2) {
Sse2.Store(h1s + i, Vector128<long>.Zero);
}
*/
return group;
}
// 清空Group的元数据
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Clear() {
// 这两个是一样的
// Metadata = Vector128.Create(SwissConfig.Empty);
fixed (Vector128<byte>* pMetadata = &Metadata) {
Sse2.Store((byte*)pMetadata, Vector128.Create(SwissConfig.Empty));
}
}
// 破坏成员,用于回收
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Destroy() {
if (Keys != null) {
NativeMemory.Free(Keys);
Keys = null;
}
if (Values != null) {
NativeMemory.Free(Values);
Values = null;
}
}
// 预取优化方法
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void PrefetchNextGroup(Group<TKey, TValue>* nextGroup) {
if (Sse.IsSupported) {
Sse.Prefetch0((byte*)&nextGroup->Metadata);
Sse.Prefetch0((byte*)nextGroup->Keys);
Sse.Prefetch0((byte*)nextGroup->Values);
// Sse.Prefetch2((byte*)nextGroup->H1Array);
}
}
// 在Group中查找匹配的槽位
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly int FindSlot(byte h2, long h1, TKey key, IEqualityComparer<TKey> comparer) {
Console.WriteLine($"FindSlot在Group中查找 h2={h2}, h1={h1}, key={key}");
var match = Vector128.Create(h2);
var eq = Sse2.CompareEqual(Metadata, match);
int mask = Sse2.MoveMask(eq);
Console.WriteLine($"FindSlot的SIMD比较结果 mask={mask:X}");
while (mask != 0) {
int slot = BitOperations.TrailingZeroCount(mask);
// Console.WriteLine($"FindSlot检查槽位 {slot}, h1={H1Array[slot]}, key={Keys[slot]}");
// 这里不需要比较 H1Array[slot] == h1 && 了
if (comparer.Equals(Keys[slot], key)) {
Console.WriteLine($"FindSlot在槽位 {slot} 找到匹配项");
return slot;
}
mask &= ~(1 << slot);
Console.WriteLine($"FindSlot更新后 mask={mask:X}");
}
Console.WriteLine("FindSlot未找到匹配项");
return -1;
}
// 查找空槽或墓碑槽
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly int FindEmptyOrTombstone() {
Console.WriteLine("查找空槽或墓碑槽");
var empty = Vector128.Create(SwissConfig.Empty);
var tombstone = Vector128.Create(SwissConfig.Tombstone);
var eqEmpty = Sse2.CompareEqual(Metadata, empty);
var eqTombstone = Sse2.CompareEqual(Metadata, tombstone);
var combined = Sse2.Or(eqEmpty, eqTombstone);
int mask = Sse2.MoveMask(combined);
Console.WriteLine($"空槽/墓碑槽 mask={mask:X}");
return mask == 0 ? -1 : BitOperations.TrailingZeroCount(mask);
}
// 设置指定槽位的元数据
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetMetadata(int slot, byte value) {
// Console.WriteLine($"设置槽位 {slot} 元数据为 {value}");
fixed (Vector128<byte>* pMetadata = &Metadata) {
((byte*)pMetadata)[slot] = value;
}
}
// 获取指定槽位的元数据
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly byte GetMetadata(int slot) {
byte value;
fixed (Vector128<byte>* pMetadata = &Metadata) {
value = ((byte*)pMetadata)[slot];
}
// Console.WriteLine($"获取槽位 {slot} 元数据: {value}");
return value;
}
}
public sealed unsafe class SwissTable<TKey, TValue> : IDisposable,
IEnumerable<KeyValuePair<TKey, TValue>>
/* 最好加入约束
where TKey : unmanaged
where TValue : unmanaged
*/
{
private Group<TKey, TValue>* _table;
private int _groupCount;
private int _count;
private int _tombstones;
private readonly IEqualityComparer<TKey> _comparer;
public int Count => _count;
public int TombstoneCount() => _tombstones;
// 静态配置变量,控制是否自动缩容
// 默认手动缩容,因为字典通常不缩容可以得到更好的性能
public bool AutoShrink { get; set; } = false;
// 构造函数: 使用默认比较器
public SwissTable() : this(EqualityComparer<TKey>.Default, 1) { }
// 构造函数: 指定比较器和初始容量
public SwissTable(IEqualityComparer<TKey> comparer, int groupNum = 1) {
Console.WriteLine($"创建SwissTable,初始组数: {groupNum}");
_comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
Initialize(groupNum > 0 ? groupNum : 1);
}
~SwissTable() {
Dispose(false);
}
// 释放资源
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
// 释放资源实现
private void Dispose(bool disposing) {
if (_table == null) return;
Console.WriteLine($"释放SwissTable,组数: {_groupCount}");
for (int i = 0; i < _groupCount; i++) {
_table[i].Destroy();
}
NativeMemory.Free(_table);
_table = null;
_groupCount = 0;
_count = 0;
_tombstones = 0;
}
// 初始化哈希表
private void Initialize(int groupNum) {
Console.WriteLine($"初始化SwissTable,请求组数: {groupNum}");
// 释放现有表
if (_table != null) {
Console.WriteLine("释放现有表...");
int oldGroupCount = _groupCount;
for (int i = 0; i < oldGroupCount; i++) {
_table[i].Destroy();
}
NativeMemory.Free(_table);
_table = null;
_groupCount = 0;
}
// 初始化新表
_groupCount = (int)BitOperations.RoundUpToPowerOf2((uint)Math.Max(1, groupNum));
Console.WriteLine($"实际分配的组数: {_groupCount}");
_table = (Group<TKey, TValue>*)NativeMemory.Alloc((nuint)(_groupCount * sizeof(Group<TKey, TValue>)), (nuint)16);
if (_table == null)
throw new OutOfMemoryException("Failed to allocate memory for SwissTable");
try {
for (int i = 0; i < _groupCount; i++) {
Console.WriteLine($"初始化Group {i}");
_table[i] = *Group<TKey, TValue>.Create();
}
}
catch {
Console.WriteLine("初始化失败,清理资源...");
for (int i = 0; i < _groupCount; i++) {
_table[i].Destroy();
}
NativeMemory.Free(_table);
_table = null;
_groupCount = 0;
throw;
}
_count = 0;
_tombstones = 0;
Console.WriteLine($"初始化完成,总组数: {_groupCount}");
}
// 计算键的哈希值(h1和h2)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private (long h1, byte h2) GetHashes(TKey key) {
if (Unsafe.IsNullRef(ref key)) throw new ArgumentNullException(nameof(key));
int fullHash = _comparer.GetHashCode(key) & 0x7FFFFFFF;
ulong hash = (ulong)fullHash;
hash ^= (hash >> 23) ^ (hash >> 47);
hash = hash * 31;
long h1 = (long)(hash >> 7) & 0x1FFFFFFFFFFFFFFL;
byte h2 = (byte)(hash & 0x7F);
Console.WriteLine($"键 {key} 的哈希值: h1={h1}, h2={h2}");
return (h1, h2);
}
// 尝试获取指定键的值
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValue(TKey key, out TValue value) {
if (Unsafe.IsNullRef(ref key)) throw new ArgumentNullException(nameof(key));
if (_table == null) {
value = default;
return false;
}
Console.WriteLine($"****尝试获取键 {key} 的值");
var (h1, h2) = GetHashes(key);
var groupIndex = h1 & (_groupCount - 1);
Console.WriteLine($"初始组索引: {groupIndex}");
// 预取第一个Group
Group<TKey, TValue>.PrefetchNextGroup(&_table[groupIndex]);
for (int i = 0; i < _groupCount; i++) {
var probeIndex = (groupIndex + i) & (_groupCount - 1);
Console.WriteLine($"探测组 {probeIndex}");
// 预取下一个Group(如果有)
if (i + 1 < _groupCount) {
var nextProbeIndex = (groupIndex + i + 1) & (_groupCount - 1);
Group<TKey, TValue>.PrefetchNextGroup(&_table[nextProbeIndex]);
}
int slot = _table[probeIndex].FindSlot(h2, h1, key, _comparer);
if (slot >= 0) {
value = _table[probeIndex].Values[slot];
Console.WriteLine($"找到键 {key} 的值: {value}");
return true;
}
if (_table[probeIndex].FindEmptyOrTombstone() >= 0)
break;
}
value = default;
Console.WriteLine($"未找到键 {key} 的值");
return false;
}
public bool TryGetValueRef(TKey key, ref TValue value) {
if (Unsafe.IsNullRef(ref key)) throw new ArgumentNullException(nameof(key));
if (_table == null) {
value = ref Unsafe.NullRef<TValue>();
return false;
}
Console.WriteLine($"****尝试获取键 {key} 的引用");
var (h1, h2) = GetHashes(key);
var groupIndex = h1 & (_groupCount - 1);
// 预取第一个Group
Group<TKey, TValue>.PrefetchNextGroup(&_table[groupIndex]);
for (int i = 0; i < _groupCount; i++) {
var probeIndex = (groupIndex + i) & (_groupCount - 1);
// 预取下一个Group(如果有)
if (i + 1 < _groupCount) {
var nextProbeIndex = (groupIndex + i + 1) & (_groupCount - 1);
Group<TKey, TValue>.PrefetchNextGroup(&_table[nextProbeIndex]);
}
int slot = _table[probeIndex].FindSlot(h2, h1, key, _comparer);
if (slot >= 0) {
value = ref _table[probeIndex].Values[slot];
Console.WriteLine($"找到键 {key} 的引用");
return true;
}
if (_table[probeIndex].FindEmptyOrTombstone() >= 0)
break;
}
value = ref Unsafe.NullRef<TValue>();
Console.WriteLine($"未找到键 {key} 的引用");
return false;
}
// 检查是否包含指定键
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ContainsKey(TKey key) {
Console.WriteLine($"检查是否包含键 {key}");
return TryGetValue(key, out _);
}
// 尝试添加键值对
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryAdd(TKey key, TValue value) {
Console.WriteLine($"****尝试添加键: {key}, 值: {value}");
if (Unsafe.IsNullRef(ref key)) throw new ArgumentNullException(nameof(key));
var (h1, h2) = GetHashes(key);
return TryInsert(key, value, h1, h2);
}
// 内部插入实现
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryInsert(TKey key, TValue value, long h1, byte h2) {
if (ShouldExpand()) {
Console.WriteLine($"需要扩容,当前计数: {_count}, 组数: {_groupCount}");
Resize();
}
var groupIndex = h1 & (_groupCount - 1);
Console.WriteLine($"初始组索引: {groupIndex}");
// 预取第一个Group
Group<TKey, TValue>.PrefetchNextGroup(&_table[groupIndex]);
for (int i = 0; i < _groupCount; i++) {
var probeIndex = (groupIndex + i) & (_groupCount - 1);
Console.WriteLine($"探测组 {probeIndex}");
// 预取下一个Group(如果有)
if (i + 1 < _groupCount) {
var nextProbeIndex = (groupIndex + i + 1) & (_groupCount - 1);
Group<TKey, TValue>.PrefetchNextGroup(&_table[nextProbeIndex]);
}
ref Group<TKey, TValue> group = ref _table[probeIndex];
int existingSlot = group.FindSlot(h2, h1, key, _comparer);
if (existingSlot >= 0) {
Console.WriteLine($"键 {key} 已存在于组 {probeIndex} 槽位 {existingSlot}");
return false;
}
int slot = group.FindEmptyOrTombstone();
if (slot >= 0) {
Console.WriteLine($"在组 {probeIndex} 槽位 {slot} 插入键 {key}");
Insert(ref group, slot, key, value, h1, h2);
return true;
}
else {
Console.WriteLine($"组 {probeIndex} 已满");
}
}
Console.WriteLine("所有组都已满,需要再次扩容...程序写错才会触发");
Resize();
return TryInsert(key, value, h1, h2);
}
// 内部插入,实际插入
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Insert(ref Group<TKey, TValue> group, int slot, TKey key, TValue value, long h1, byte h2) {
if (group.GetMetadata(slot) == SwissConfig.Tombstone) {
Console.WriteLine($"替换墓碑槽位 {slot}");
_tombstones--;
}
// group.H1Array[slot] = h1;
group.Keys[slot] = key;
group.Values[slot] = value;
group.SetMetadata(slot, h2);
_count++;
Console.WriteLine($"插入成功,当前计数: {_count}, 墓碑数: {_tombstones}");
}
// 添加或更新键值对
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool AddOrUpdate(TKey key, TValue value) {
if (Unsafe.IsNullRef(ref key)) throw new ArgumentNullException(nameof(key));
Console.WriteLine($"添加或更新键: {key}, 值: {value}");
var (h1, h2) = GetHashes(key);
var groupIndex = h1 & (_groupCount - 1);
Console.WriteLine($"初始组索引: {groupIndex}");
// 预取第一个Group
Group<TKey, TValue>.PrefetchNextGroup(&_table[groupIndex]);
for (int i = 0; i < _groupCount; i++) {
var probeIndex = (groupIndex + i) & (_groupCount - 1);
// 预取下一个Group(如果有)
if (i + 1 < _groupCount) {
var nextProbeIndex = (groupIndex + i + 1) & (_groupCount - 1);
Group<TKey, TValue>.PrefetchNextGroup(&_table[nextProbeIndex]);
}
ref Group<TKey, TValue> group = ref _table[probeIndex];
int slot = group.FindSlot(h2, h1, key, _comparer);
if (slot >= 0) {
Console.WriteLine($"更新组 {probeIndex} 槽位 {slot} 的值");
group.Values[slot] = value;
return false;
}
slot = group.FindEmptyOrTombstone();
if (slot >= 0) {
Console.WriteLine($"在组 {probeIndex} 槽位 {slot} 插入新键值对");
Insert(ref group, slot, key, value, h1, h2);
return true;
}
}
Console.WriteLine("所有组都已满,需要扩容");
Resize();
return AddOrUpdate(key, value);
}
// 移除指定键
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Remove(TKey key) {
if (Unsafe.IsNullRef(ref key)) throw new ArgumentNullException(nameof(key));
Console.WriteLine($"****尝试移除键: {key}");
if (_table == null) return false;
var (h1, h2) = GetHashes(key);
var groupIndex = h1 & (_groupCount - 1);
Console.WriteLine($"初始组索引: {groupIndex}");
// 预取第一个Group
Group<TKey, TValue>.PrefetchNextGroup(&_table[groupIndex]);
for (int i = 0; i < _groupCount; i++) {
var probeIndex = (groupIndex + i) & (_groupCount - 1);
// 预取下一个Group(如果有)
if (i + 1 < _groupCount) {
var nextProbeIndex = (groupIndex + i + 1) & (_groupCount - 1);
Group<TKey, TValue>.PrefetchNextGroup(&_table[nextProbeIndex]);
}
int slot = _table[probeIndex].FindSlot(h2, h1, key, _comparer);
if (slot >= 0) {
Console.WriteLine($"在组 {probeIndex} 槽位 {slot} 找到键 {key},执行移除");
RemoveAt(probeIndex, slot);
return true;
}
if (_table[probeIndex].FindEmptyOrTombstone() >= 0)
break;
}
Console.WriteLine($"未找到键 {key},移除失败");
return false;
}
// 移除指定位置的元素
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void RemoveAt(long groupIndex, int slot) {
Console.WriteLine($"移除组 {groupIndex} 槽位 {slot}");
_table[groupIndex].SetMetadata(slot, SwissConfig.Tombstone);
_count--;
_tombstones++;
Console.WriteLine($"移除完成,当前计数: {_count}, 墓碑数: {_tombstones}");
// 只有在自动缩容模式下才检查是否需要缩容
if (AutoShrink && ShouldShrink()) {
Console.WriteLine("自动缩容条件满足,执行缩容");
Resize();
}
}
// 手动缩容方法
public void Shrink() {
Console.WriteLine("手动缩容检查");
if (ShouldShrink()) {
Console.WriteLine("手动缩容条件满足,执行缩容");
Resize();
}
else {
Console.WriteLine("手动缩容条件不满足,跳过");
}
}
// 检查是否需要扩容
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool ShouldExpand() {
bool should = _count > (int)(_groupCount * SwissConfig.GroupSize * SwissConfig.MaxCap);
if (should) Console.WriteLine($"扩容检查: 当前计数 {_count} > {_groupCount * SwissConfig.GroupSize * SwissConfig.MaxCap}");
return should;
}
// 检查是否需要缩容
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool ShouldShrink() {
bool should = _count < (int)(_groupCount * SwissConfig.GroupSize * SwissConfig.MinCap);
if (should) Console.WriteLine($"缩容检查: 当前计数 {_count} < {_groupCount * SwissConfig.GroupSize * SwissConfig.MinCap}");
return should;
}
// 调整哈希表大小
private void Resize() {
Console.WriteLine($"开始调整大小,当前组数: {_groupCount}, 元素数: {_count}, 墓碑数: {_tombstones}");
int newGroupCount = ShouldShrink()
? Math.Max(1, _groupCount / 2)
: _groupCount * 2;
newGroupCount = (int)BitOperations.RoundUpToPowerOf2((uint)newGroupCount);
Console.WriteLine($"新组数计算为: {newGroupCount} ({(ShouldShrink() ? "缩容" : "扩容")})");
var newTable = (Group<TKey, TValue>*)NativeMemory.Alloc(
(nuint)(newGroupCount * sizeof(Group<TKey, TValue>)),
(nuint)16);
if (newTable == null)
throw new OutOfMemoryException("Failed to allocate memory for resized table");
try {
Group<TKey, TValue>* oldTable = _table;
int oldGroupCount = _groupCount;
Console.WriteLine("初始化新表的Groups...");
for (int i = 0; i < newGroupCount; i++) {
newTable[i] = *Group<TKey, TValue>.Create();
}
int migratedCount = 0;
Console.WriteLine("开始迁移数据...");
if (oldTable != null) {
for (int i = 0; i < oldGroupCount; i++) {
Console.WriteLine($"迁移组 {i}...");
for (int slot = 0; slot < SwissConfig.GroupSize; slot++) {
byte meta = oldTable[i].GetMetadata(slot);
if (meta == SwissConfig.Empty || meta == SwissConfig.Tombstone)
continue;
TKey key = oldTable[i].Keys[slot];
TValue value = oldTable[i].Values[slot];
// 由于可变类型,这里需要重新计算hash
var (h1, h2) = GetHashes(key);
Console.WriteLine($"迁移键: {key}, h1={h1}, h2={h2}");
long newGroupIndex = h1 & (newGroupCount - 1);
ref Group<TKey, TValue> newGroup = ref newTable[newGroupIndex];
int newSlot = newGroup.FindEmptyOrTombstone();
if (newSlot == -1) {
throw new InvalidOperationException("Critical error: no empty slot during resize");
}
Console.WriteLine($"将键 {key} 迁移到新组 {newGroupIndex} 槽位 {newSlot}");
// newGroup.H1Array[newSlot] = h1;
newGroup.Keys[newSlot] = key;
newGroup.Values[newSlot] = value;
newGroup.SetMetadata(newSlot, h2);
migratedCount++;
}
}
for (int i = 0; i < oldGroupCount; i++) {
oldTable[i].Destroy();
}
NativeMemory.Free(oldTable);
oldTable = null;
oldGroupCount = 0;
}
_table = newTable;
_groupCount = newGroupCount;
_count = migratedCount;
_tombstones = 0; // 新表无墓碑
Console.WriteLine($"调整大小完成,新组数: {_groupCount}, 迁移元素数: {migratedCount}");
}
catch (Exception ex) {
Console.WriteLine($"调整大小过程中发生错误: {ex.Message}");
// 清理部分初始化的新表
for (int i = 0; i < newGroupCount; i++) {
newTable[i].Destroy();
}
NativeMemory.Free(newTable);
newTable = null;
throw;
}
}
// 清空哈希表
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Clear() {
Console.WriteLine("清空哈希表");
if (_table == null) return;
for (int i = 0; i < _groupCount; i++) {
_table[i].Clear();
}
_count = 0;
_tombstones = 0;
Console.WriteLine("哈希表已清空");
}
// 获取枚举器
public Enumerator GetEnumerator() => new Enumerator(_table, _groupCount);
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()
=> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// 枚举器实现
public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>, IEnumerator {
private readonly Group<TKey, TValue>* _groups;
private readonly int _groupCount;
private int _currentGroup;
private int _currentSlot;
private KeyValuePair<TKey, TValue> _current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Enumerator(Group<TKey, TValue>* groups, int groupCount) {
_groups = groups;
_groupCount = groupCount;
_currentGroup = 0;
_currentSlot = -1;
_current = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext() {
while (_currentGroup < _groupCount) {
ref Group<TKey, TValue> group = ref _groups[_currentGroup];
// 使用局部变量减少数组边界检查
int slot = _currentSlot + 1;
while (slot < SwissConfig.GroupSize) {
byte meta = group.GetMetadata(slot);
if (meta != SwissConfig.Empty && meta != SwissConfig.Tombstone) {
_currentSlot = slot;
_current = new KeyValuePair<TKey, TValue>(
group.Keys[slot],
group.Values[slot]);
return true;
}
slot++;
}
_currentGroup++;
_currentSlot = -1;
}
_current = default;
return false;
}
public readonly KeyValuePair<TKey, TValue> Current => _current;
// 提供ref返回版本以支持高性能场景
public ref readonly KeyValuePair<TKey, TValue> CurrentRef {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get {
// 使用局部变量避免直接返回字段引用
ref var localRef = ref _current;
return ref localRef;
}
}
readonly object IEnumerator.Current => Current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void Dispose() { }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset() {
_currentGroup = 0;
_currentSlot = -1;
_current = default;
}
// 这里可以实现一个双调排序,因为具备2n性质,所以输出一个SortArray
}
}
public class Program {
public static void Main() {
// 测试自动缩容模式
// TestAutoShrinkMode();
// 测试手动缩容模式
// TestManualShrinkMode();
// 测试基本功能
TestBasicFunctionality();
}
// 测试自动缩容模式
public static void TestAutoShrinkMode() {
Console.WriteLine("\n=== 测试自动缩容模式 ===");
var map = new SwissTable<int, string>();
map.AutoShrink = true;
// 添加元素
for (int i = 1; i <= 20; i++) {
map.TryAdd(i, $"Value_{i}");
}
Console.WriteLine($"添加后元素数量: {map.Count}");
// 移除元素(会自动触发缩容)
for (int i = 1; i <= 15; i++) {
map.Remove(i);
}
Console.WriteLine($"移除后元素数量: {map.Count}");
}
// 测试手动缩容模式
public static void TestManualShrinkMode() {
Console.WriteLine("\n=== 测试手动缩容模式 ===");
var map = new SwissTable<int, string>();
map.AutoShrink = false;
// 添加元素
for (int i = 1; i <= 20; i++) {
map.TryAdd(i, $"Value_{i}");
}
Console.WriteLine($"添加后元素数量: {map.Count}");
// 移除元素(不会自动缩容)
for (int i = 1; i <= 15; i++) {
map.Remove(i);
}
Console.WriteLine($"移除后元素数量: {map.Count} (未缩容)");
// 手动缩容
map.Shrink();
Console.WriteLine($"手动缩容后元素数量: {map.Count}");
}
// 测试基本功能
public static void TestBasicFunctionality() {
Console.WriteLine("\n=== 测试int类型键及扩容后查找 ===");
var intMap = new SwissTable<int, string>();
int num = 0xFF;
if (!intMap.TryAdd(num, $"Value_{num}")) {
Console.WriteLine($"添加键{num}失败!");
}
// 添加足够多的元素以触发多次扩容
int targetCount = 20;
for (int i = 0; i <= targetCount; i++) {
if (!intMap.TryAdd(i, $"Value_{i}")) {
Console.WriteLine($"添加键{i}失败!");
break;
}
}
Console.WriteLine($"\n=== 最终验证所有键 ===");
bool allFound = true;
for (int i = 0; i <= targetCount; i++) {
if (!intMap.TryGetValue(i, out _)) {
Console.WriteLine($"最终验证失败!键{i}未找到");
allFound = false;
break;
}
}
Console.WriteLine(allFound ? "所有键在扩容后都能正确找到" : "存在扩容后丢失的键");
Console.WriteLine("\n=== 遍历所有元素 ===");
foreach(var item in intMap) {
Console.WriteLine($"{item}");
}
Console.WriteLine("\n测试完成!");
}
}