2012-06-27 8 views
9

Estoy usando C# 4.0. "Optimizar código" en Visual Studio está activado.¿Cuál es el objetivo de este C# Dictionary <,> optimización?

considere el siguiente código, en una clase:

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

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

Aquí, una llamada a IncrementDictionary hace una de dos cosas:

  • Si no existe valor de key, a continuación, se crea un valor e inicializado en 1.
  • Si existe un valor, el valor se incrementa en 1.

Ahora observen lo que sucede cuando uso ILSpy descompilar el resultado:

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; 
} 

Nota: En el código de la producción real de usar esto, el optimizador/compilador también crea: int key2 = key; y utiliza key2 en la última línea.

Ok, var ha sido reemplazado por Dictionary<int, int>, que se espera. Y la declaración if se simplificó para agregar un return en lugar de usar un else.

¿Pero por qué diablos era una nueva referencia al diccionario original creado?

+3

Nota importante: no se crea un segundo diccionario. Por el contrario, se toma una segunda referencia al diccionario existente. – Jon

+0

@Jon: Correcto, mi error. –

+3

El ILSpy que publicó no es una representación exacta de lo que está sucediendo en el código CIL directamente. Es posible que esto sea un error o imperfección en el decompilador CIL-to-C#. ¿Puedes publicar el CIL para que podamos estar seguros? – Dai

Respuesta

11

supongo que podría ser evitar una condición de carrera en la que si usted tiene:

dictionary[i] = dictionary[i] + 1

No es atómica. El dictionary que se le asignó podría cambiar después de, obtuvo el valor y se incrementó.

Imagínese este código:

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 
} 

Con la asignación de variables locales que tienen, se parece más a esto para evitar la condición:

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; 
} 

Tenga en cuenta que no satisface plenamente todas hilo -los requisitos de seguridad. Por ejemplo, en este caso, comenzamos a incrementar el valor, pero la referencia dictionary en la clase ha cambiado a una instancia completamente nueva. Pero si necesita ese nivel más alto de seguridad de subprocesos, entonces necesita implementar su propia sincronización/bloqueo agresivo, que generalmente está fuera del alcance de las optimizaciones del compilador. Sin embargo, este, por lo que puedo decir, realmente no agrega ningún gran golpe (si lo hay) al procesamiento y evita esta condición. Esto podría ser especialmente el caso si dictionary es una propiedad no es un campo como lo es en su ejemplo, en cuyo caso definitivamente sería una optimización para no resolver el getter de propiedad dos veces. (Por casualidad, ¿su código real usa una propiedad para el diccionario en lugar del campo que publicó?)

EDIT: Bueno, por un método simple:

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

La IL informó de LINQPad es:

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   

No estoy del todo seguro (no soy un mago de la IL) , pero creo que la llamada a dup básicamente duplica la misma referencia de diccionario en la pila, por lo tanto, independientemente de que las llamadas de obtención y establecimiento apunten al mismo diccionario. Tal vez así es como ILSpy lo representa como código C# (es más o menos lo mismo al menos como lo hace el comportamiento). Creo. Por favor, corrígeme si me equivoco porque, como dije, todavía no conozco a IL como la palma de mi mano.

EDITAR: Tengo que ejecutar, pero la esencia es: ++ y += no son operaciones atómicas y en realidad es mucho más complicado en las instrucciones ejecutadas que las que se muestran en C#. Como tal, para asegurarse de que cada uno de los pasos get/increment/set se realiza en la misma instancia de diccionario (como era de esperar y requiere del código C#) se hace una referencia local al diccionario para evitar ejecutar el campo " obtener "operación dos veces" lo que podría dar como resultado apuntar a una nueva instancia. Cómo ILSpy muestra que todo el trabajo relacionado con una operación indexada + = está a la altura.

+0

No, 'dictionary2' todavía apunta al diccionario original. –

+2

Derecha. 'Increment()' controla 'dictionary2': es una referencia local al diccionario original. –

+0

Ah, ya veo. Gran respuesta. –

1

Tu edición en mal estado lo que estaba tratando de demostrar, pero la razón por la que dictionary[key]++ hace una copia temporal de dictionary es que no tiene ninguna manera de saber si el captador indexada para Dictionary<int,int> cambiará campo dictionary. La especificación indica que incluso si el campo dictionary se cambia durante la obtención indexada, la posición indexada se seguirá ejecutando en el mismo objeto.

Por cierto, .net realmente debería tener (y todavía debe) proporcionar un medio por el cual las clases puedan exponer propiedades como ref. Sería posible que Dictionary proporcionara los métodos ActOnValue(TKey key, ActionByRef<TValue> action) y ActOnValue<TParam>(TKey key, ActionByRef<TValue, TParam> action, ref TParam param) (asumiendo que ActionByRef<T> es como pero el parámetro declaró ref, y también ActionByRef<T1,T2>). Si lo hiciera, uno podría realizar una lectura-modificación-escritura en un objeto de colección sin tener que indexar dos veces en la colección. Desafortunadamente, no existe una convención estándar para exponer las propiedades de esa manera, ni hay soporte de idiomas.

+0

¿Se ha corregido la edición ahora? –

+0

@SimpleCoder: se ve mejor. – supercat

+0

Ok gracias. Lo que dices tiene sentido, nunca había pensado en eso. –

Cuestiones relacionadas