用一个**“字典里高频更新某个值”**的场景来举例,这是 CollectionsMarshal 最容易体现价值的地方。
场景:统计玩家伤害
假设你有一个 Dictionary<string, int>,记录每个玩家的累计伤害,每一帧都会更新:
var damageMap = new Dictionary<string, int>();
void AddDamage(string player, int dmg)
{
if (damageMap.TryGetValue(player, out var value))
{
value += dmg;
damageMap[player] = value;
}
else
{
damageMap[player] = dmg;
}
}
这段代码的问题在于:
TryGetValue已经按 key 找到了一次 value;damageMap[player] = value又按 key 找了一次;- 如果 value 是 struct,
out var value还会先拷贝一份出来,改完再写回去。
用 CollectionsMarshal 优化
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
void AddDamage(string player, int dmg)
{
ref int value = ref CollectionsMarshal.GetValueRefOrAddDefault(damageMap, player, out bool exists);
if (!exists)
{
value = dmg;
}
else
{
value += dmg;
}
}
这里只查了一次 key,拿到的是字典内部的 ref int,直接原地修改:
value += dmg;
不需要再 damageMap[player] = value。
为什么更快
| 写法 | 查找次数 | 是否拷贝 | 是否适合高频更新 |
|---|---|---|---|
TryGetValue + dict[key] = value | 2 次 | struct 会拷贝 | 一般 |
GetValueRefOrAddDefault | 1 次 | 直接改引用 | 更适合 |
尤其在热循环里,比如每帧统计伤害、击杀、金币、经验,或者服务器高频处理玩家状态时,这种“一次查找 + 引用修改”的模式会减少哈希查找和临时拷贝。
注意事项
- 拿到
ref期间,不要往字典里Add、Remove,也不要触发扩容,否则引用可能失效。 - 不要把它当成普通字典 API 到处替换,只在确实高频、已经定位到瓶颈的地方用。
- 如果 value 是 class,优势会小一些;value 是 struct 时收益更明显。
测试代码
// 模拟一个 struct 值类型,避免编译器过度优化
public struct DamageRecord
{
public int Count;
public long LastTick;
}
[MemoryDiagnoser]
public class DictionaryUpdateBenchmarks
{
private const int DictSize = 1000;
private readonly int[] _keys;
private readonly Random _random = new(42);
private Dictionary<int, DamageRecord> _dictTraditional;
private Dictionary<int, DamageRecord> _dictMarshal;
private Dictionary<int, DamageRecord> _dictIndexer;
public DictionaryUpdateBenchmarks()
{
_keys = new int[10000];
for (var i = 0; i < _keys.Length; i++)
_keys[i] = _random.Next(DictSize);
}
[GlobalSetup]
public void Setup()
{
_dictTraditional = Enumerable.Range(0, DictSize)
.ToDictionary(i => i, i => new DamageRecord { Count = i, LastTick = i });
_dictMarshal = Enumerable.Range(0, DictSize)
.ToDictionary(i => i, i => new DamageRecord { Count = i, LastTick = i });
_dictIndexer = Enumerable.Range(0, DictSize)
.ToDictionary(i => i, i => new DamageRecord { Count = i, LastTick = i });
}
[Benchmark]
public void Traditional_TryGetValue()
{
var dict = _dictTraditional;
foreach (var key in _keys)
{
if (dict.TryGetValue(key, out var rec))
{
rec.Count++;
rec.LastTick = key;
dict[key] = rec;
}
}
}
[Benchmark]
public void CollectionsMarshal_GetValueRefOrNullRef()
{
var dict = _dictMarshal;
foreach (var key in _keys)
{
ref var rec = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key);
if (!Unsafe.IsNullRef(ref rec))
{
rec.Count++;
rec.LastTick = key;
}
}
}
[Benchmark]
public void Indexer_WriteBack()
{
var dict = _dictIndexer;
foreach (var key in _keys)
{
var rec = dict[key];
rec.Count++;
rec.LastTick = key;
dict[key] = rec;
}
}
}
| Method | Mean | Error | StdDev | Allocated |
|---|---|---|---|---|
| Traditional_TryGetValue | 93.65 us | 0.493 us | 0.437 us | - |
| CollectionsMarshal_GetValueRefOrNullRef | 44.80 us | 0.403 us | 0.377 us | - |
| Indexer_WriteBack | 96.04 us | 0.479 us | 0.448 us | - |
CollectionsMarshal快了一倍
521



被折叠的 条评论
为什么被折叠?



