C# 字典的原子 AddOrUpdate
Atomic AddOrUpdate for a C# Dictionary
假设如下代码:
if (myDictionary.ContainsKey(aKey))
myDictionary[aKey] = aValue;
else
myDictionary.Add(aKey, aValue);
这段代码访问字典两次,一次是判断aKey
是否存在,另一次是更新(如果存在)或者添加(如果不存在)。我想当这段代码只执行几次时,这种方法的性能是“可以接受的”。但是,在我的应用程序中,类似的代码大约执行了 50 万次。我分析了我的代码,它显示 80% 的 CPU 时间花在了这部分(见下图),因此这激发了改进。
注意,字典是lambdas
.
第一个解决方法很简单:
myDictionary[aKey] = aValue;
如果 aKey
存在,它的值被替换为 aValue
;如果不存在,则将 KeyValuePair
以 aKey
作为键,将 aValue
作为值添加到 myDictionary
。但是,这种方法有两个缺点:
首先,你不知道aKey
是否存在,这阻止了你额外的逻辑。例如,您不能根据此解决方法重写以下代码:
int addCounter = 0, updateCounter = 0;
if (myDictionary.ContainsKey(aKey))
{
myDictionary[aKey] = aValue;
addCounter++;
}
else
{
myDictionary.Add(aKey, aValue);
updateCounter++;
}
其次,更新不能是旧值的函数。例如,您不能执行类似于以下的逻辑:
if (myDictionary.ContainsKey(aKey))
myDictionary[aKey] = (myDictionary[aKey] * 2) + aValue;
else
myDictionary.Add(aKey, aValue);
第二种解决方法 是使用 ConcurrentDictionary
。很明显,使用 delegates
我们可以解决上述 second 的问题;但是,我仍然不清楚我们如何解决 第一个 问题。
提醒一下,我关心的是加速。鉴于只有一个线程使用此过程,我认为仅一个线程的并发(带锁)惩罚不值得使用 ConcurrentDictionary
.
我漏掉了一点吗?谁有更好的建议?
如果您真的想要 AddOrUpdate
方法,如 ConcurrentDictionary
中的方法,但没有使用一个的性能影响,您将必须自己实现这样的字典。
好消息是,由于 CoreCLR 是开源的,您可以从 CoreCLR repository 获取实际的 .Net 字典源并应用您自己的修改。好像不会那么难,看看那里的Insert
私有方法
一种可能的实现方式是(未经测试):
public void AddOrUpdate(TKey key, Func<TKey, TValue> adder, Func<TKey, TValue, TValue> updater) {
if( key == null ) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (buckets == null) Initialize(0);
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int targetBucket = hashCode % buckets.Length;
for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
entries[i].value = updater(key, entries[i].value);
version++;
return;
}
}
int index;
if (freeCount > 0) {
index = freeList;
freeList = entries[index].next;
freeCount--;
}
else {
if (count == entries.Length)
{
Resize();
targetBucket = hashCode % buckets.Length;
}
index = count;
count++;
}
entries[index].hashCode = hashCode;
entries[index].next = buckets[targetBucket];
entries[index].key = key;
entries[index].value = adder(key);
buckets[targetBucket] = index;
version++;
}
自 .NET 6 以来,有一种新方法 CollectionsMarshal.GetValueRefOrAddDefault
可以做到这一点。
示例用法:
Dictionary<string, string> dictionary = new Dictionary<string, string>();
ref string? dictionaryValue = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, "key", out bool exists);
//variable 'exists' is true if key was present, and false if it had to be added
if (exists)
{
//Update the value of dictionaryValue variable
dictionaryValue = dictionaryValue?.ToLowerCaseInvariant();
}
else
{
//assign new value
dictionaryValue = "test";
}
唯一的缺点是调用此方法后您无法决定不添加新值。如果缺少键,它总是在您的字典中创建占位符空值。您基本上必须分配这个新值,否则您的字典中将留下一个空条目。
假设如下代码:
if (myDictionary.ContainsKey(aKey))
myDictionary[aKey] = aValue;
else
myDictionary.Add(aKey, aValue);
这段代码访问字典两次,一次是判断aKey
是否存在,另一次是更新(如果存在)或者添加(如果不存在)。我想当这段代码只执行几次时,这种方法的性能是“可以接受的”。但是,在我的应用程序中,类似的代码大约执行了 50 万次。我分析了我的代码,它显示 80% 的 CPU 时间花在了这部分(见下图),因此这激发了改进。
lambdas
.
第一个解决方法很简单:
myDictionary[aKey] = aValue;
如果 aKey
存在,它的值被替换为 aValue
;如果不存在,则将 KeyValuePair
以 aKey
作为键,将 aValue
作为值添加到 myDictionary
。但是,这种方法有两个缺点:
首先,你不知道aKey
是否存在,这阻止了你额外的逻辑。例如,您不能根据此解决方法重写以下代码:
int addCounter = 0, updateCounter = 0;
if (myDictionary.ContainsKey(aKey))
{
myDictionary[aKey] = aValue;
addCounter++;
}
else
{
myDictionary.Add(aKey, aValue);
updateCounter++;
}
其次,更新不能是旧值的函数。例如,您不能执行类似于以下的逻辑:
if (myDictionary.ContainsKey(aKey))
myDictionary[aKey] = (myDictionary[aKey] * 2) + aValue;
else
myDictionary.Add(aKey, aValue);
第二种解决方法 是使用 ConcurrentDictionary
。很明显,使用 delegates
我们可以解决上述 second 的问题;但是,我仍然不清楚我们如何解决 第一个 问题。
提醒一下,我关心的是加速。鉴于只有一个线程使用此过程,我认为仅一个线程的并发(带锁)惩罚不值得使用 ConcurrentDictionary
.
我漏掉了一点吗?谁有更好的建议?
如果您真的想要 AddOrUpdate
方法,如 ConcurrentDictionary
中的方法,但没有使用一个的性能影响,您将必须自己实现这样的字典。
好消息是,由于 CoreCLR 是开源的,您可以从 CoreCLR repository 获取实际的 .Net 字典源并应用您自己的修改。好像不会那么难,看看那里的Insert
私有方法
一种可能的实现方式是(未经测试):
public void AddOrUpdate(TKey key, Func<TKey, TValue> adder, Func<TKey, TValue, TValue> updater) {
if( key == null ) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (buckets == null) Initialize(0);
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int targetBucket = hashCode % buckets.Length;
for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
entries[i].value = updater(key, entries[i].value);
version++;
return;
}
}
int index;
if (freeCount > 0) {
index = freeList;
freeList = entries[index].next;
freeCount--;
}
else {
if (count == entries.Length)
{
Resize();
targetBucket = hashCode % buckets.Length;
}
index = count;
count++;
}
entries[index].hashCode = hashCode;
entries[index].next = buckets[targetBucket];
entries[index].key = key;
entries[index].value = adder(key);
buckets[targetBucket] = index;
version++;
}
自 .NET 6 以来,有一种新方法 CollectionsMarshal.GetValueRefOrAddDefault
可以做到这一点。
示例用法:
Dictionary<string, string> dictionary = new Dictionary<string, string>();
ref string? dictionaryValue = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, "key", out bool exists);
//variable 'exists' is true if key was present, and false if it had to be added
if (exists)
{
//Update the value of dictionaryValue variable
dictionaryValue = dictionaryValue?.ToLowerCaseInvariant();
}
else
{
//assign new value
dictionaryValue = "test";
}
唯一的缺点是调用此方法后您无法决定不添加新值。如果缺少键,它总是在您的字典中创建占位符空值。您基本上必须分配这个新值,否则您的字典中将留下一个空条目。