Entonces, tenía este código:
constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
for(int i = 0; i < N; ++i) {
sum[i] = a[i] + b[i];
}
}
void f2(char* sum, char* a, char* b) {
char* end = sum + N;
while(sum != end) {
*sum++ = *a++ + *b++;
}
}
Quería ver el código que generaría GCC 4.7.2. Así que ejecuté g++ -march=native -O3 -masm=intel -S a.c++ -std=c++11
y obtuve el siguiente resultado:
.file "a.c++"
.intel_syntax noprefix
.text
.p2align 4,,15
.globl _Z2f1PcS_S_
.type _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
.cfi_startproc
lea rcx, [rdx+16]
lea rax, [rdi+16]
cmp rdi, rcx
setae r8b
cmp rdx, rax
setae cl
or cl, r8b
je .L5
lea rcx, [rsi+16]
cmp rdi, rcx
setae cl
cmp rsi, rax
setae al
or cl, al
je .L5
xor eax, eax
.p2align 4,,10
.p2align 3
.L3:
movdqu xmm0, XMMWORD PTR [rdx+rax]
movdqu xmm1, XMMWORD PTR [rsi+rax]
paddb xmm0, xmm1
movdqu XMMWORD PTR [rdi+rax], xmm0
add rax, 16
cmp rax, 992
jne .L3
mov ax, 8
mov r9d, 992
.L2:
sub eax, 1
lea rcx, [rdx+r9]
add rdi, r9
lea r8, [rax+1]
add rsi, r9
xor eax, eax
.p2align 4,,10
.p2align 3
.L4:
movzx edx, BYTE PTR [rcx+rax]
add dl, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], dl
add rax, 1
cmp rax, r8
jne .L4
rep
ret
.L5:
mov eax, 1000
xor r9d, r9d
jmp .L2
.cfi_endproc
.LFE0:
.size _Z2f1PcS_S_, .-_Z2f1PcS_S_
.p2align 4,,15
.globl _Z2f2PcS_S_
.type _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
.cfi_startproc
lea rcx, [rdx+16]
lea rax, [rdi+16]
cmp rdi, rcx
setae r8b
cmp rdx, rax
setae cl
or cl, r8b
je .L19
lea rcx, [rsi+16]
cmp rdi, rcx
setae cl
cmp rsi, rax
setae al
or cl, al
je .L19
xor eax, eax
.p2align 4,,10
.p2align 3
.L17:
movdqu xmm0, XMMWORD PTR [rdx+rax]
movdqu xmm1, XMMWORD PTR [rsi+rax]
paddb xmm0, xmm1
movdqu XMMWORD PTR [rdi+rax], xmm0
add rax, 16
cmp rax, 992
jne .L17
add rdi, 992
add rsi, 992
add rdx, 992
mov r8d, 8
.L16:
xor eax, eax
.p2align 4,,10
.p2align 3
.L18:
movzx ecx, BYTE PTR [rdx+rax]
add cl, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], cl
add rax, 1
cmp rax, r8
jne .L18
rep
ret
.L19:
mov r8d, 1000
jmp .L16
.cfi_endproc
.LFE1:
.size _Z2f2PcS_S_, .-_Z2f2PcS_S_
.ident "GCC: (GNU) 4.7.2"
.section .note.GNU-stack,"",@progbits
Apesto en la lectura del ensamblaje, así que decidí agregar algunos marcadores para saber dónde fueron los cuerpos de los bucles:
constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
for(int i = 0; i < N; ++i) {
asm("# im in ur loop");
sum[i] = a[i] + b[i];
}
}
void f2(char* sum, char* a, char* b) {
char* end = sum + N;
while(sum != end) {
asm("# im in ur loop");
*sum++ = *a++ + *b++;
}
}
Y GCC escupió esto:
.file "a.c++"
.intel_syntax noprefix
.text
.p2align 4,,15
.globl _Z2f1PcS_S_
.type _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
.cfi_startproc
xor eax, eax
.p2align 4,,10
.p2align 3
.L2:
#APP
# 4 "a.c++" 1
# im in ur loop
# 0 "" 2
#NO_APP
movzx ecx, BYTE PTR [rdx+rax]
add cl, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], cl
add rax, 1
cmp rax, 1000
jne .L2
rep
ret
.cfi_endproc
.LFE0:
.size _Z2f1PcS_S_, .-_Z2f1PcS_S_
.p2align 4,,15
.globl _Z2f2PcS_S_
.type _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
.cfi_startproc
xor eax, eax
.p2align 4,,10
.p2align 3
.L6:
#APP
# 12 "a.c++" 1
# im in ur loop
# 0 "" 2
#NO_APP
movzx ecx, BYTE PTR [rdx+rax]
add cl, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], cl
add rax, 1
cmp rax, 1000
jne .L6
rep
ret
.cfi_endproc
.LFE1:
.size _Z2f2PcS_S_, .-_Z2f2PcS_S_
.ident "GCC: (GNU) 4.7.2"
.section .note.GNU-stack,"",@progbits
Esto es considerablemente más corto y tiene algunas diferencias significativas como la falta de instrucciones SIMD. Esperaba el mismo resultado, con algunos comentarios en algún punto intermedio. ¿Estoy haciendo alguna suposición incorrecta aquí? ¿El optimizador de GCC está obstaculizado por los comentarios de ASM?
c++
gcc
assembly
optimization
inline-assembly
R. Martinho Fernandes
fuente
fuente
asm
forma extendida con salida vacía y listas de golpes.asm("# im in ur loop" : : );
(ver documentación )-fverbose-asm
bandera, que agrega algunas anotaciones para ayudar a identificar cómo se mueven las cosas entre los registros.Respuestas:
Las interacciones con las optimizaciones se explican aproximadamente en la mitad de la página "Instrucciones de ensamblador con operandos de expresión C" en la documentación.
GCC no intenta entender nada del ensamblado real dentro del
asm
; lo único que sabe sobre el contenido es lo que usted (opcionalmente) le dice en la especificación del operando de entrada y salida y en la lista de registro de golpes.En particular, tenga en cuenta:
y
Entonces, la presencia del
asm
interior de su bucle ha inhibido una optimización de vectorización, porque GCC asume que tiene efectos secundarios.fuente
asm
declaración tiene que ejecutarse una vez por cada vez que lo haría en la máquina abstracta de C ++, y GCC elige no vectorizar y luego emitir el asm 16 veces seguidas porpaddb
. Sin embargo, creo que eso sería legal, porque los accesos de caracteres no lo sonvolatile
. (A diferencia de una declaración de asm extendida con una"memory"
paliza)Tenga en cuenta que gcc vectorizó el código, dividiendo el cuerpo del bucle en dos partes, la primera procesando 16 elementos a la vez y la segunda haciendo el resto más tarde.
Como comentó Ira, el compilador no analiza el bloque asm, por lo que no sabe que es solo un comentario. Incluso si lo hiciera, no tiene forma de saber lo que pretendía. Los bucles optimizados tienen el cuerpo duplicado, ¿debería poner su conjunto en cada uno? ¿Le gustaría que no se ejecute 1000 veces? No lo sabe, por lo que toma la ruta segura y vuelve al simple bucle único.
fuente
No estoy de acuerdo con "gcc no entiende lo que hay en el
asm()
bloque". Por ejemplo, gcc puede lidiar bastante bien con la optimización de parámetros e incluso con la reorganización deasm()
bloques de manera que se entremezclan con el código C generado. Por eso, si observa el ensamblador en línea en, por ejemplo, el kernel de Linux, casi siempre tiene el prefijo__volatile__
para asegurarse de que el compilador "no mueva el código". He tenido gcc moviendo mi "rdtsc", lo que hizo mis mediciones del tiempo que tomó hacer cierta cosa.Como se documenta, gcc trata ciertos tipos de
asm()
bloques como "especiales" y, por lo tanto, no optimiza el código en ninguno de los lados del bloque.Eso no quiere decir que gcc, a veces, no se confunda con bloques ensambladores en línea, o simplemente decida renunciar a alguna optimización particular porque no puede seguir las consecuencias del código ensamblador, etc., etc. Más importante aún, a menudo puede confundirse al faltar etiquetas clobber, por lo que si tiene alguna instrucción como
cpuid
que cambia el valor de EAX-EDX, pero usted escribió el código para que solo use EAX, el compilador puede almacenar cosas en EBX, ECX y EDX, y luego su código actúa muy extraño cuando estos registros se sobrescriben ... Si tiene suerte, se bloquea de inmediato, entonces es fácil averiguar qué sucede. Pero si no tiene suerte, se colapsa en el futuro ... Otro truco es la instrucción de división que da un segundo resultado en edx. Si no le importa el módulo, es fácil olvidar que se cambió EDX.fuente
volatile asm
le dice a GCC que el código puede tener "efectos secundarios importantes" y lo tratará con más cuidado. Se puede todavía ser eliminado como parte de muertos-código-optimización o se mueve hacia fuera. La interacción con el código C debe asumir ese caso (raro) e imponer una evaluación secuencial estricta (por ejemplo, creando dependencias dentro del ASM).asm("")
) es implícitamente volátil, al igual que Extended asm sin operandos de salida. GCC no comprende la cadena de la plantilla asm, solo las restricciones; por eso es esencial describir de manera precisa y completa su asm al compilador usando restricciones. La sustitución de operandos en la cadena de plantilla no requiere más comprensión queprintf
usar una cadena de formato. TL: DR: no use GNU C Basic asm para nada, excepto tal vez casos de uso como este con comentarios puros.Esta respuesta ahora está modificada: originalmente se escribió con una mentalidad que consideraba Basic Asm en línea como una herramienta bastante especificada, pero no es nada de eso en GCC. Basic Asm es débil, por lo que se editó la respuesta.
Cada comentario de ensamblado actúa como un punto de interrupción.
EDITAR: Pero uno roto, ya que usa Basic Asm. Inline
asm
(unaasm
declaración dentro del cuerpo de una función) sin una lista de golpes explícita es una característica débilmente especificada en GCC y su comportamiento es difícil de definir. No parece (no entiendo completamente sus garantías) adjunto a nada en particular, por lo que si bien el código ensamblador debe ejecutarse en algún momento si se ejecuta la función, no está claro cuándo se ejecuta para cualquier Nivel de optimización trivial . Un punto de interrupción que se puede reordenar con una instrucción vecina no es un "punto de interrupción" muy útil. FIN EDITARPuede ejecutar su programa en un intérprete que se interrumpe en cada comentario e imprime el estado de cada variable (utilizando información de depuración). Estos puntos deben existir para que observes el entorno (estado de registros y memoria).
Sin el comentario, no existe ningún punto de observación y el bucle se compila como una única función matemática que toma un entorno y produce un entorno modificado.
Quiere saber la respuesta de una pregunta sin sentido: quiere saber cómo se compila cada instrucción (o tal vez bloque, o tal vez rango de instrucción), pero no se compila ninguna instrucción (o bloque) aislada; todo el material se compila como un todo.
Una mejor pregunta sería:
Hola GCC. ¿Por qué cree que esta salida de ASM está implementando el código fuente? Explique paso a paso, con cada suposición.
Pero entonces no querría leer una prueba más larga que la salida de asm, escrita en términos de representación interna de GCC.
fuente
"memory"
golpe implícito ) como una curita para el código de errores existente que seguramente existe. Incluso para instrucciones comoasm("cli")
esa que solo afectan una parte del estado arquitectónico que el código generado por el compilador no toca, aún lo necesita ordenado wrt. cargas / almacenes generados por el compilador (por ejemplo, si está deshabilitando interrupciones alrededor de una sección crítica).add rsp, -128
primero. Pero hacer eso es obviamente una muerte cerebral.asm("" :::)
(implícitamente volátil porque no tiene salidas, pero no está vinculado al resto del código por dependencias de entrada o salida. Y sin"memory"
clobber). Y, por supuesto, no%operand
reemplaza la cadena de la plantilla, por lo que literal%
no tiene que escaparse como%%
. Entonces sí, de acuerdo, desaprobar Basic Asm fuera de las__attribute__((naked))
funciones y el alcance global sería una buena idea.