c# cil compound-assignment

c# - ¿Hay una explicación para los operadores en línea en "k+= c+= k+= c;”?



cil compound-assignment (7)

¿Cuál es la explicación para el resultado de la siguiente operación?

k += c += k += c;

Estaba tratando de entender el resultado de salida del siguiente código:

int k = 10; int c = 30; k += c += k += c; //k=80 instead of 110 //c=70

y actualmente me cuesta entender por qué el resultado de "k" es 80. ¿Por qué asignar k = 40 no funciona (en realidad, Visual Studio me dice que ese valor no se usa en ningún otro lugar)?

¿Por qué es k 80 y no 110?

Si divido la operación para:

k+=c; c+=k; k+=c;

el resultado es k = 110.

Estaba tratando de mirar a través del CIL , pero no soy tan profundo en la interpretación del CIL generado y no puedo obtener algunos detalles:

// [11 13 - 11 24] IL_0001: ldc.i4.s 10 IL_0003: stloc.0 // k // [12 13 - 12 24] IL_0004: ldc.i4.s 30 IL_0006: stloc.1 // c // [13 13 - 13 30] IL_0007: ldloc.0 // k expect to be 10 IL_0008: ldloc.1 // c IL_0009: ldloc.0 // k why do we need the second load? IL_000a: ldloc.1 // c IL_000b: add // I expect it to be 40 IL_000c: dup // What for? IL_000d: stloc.0 // k - expected to be 40 IL_000e: add IL_000f: dup // I presume the "magic" happens here IL_0010: stloc.1 // c = 70 IL_0011: add IL_0012: stloc.0 // k = 80??????


En primer lugar, las respuestas de Henk y Olivier son correctas; Quiero explicarlo de una manera ligeramente diferente. Específicamente, quiero abordar este punto que usted hizo. Usted tiene este conjunto de declaraciones:

int k = 10; int c = 30; k += c += k += c;

Y luego concluyes incorrectamente que esto debería dar el mismo resultado que este conjunto de declaraciones:

int k = 10; int c = 30; k += c; c += k; k += c;

Es informativo ver cómo se equivocó y cómo hacerlo correctamente. La forma correcta de descomponerlo es así.

Primero, reescribe el más externo + =

k = k + (c += k += c);

Segundo, reescriba el + externo. Espero que esté de acuerdo en que x = y + z debe ser siempre lo mismo que "evaluar y a temporal, evaluar z a temporal, sumar los temporales, asignar la suma a x" . Así que vamos a hacer eso muy explícito:

int t1 = k; int t2 = (c += k += c); k = t1 + t2;

Asegúrese de que esté claro, porque este es el paso en el que se equivocó . Al dividir las operaciones complejas en operaciones más simples, debe asegurarse de hacerlo lenta y cuidadosamente y no omitir pasos . Saltando pasos es donde cometemos errores.

Bien, ahora divida la asignación a t2, otra vez, lentamente y con cuidado.

int t1 = k; int t2 = (c = c + (k += c)); k = t1 + t2;

La asignación asignará el mismo valor a t2 que a c, así que digamos que:

int t1 = k; int t2 = c + (k += c); c = t2; k = t1 + t2;

Genial. Ahora rompe la segunda línea:

int t1 = k; int t3 = c; int t4 = (k += c); int t2 = t3 + t4; c = t2; k = t1 + t2;

Genial, estamos progresando. Desglosa la asignación a t4:

int t1 = k; int t3 = c; int t4 = (k = k + c); int t2 = t3 + t4; c = t2; k = t1 + t2;

Ahora rompe la tercera línea:

int t1 = k; int t3 = c; int t4 = k + c; k = t4; int t2 = t3 + t4; c = t2; k = t1 + t2;

Y ahora podemos ver todo el asunto:

int k = 10; // 10 int c = 30; // 30 int t1 = k; // 10 int t3 = c; // 30 int t4 = k + c; // 40 k = t4; // 40 int t2 = t3 + t4; // 70 c = t2; // 70 k = t1 + t2; // 80

Entonces, cuando hayamos terminado, k es 80 yc es 70.

Ahora veamos cómo se implementa esto en la IL:

int t1 = k; int t3 = c; is implemented as ldloc.0 // stack slot 1 is t1 ldloc.1 // stack slot 2 is t3

Ahora esto es un poco complicado:

int t4 = k + c; k = t4; is implemented as ldloc.0 // load k ldloc.1 // load c add // sum them to stack slot 3 dup // t4 is stack slot 3, and is now equal to the sum stloc.0 // k is now also equal to the sum

Podríamos haber implementado lo anterior como

ldloc.0 // load k ldloc.1 // load c add // sum them stloc.0 // k is now equal to the sum ldloc.0 // t4 is now equal to k

pero usamos el truco "dup" porque hace que el código sea más corto, facilita el jitter y obtenemos el mismo resultado. En general, el generador de código C # intenta mantener los temporales "efímeros" en la pila tanto como sea posible. Si le resulta más fácil seguir el IL con menos efímeros, desactive las optimizaciones y el generador de código será menos agresivo.

Ahora tenemos que hacer el mismo truco para obtener c:

int t2 = t3 + t4; // 70 c = t2; // 70 is implemented as: add // t3 and t4 are the top of the stack. dup stloc.1 // again, we do the dup trick to get the sum in // both c and t2, which is stack slot 2.

y finalmente:

k = t1 + t2; is implemented as add // stack slots 1 and 2 are t1 and t2. stloc.0 // Store the sum to k.

Como no necesitamos la suma para nada más, no la engañamos. La pila ahora está vacía, y estamos al final de la declaración.

La moraleja de la historia es: cuando intenta comprender un programa complicado, siempre analice las operaciones una por una . No tome atajos; Ellos te llevarán por mal camino.


Probé el ejemplo con gcc y pgcc y obtuve 110. Revisé el IR que generaron, y el compilador expandió el expr a:

k = 10; c = 30; k = c+k; c = c+k; k = c+k;

que me parece razonable.


Puedes resolver esto contando.

a = k += c += k += c

Hay dos c s y dos k s asi que

a = 2c + 2k

Y, como consecuencia de los operadores del lenguaje, k también es igual a 2c + 2k

Esto funcionará para cualquier combinación de variables en este estilo de cadena:

a = r += r += r += m += n += m

Asi que

a = 2m + n + 3r

Y r será igual a igual.

Puede calcular los valores de los otros números calculando solo hasta su asignación más a la izquierda. Entonces m es igual a 2m + n y n es igual a n + m .

Esto demuestra que k += c += k += c; es diferente a k += c; c += k; k += c; k += c; c += k; k += c; Y por eso recibes diferentes respuestas.

Algunas personas en los comentarios parecen preocuparse de que pueda tratar de generalizar en exceso este acceso directo a todos los tipos posibles de adición. Por lo tanto, dejaré en claro que este acceso directo solo es aplicable a esta situación, es decir, encadenar asignaciones de suma para los tipos de números integrados. No funciona (necesariamente) si agrega otros operadores, por ejemplo, () o + , o si llama a funciones o si ha anulado += , o si está usando algo diferente a los tipos de números básicos. Sólo está destinado a ayudar con la situación particular en la pregunta .


Respuesta simple: Reemplaza vars con valores y lo tienes:

int k = 10; int c = 30; k += c += k += c; 10 += 30 += 10 += 30 = 10 + 30 + 10 + 30 = 80 !!!


Se reduce a: ¿se aplica el primer += al k original o al valor que se calculó más a la derecha?

La respuesta es que aunque las asignaciones se enlazan de derecha a izquierda, las operaciones continúan de izquierda a derecha.

Así que el extremo izquierdo += está ejecutando 10 += 70 .


Una operación como a op= b; es equivalente a a = a op b; . Una asignación se puede usar como una declaración o como una expresión, mientras que como una expresión produce el valor asignado. Su declaración ...

k += c += k += c;

... puede, ya que el operador de asignación es asociativo por derecho, también puede escribirse como

k += (c += (k += c));

o (expandido)

k = k + (c = c + (k = k + c)); 10 → 30 → 10 → 30 // operand evaluation order is from left to right | | ↓ ↓ | ↓ 40 ← 10 + 30 // operator evaluation ↓ 70 ← 30 + 40 80 ← 10 + 70

Donde durante toda la evaluación se utilizan los valores antiguos de las variables involucradas. Esto es especialmente cierto para el valor de k (vea mi revisión de la IL a continuación y el link Wai Ha Lee proporcionó). Por lo tanto, no está obteniendo 70 + 40 (nuevo valor de k ) = 110, sino 70 + 10 (antiguo valor de k ) = 80.

El punto es que (según la spec C #) "Los operandos en una expresión se evalúan de izquierda a derecha" (los operandos son las variables k en nuestro caso). Esto es independiente de la precedencia y la asociatividad del operador, que en este caso dictan un orden de ejecución de derecha a izquierda. (Vea los comentarios a la answer Eric Lippert en esta página).

Ahora echemos un vistazo a la IL. IL asume una máquina virtual basada en la pila, es decir, no utiliza registros.

IL_0007: ldloc.0 // k (is 10) IL_0008: ldloc.1 // c (is 30) IL_0009: ldloc.0 // k (is 10) IL_000a: ldloc.1 // c (is 30)

La pila ahora tiene este aspecto (de izquierda a derecha; la parte superior de la pila está a la derecha)

10 30 10 30

IL_000b: add // pops the 2 top (right) positions, adds them and pushes the sum back

10 30 40

IL_000c: dup

10 30 40 40

IL_000d: stloc.0 // k <-- 40

10 30 40

IL_000e: add

10 70

IL_000f: dup

10 70 70

IL_0010: stloc.1 // c <-- 70

10 70

IL_0011: add

80

IL_0012: stloc.0 // k <-- 80

Tenga en cuenta que IL_000c: dup , IL_000d: stloc.0 , es decir, la primera asignación a k , podría optimizarse. Probablemente esto se hace para las variables por el jitter al convertir IL a código de máquina.

Tenga en cuenta también que todos los valores requeridos por el cálculo se insertan en la pila antes de realizar cualquier asignación o se calculan a partir de estos valores. Los valores asignados (por stloc ) nunca se reutilizan durante esta evaluación. stloc hace estallar la parte superior de la pila.

La salida de la siguiente prueba de consola es (Modo de Release con optimizaciones activadas)

evaluando k (10)
evaluando c (30)
evaluando k (10)
evaluando c (30)
40 asignados a k
70 asignados a c
80 asignados a k

private static int _k = 10; public static int k { get { Console.WriteLine($"evaluating k ({_k})"); return _k; } set { Console.WriteLine($"{value} assigned to k"); _k = value; } } private static int _c = 30; public static int c { get { Console.WriteLine($"evaluating c ({_c})"); return _c; } set { Console.WriteLine($"{value} assigned to c"); _c = value; } } public static void Test() { k += c += k += c; }


para este tipo de asignaciones en cadena, debe asignar los valores desde el inicio en el lado más a la derecha. Tiene que asignar y calcular y asignarlo al lado izquierdo, y continuar hasta la final (asignación más a la izquierda), seguro que se calcula como k = 80.