这个C#Dictionary<,>的重点是什么?优化?

时间:2012-06-27 22:55:54

标签: c#

我正在使用C#4.0。启用了Visual Studio中的“优化代码”。

在类中考虑以下代码:

Dictionary<int, int> dictionary = new Dictionary<int, int>();

public void IncrementDictionary(int key) {
    if (!dictionary.ContainsKey(key)) {
        dictionary[key] = 1;
    } else {
        dictionary[key]++;
    }
}

在这里,对IncrementDictionary的调用会执行以下两项操作之一:

  • 如果key没有值,则创建一个值并初始化为1.
  • 如果存在值,则值将增加1.

现在观察当我使用ILSpy反编译结果时会发生什么:

Dictionary<int, int> dictionary = new Dictionary<int, int>();

public void IncrementDictionary(int key) {
    if (!dictionary.ContainsKey(key)) {
        dictionary[key] = 1;
        return;
    }
    Dictionary<int, int> dictionary2;
    (dictionary2 = dictionary)[key] = dictionary2[key] + 1;
}

注意:在使用此实际生产代码中,优化器/编译器还会创建:int key2 = key;并在最后一行中使用key2

好的,var已被Dictionary<int, int>取代,这是预期的。并if语句已简化为添加return而非使用else

但为什么heck是对原始词典的新引用?

2 个答案:

答案 0 :(得分:11)

我猜这可能是为了避免竞争条件,如果你有:

dictionary[i] = dictionary[i] + 1

这不是原子的。分配到的dictionary可以在获得值后更改并递增。

想象一下这段代码:

public Dictionary<int, int> dictionary = new Dictionary<int, int>();

public void Increment()
{
    int newValue = dictionary[0] + 1;
    //meanwhile, right now in another thread: dictionary = new Dictionary<int, int>();
    dictionary[0] = newValue; //at this point, "dictionary" is actually pointing to a whole new instance
}

使用它们具有的局部变量赋值,它看起来更像是为了避免这种情况:

public void IncrementFix()
{
    var dictionary2 = dictionary;
    //in another thread: dictionary = new Dictionary<int, int>();
    //this is OK, dictionary2 is still pointing to the ORIGINAL dictionary
    int newValue = dictionary2[0] + 1;
    dictionary2[0] = newValue;
}

请注意,它并不完全满足所有线程安全要求。例如,在这种情况下,我们开始递增值,但类的dictionary引用已更改为一个全新的实例。但是,如果您需要更高级别的线程安全性,那么您需要实现自己的主动同步/锁定,这通常超出了编译器优化的范围。然而,根据我的判断,这一点并没有真正为处理增加任何重大打击(如果有的话)并避免这种情况。如果dictionary属性而不是示例中的字段,则可能尤其如此,在这种情况下,肯定是不能解析属性获取器两次的优化。 (实际上,您的实际代码是使用字典的属性而不是您发布的字段吗?)

编辑:嗯,对于一个简单的方法:

public void IncrementDictionary() 
{
    dictionary[0]++;
}

LINQPad报道的IL是:

IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  ldfld       UserQuery.dictionary
IL_0007:  dup         
IL_0008:  stloc.0     
IL_0009:  ldc.i4.0    
IL_000A:  ldloc.0     
IL_000B:  ldc.i4.0    
IL_000C:  callvirt    System.Collections.Generic.Dictionary<System.Int32,System.Int32>.get_Item
IL_0011:  ldc.i4.1    
IL_0012:  add         
IL_0013:  callvirt    System.Collections.Generic.Dictionary<System.Int32,System.Int32>.set_Item
IL_0018:  nop         
IL_0019:  ret         

我不完全确定(我不是IL wiz),但我认为dup调用基本上会使堆栈上的相同字典引用加倍,所以无论get和set调用都指向同一本字典。也许这就是ILSpy如何将其表示为C#代码(至少与行为相似,它或多或少相同)。我认为。如果我错了,请纠正我,因为就像我说的那样,我不知道IL就像我手背一样。

编辑:要运行,但最后的要点是:+++=不是原子操作,实际上执行指令比C#中描述的更复杂。因此,为了确保每个get / increment / set步骤都在同一个字典实例上执行(正如您所期望的那样并且需要来自C#代码),对字典进行本地引用以避免运行字段“得到“操作两次,这可能导致指向一个新的实例。 ILSpy如何描述索引+ =操作所涉及的所有工作都取决于它。

答案 1 :(得分:1)

您的编辑搞砸了您要显示的内容,但dictionary[key]++制作dictionary临时副本的原因是它无法知道Dictionary<int,int>的索引获取器}将更改字段dictionary。该规范表明,即使在索引get期间更改了字段dictionary,索引的put仍将在同一对象上执行。

BTW,.net确实应该(并且仍然应该)提供一种方法,通过这种方法,类可以将属性公开为refDictionary可以提供方法ActOnValue(TKey key, ActionByRef<TValue> action)ActOnValue<TParam>(TKey key, ActionByRef<TValue, TParam> action, ref TParam param)(假设ActionByRef<T>Action<T>类似,但参数声明为ref,同样如此ActionByRef<T1,T2>)。如果是这样,可以对集合对象执行读取 - 修改 - 写入,而无需两次索引到集合。不幸的是,没有以这种方式公开属性的标准约定,也没有任何语言支持。