高效地更新.NET词典中的绑定

本文关键字:绑定 更新 NET 高效 | 更新日期: 2023-09-27 18:29:08

我使用字典来累积键的出现次数,因此,核心操作是编写一个键值对,其中的值是前一个值加一,如果没有前一个数值,则仅为一。然而,这需要两个单独的字典操作(读和写),而我本可以只做一个(AddOrUpdate)。

我注意到并发字典支持AddOrUpdate,但普通的通用Dictionary似乎不支持

因此,对可变int的引用字典会更快。然而,这引入了不必要的引用,这意味着堆分配和写障碍。所以我猜有可能做得更好,但如果不从头开始重写Dictionary,我看不出该怎么做。我说得对吗?

高效地更新.NET词典中的绑定

您可以这样做:

private class Counter
{
  public string Key       { get ; set ; }
  public int    Frequency { get ; set ; }
}
...
Dictionary<string,Counter> frequencyTable = new Dictionary<string,Counter>() ;
...
string someKey = GetKeyToLookup() ;
Counter item = null ;
bool hit = frequencyTable.TryGetValue( someKey,out item ) ;
if ( !hit )
{
  item = new Counter{ Key=someKey,Frequency=0 } ;
}
++ item.Frequency ;

如果这还不够好,为什么要自己写呢?使用高性能C5收藏库。它是免费的(事实上,最初由微软资助),构建在微软的System.Collections.Generic接口上,其字典、集合和包支持FindOrAdd()语义。

  • Nuget:http://www.nuget.org/packages/C5/
  • 项目主页:http://www.itu.dk/research/c5/
  • 文件为ITU-TR-2006-76—C#和CLI的C5通用集合库:2008-02-10版本1.1.0。它有点过时了,因为它反映的是v1.1.1版本,而不是当前版本(2013年8月27日的v2.2版本)。不过,基本情况没有改变

正如Jim Mischel所提到的,不可能为更改字典的项值进行单一查找。ConcurrentDictionary.AddOrUpdate方法执行多个查找操作(反射源):

public TValue AddOrUpdate(TKey key, TValue addValue, Func<TKey, TValue, TValue> updateValueFactory)
{
    TValue local2;
    if (key == null)
    {
        throw new ArgumentNullException("key");
    }
    if (updateValueFactory == null)
    {
        throw new ArgumentNullException("updateValueFactory");
    }
    do
    {
        TValue local3;
        while (this.TryGetValue(key, out local3))
        {
            TValue newValue = updateValueFactory(key, local3);
            if (this.TryUpdate(key, newValue, local3))
            {
                return newValue;
            }
        }
    }
    while (!this.TryAddInternal(key, addValue, false, true, out local2));
    return local2;
}

我已经用并发字典和简单字典进行了性能测试:

IDictionary的AddOrUpdate扩展:

public static class DictionaryExtensions
{
    public static void AddOrUpdate<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, TValue initValue, Func<TKey, TValue, TValue> updateFunc)
    {
        TValue value;
        value = dict.TryGetValue(key, out value) ? updateFunc(key, value) : initValue;
        dict[key] = value;
    }
}

测试:

static void Main(string[] args)
{
    const int dictLength = 100000;
    const int testCount = 1000000;
    var cdict = new ConcurrentDictionary<string, int>(GetRandomData(dictLength));
    var dict = GetRandomData(dictLength).ToDictionary(x => x.Key, x => x.Value);
    var stopwatch = new Stopwatch();
    stopwatch.Start();
    foreach (var pair in GetRandomData(testCount))
        cdict.AddOrUpdate(pair.Key, 1, (x, y) => y+1);          
    stopwatch.Stop();
    Console.WriteLine("Concurrent dictionary: {0}", stopwatch.ElapsedMilliseconds);
    stopwatch.Reset();
    stopwatch.Start();
    foreach (var pair in GetRandomData(testCount))
        dict.AddOrUpdate(pair.Key, 1, (x, y) => y+1);   
    stopwatch.Stop();
    Console.WriteLine("Dictionary: {0}", stopwatch.ElapsedMilliseconds);
    Console.ReadLine();
}
static IEnumerable<KeyValuePair<string, int>> GetRandomData(int count)
{
    const int constSeed = 100;
    var randGenerator = new Random(constSeed);
    return Enumerable.Range(0, count).Select((x, ind) => new KeyValuePair<string, int>(randGenerator.Next().ToString() + "_" + ind, randGenerator.Next()));
}

我的环境测试结果(毫秒):

ConcurrentDictionary: 2504
Dictionary: 1351

如果使用引用类型,字典更新不需要多次查找:

假设您有一个Dictionary<string, Foo>,其中Foo是一个引用类型,并包含一个Count属性:

void UpdateCount(string key)
{
    Foo f;
    if (dict.TryGetValue(key, out f))
    {
        // do the update
        ++f.Count;
    }
    else
    {
        dict[key] = 1;
    }
}

如果您的值是值类型。。。那么,您必须处理值类型语义。这包括必须进行两次查找。

也就是说,查字典的速度相当快。如果这导致了一个问题,那么你必须要计算大量的事件。