我正在使用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]++;
}
}
Run Code Online (Sandbox Code Playgroud)
在这里,调用以IncrementDictionary执行以下两项操作之一:
key,则创建一个值并将其初始化为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;
}
Run Code Online (Sandbox Code Playgroud)
注意:在使用它的实际生产代码中,优化器/编译器还会创建:int key2 = key;并key2在最后一行中使用.
好的,var已被取代Dictionary<int, int>,这是预期的.而if声明简化为增加return使用替代else.
但为什么heck是对原始字典创建的新参考?
Chr*_*air 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
}
Run Code Online (Sandbox Code Playgroud)
通过它们具有的局部变量赋值,它看起来更像是为了避免这种情况:
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;
}
Run Code Online (Sandbox Code Playgroud)
请注意,它并不完全满足所有线程安全要求.例如,在这种情况下,我们开始递增值,但dictionary类的引用已更改为一个全新的实例.但是,如果您需要更高级别的线程安全性,那么您需要实现自己的主动同步/锁定,这通常超出了编译器优化的范围.然而,根据我的判断,这一点并没有真正为处理增加任何重大打击(如果有的话)并避免这种情况.这可能尤其是如果dictionary是属性不是一个领域,因为它是在你的榜样,在这种情况下,它肯定是无法解决属性getter两次的优化.(无论如何,你的实际代码是使用字典的属性而不是你发布的字段吗?)
编辑:嗯,一个简单的方法:
public void IncrementDictionary()
{
dictionary[0]++;
}
Run Code Online (Sandbox Code Playgroud)
来自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
Run Code Online (Sandbox Code Playgroud)
我不完全确定(我不是一个IL wiz),但我认为这个dup调用基本上会在堆栈上加倍相同的字典引用,所以无论get和set调用都指向同一个字典.也许这就是ILSpy如何将其表示为C#代码(至少与行为相似,它或多或少相同).我认为.如果我错了,请纠正我,因为就像我说的那样,我不知道IL就像我的手背了.
编辑:总得来看,但它最终的要点是:++并+=没有一个原子操作,实际上是一个很好的协议比在C#中描绘执行的指令更为复杂.因此,为了确保每个get/increment/set步骤都在同一个字典实例上执行(正如您所期望的那样并且需要来自C#代码),对字典进行本地引用以避免运行字段"得到"操作两次,这可能导致指向一个新的实例.ILSpy如何描述索引+ =操作所涉及的所有工作都取决于它.