c++ optimization gcc assembly

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 de asm .

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.