c++ - gcc flags
¿Por qué agregar comentarios de ensamblaje causa un cambio tan radical en el código generado? (4)
Cada comentario de ensamblaje actúa como un punto de interrupción. Puede ejecutar su programa en un intérprete que se rompe en cada comentario e imprime el estado de cada variable (usando información de depuración). Estos puntos deben existir para que pueda observar el entorno (estado de los registros y la memoria).
Sin el comentario, no existe un punto de observación, y el ciclo se compila como una función matemática única que toma un entorno y produce un entorno modificado.
Desea saber la respuesta a una pregunta sin sentido: desea saber cómo se compila cada instrucción (o tal vez el bloque, o quizás el rango de instrucción), pero no se compila ninguna instrucción aislada (o bloque); todo el material está compilado como un todo.
Una mejor pregunta sería:
Hola GCC. ¿Por qué crees que esta salida de asm está implementando el código fuente? Por favor explique paso a paso, con cada suposición.
Pero entonces no querrá leer una prueba más larga que la salida de asm, escrita en términos de representación interna de GCC.
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
Asco en el montaje de lectura, así que decidí agregar algunos marcadores para saber dónde estaban 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 la misma salida, con algunos comentarios en algún lugar en el medio de ella. ¿Estoy haciendo una suposición errónea aquí? ¿El optimizador de GCC está obstaculizado por los comentarios de asm?
Las interacciones con las optimizaciones se explican a la mitad de la página "Instrucciones de ensamblador con operandos de expresión C" en la documentación.
GCC no intenta comprender ninguno de los ensambles reales dentro del asm
; lo único que sabe sobre el contenido es lo que (opcionalmente) le dice en la especificación del operando de salida y entrada y en la lista de clobber del registro.
En particular, nota:
Una instrucción
asm
sin ningún operando de salida se tratará de manera idéntica a una instrucción volátil deasm
.
y
La palabra clave
volatile
indica que la instrucción tiene importantes efectos secundarios [...]
De modo que la presencia del asm
dentro de su bucle ha inhibido la optimización de la vectorización, porque GCC supone que tiene efectos secundarios.
No estoy de acuerdo con el "gcc no entiende lo que está en el bloque asm()
". Por ejemplo, gcc puede manejar bastante bien la optimización de parámetros, e incluso reorganizar los bloques de asm()
modo que se entremezcle con el código de C generado. Esta es la razón por la cual, si observa el ensamblador en línea, por ejemplo, en el kernel de Linux, casi siempre tiene el prefijo __volatile__
para asegurarse de que el compilador "no mueva el código". He tenido gcc mover mi "rdtsc" alrededor, lo que hizo que mis medidas del tiempo que tomó para hacer ciertas cosas.
Como se documentó, gcc trata ciertos tipos de bloques asm()
como "especiales", y por lo tanto no optimiza el código en ninguno de los lados del bloque.
Eso no quiere decir que gcc no se confunda a veces con los bloques de ensambladores en línea, o simplemente decida abandonar una optimización particular porque no puede seguir las consecuencias del código ensamblador, etc., etc. Más importante aún, a menudo puede confundirse con etiquetas perdidas, por lo tanto, 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 de manera muy extraña cuando estos registros se sobrescriben ... Si tiene suerte, se bloquea inmediatamente, entonces es fácil descubrir qué sucede. Pero si tienes mala suerte, se bloquea muy por debajo de la línea ... Otra complicada es la instrucción dividida que da un segundo resultado en edx. Si no le importa el módulo, es fácil olvidar que EDX fue cambiado.
Tenga en cuenta que gcc vectorizó el código, dividiendo el cuerpo del bucle en dos partes, el primero procesando 16 elementos a la vez, y el segundo 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 hizo, no tiene forma de saber lo que pretendía. Los bucles optimizados tienen el cuerpo doblado, ¿debería poner su asm en cada uno? ¿Te gustaría que no se ejecute 1000 veces? No lo sabe, por lo que sigue la ruta segura y vuelve al sencillo bucle simple.