2012-03-28 17 views
34

Para el código como este:¿Por qué se generan AND instrucciones?

int res = 0; 
for (int i = 0; i < 32; i++) 
{ 
    res += 1 << i; 
} 

Este código se genera (modo de lanzamiento, sin depurador asociado, de 64 bits):

xor edx,edx 
mov r8d,1 
_loop: 
lea ecx,[r8-1] 
and ecx,1Fh  ; why? 
mov eax,1 
shl eax,cl 
add edx,eax 
mov ecx,r8d 
and ecx,1Fh  ; why? 
mov eax,1 
shl eax,cl 
add edx,eax 
lea ecx,[r8+1] 
and ecx,1Fh  ; why? 
mov eax,1 
shl eax,cl 
add edx,eax 
lea ecx,[r8+2] 
and ecx,1Fh  ; why? 
mov eax,1 
shl eax,cl 
add edx,eax 
add r8d,4 
cmp r8d,21h 
jl _loop 

ahora puedo ver el punto de la mayoría de las instrucciones allí, pero lo que es con las instrucciones AND? ecx será nunca sea más de 0x1F en este código de todos modos, pero lo disculpo por no darme cuenta (y también por no darme cuenta de que el resultado es una constante), no es un compilador anticipado que puede gastar mucho tiempo en el análisis después de todo. Pero más importante aún, SHL con un operando de 32 bits enmascara cl por 0x1F ya. Entonces me parece que estos AND son completamente inútiles. ¿Por qué se generan? ¿Tienen algún propósito que me estoy perdiendo?

+2

Fuera de interés, ¿tiene un enlace sobre SHL ya enmascarado? Miré en un par de lugares pero no pude encontrar ninguna declaración al respecto. (Ataque eso, acabo de encontrarlo.) –

+0

No es un experto, así que disculpe si es una pregunta estúpida, pero cómo se llena el registro 'cl'. ¿Es la operación 'y' la manera más rápida de poblar eso quizás? – weston

+0

Este código generado en su totalidad me parece gibero. ¿Por qué dividir un lote de instrucciones simple en tantos subjuegos? No tengo Visual Studio disponible en este momento, pero realmente me cuesta creer que el compilador JIT produzca este tipo de ensamblaje ..... – Polity

Respuesta

27

El and ya está presente en el código CIL emitida por el compilador de C#:

IL_0009: ldc.i4.s 31 
    IL_000b: and 
    IL_000c: shl 

La especificación para la instrucción CIL shl dice:

El valor de retorno no se especifica si shiftAmount es mayor o igual que el tamaño valor.

El # spec C, sin embargo, define el desplazamiento de 32 bits para tomar el turno de contar mod 32:

Cuando el tipo de x es int o uint, el valor de desplazamiento está dado por la bajo orden cinco bits de conteo. En otras palabras, el recuento de turnos se calcula desde count & 0x1F.

En esta situación, el compilador C# realmente no puede hacer mucho mejor que emitir una operación explícita and. Lo mejor que puedes esperar es que el JITter lo note y optimice la redundancia and, pero eso lleva tiempo, y la velocidad de JIT es bastante importante. Así que considere esto el precio pagado por un sistema basado en JIT.

La verdadera pregunta, supongo, es por qué el CIL especifica la instrucción shl de esa manera, cuando C# y x86 especifican el comportamiento de truncamiento. Eso no lo sé, pero especulo que es importante que la especificación CIL evite especificar un comportamiento que puede JIT a algo costoso en algunos conjuntos de instrucciones. Al mismo tiempo, es importante que C# tenga el menor número posible de comportamientos indefinidos, porque la gente invariablemente termina usando dichos comportamientos indefinidos hasta que la próxima versión del compilador/framework/OS/lo cambie, rompiendo el código.

+0

En x64, shl trunca el valor de desplazamiento en 64 bits lo que puede ocasionar algunas sorpresas desagradables al cambiar los valores de 64 bits y pasar de x86 a x64 . –

+1

Si el CIL 'shl' se definió como truncado, seguramente lo más costoso que se debe hacer es lo mismo Y eso no se evitó ahora de todos modos? – harold

+0

@IgorSkochinsky Lo consideré, pero no tengo una especificación x64 a mano, y la única referencia que pude encontrar _seemed_ para sugerir que un 'shl eax' truncará a 32, al igual que lo haría en x86. ¿No es así? –

5

El compilador de C# tiene que insertar estas instrucciones AND al generar código intermedio (independiente de la máquina), porque el operador de desplazamiento izquierdo C# debe usar solo 5 bits menos significativos.

Al generar código x86, la optimización del compilador puede eliminar estas instrucciones innecesarias. Pero, al parecer, omite esta optimización (probablemente, porque no puede darse el lujo de dedicar demasiado tiempo al análisis).

10

x64 los núcleos ya aplican la máscara de 5 bits a la cantidad de desplazamiento.Del manual del procesador Intel, volumen 2B página 4-362:

El operando de destino puede ser un registro o una ubicación de memoria. El operando de recuento puede ser un valor inmediato o el registro CL. El recuento se enmascara a 5 bits (o 6 bits si está en modo de 64 bits y se utiliza REG.W). Se proporciona un código de operación de codificación especial para una cuenta de 1.

Así que es código de máquina que no es necesario. Desafortunadamente, el compilador C# no puede hacer suposiciones sobre el comportamiento del procesador y debe aplicar las reglas de lenguaje C#. Y genere IL cuyo comportamiento se especifica en la especificación CLI. ECMA-335, Partion III, capítulo 3.58 dice esto sobre el código de operación SHL:

La instrucción SHL cambia el valor (int32, Int64 o int nativo) dada por el número de bits especificado por shiftAmount. shiftAmount es de tipo int32 o native int. El valor de retorno no se especifica si shiftAmount es mayor o igual que el ancho del valor.

No especificado es el problema aquí. El comportamiento específico de empernado sobre los detalles de implementación no especificados produce el código innecesario. Técnicamente, el jitter podría optimizar el código de operación de distancia. Aunque eso es complicado, no conoce la regla del idioma. Cualquier lenguaje que especifique que no hay enmascaramiento tendrá dificultades para generar IL adecuada. Puede publicar en connect.microsoft.com para obtener la opinión del equipo de jitter sobre el asunto.

Cuestiones relacionadas