Tengo este simple código en c:

#include <stdio.h>

void test() {}

int main()
{
    if (2 < 3) {

        int zz = 10;
    }
    return 0;
}

Cuando veo la salida de ensamblaje de este código:

test():
  pushq %rbp
  movq %rsp, %rbp
  nop
  popq %rbp
  ret
main:
  pushq %rbp
  movq %rsp, %rbp
  movl $10, -4(%rbp) // space is created for zz on stack
  movl $0, %eax
  popq %rbp
  ret

Obtuve el ensamblado de aquí (opciones predeterminadas) ¿No puedo ver dónde están las instrucciones para la verificación condicional?

0
anekix 22 feb. 2018 a las 14:01

6 respuestas

La mejor respuesta

Lo interesante aquí es que gcc y clang optimizan el if() incluso en -O0 , a diferencia de otros compiladores (ICC y MSVC).

gcc -O0 no significa que no haya optimización, significa no < em> extra optimización más allá de lo que se necesita para compilar. Pero gcc tiene que transformarse a través de un par de representaciones internas de la lógica de la función antes de emitir asm. (GIMPLE y registro de transferencia de idioma). gcc no tiene un "modo tonto" especial donde translitera servilmente cada parte de cada expresión en C a asm.

Incluso uno súper simple -compilador de paso como TCC realiza optimizaciones menores dentro de una expresión (o incluso una declaración), como darse cuenta de que una condición siempre verdadera no requiere ramificación.

gcc -O0 es el valor predeterminado, que obviamente usaste porque el almacén muerto para zz no está optimizado.

gcc -O0 tiene como objetivo compilar rápidamente y para proporcionar resultados de depuración consistentes .

  • cada variable C existe en la memoria, ya sea que se use alguna vez o no.
  • nada se mantiene en los registros de las declaraciones de C, por lo que puede modificar cualquier variable de C con un depurador mientras realiza un solo paso. es decir, derramar / recargar todo entre sentencias C separadas. (Esta es la razón por la cual la evaluación comparativa para -O0 no tiene sentido: escribir el mismo código con menos expresiones más grandes es más rápido solo en -O0, no con configuraciones reales como -O3).

    Otras consecuencias interesantes: la propagación constante no funciona, consulte ¿Por qué la división de enteros por -1 (negativa) da como resultado FPE? para un caso en el que gcc usa div para una variable establecida en una constante, frente a algo más simple para una constante literal.

  • Cada declaración se compila de forma independiente, por lo que puede incluso jump a una línea de origen diferente (dentro de la misma función) utilizando GDB y obtener resultados consistentes. (A diferencia del código optimizado donde es probable que se bloquee o dé tonterías, y definitivamente no coincida con la máquina abstracta C).

Dados todos esos requisitos para el comportamiento de gcc -O0, if (2 < 3) aún puede optimizarse para cero instrucciones asm. El comportamiento no depende del valor de ninguna variable, y es una sola declaración. No hay forma de que nunca se pueda tomar, por lo que la forma más simple de compilarlo es sin instrucciones: caer en el { body } del if.

Tenga en cuenta que las reglas / restricciones de gcc -O0 van mucho más allá de la regla de C como si el código de máquina para una función simplemente tiene que implementar todo el comportamiento externo visible de la fuente C. gcc -O3 optimiza toda la función a solo

main:                 # with optimization
    xor    eax, eax
    ret

Porque no le importa mantener asm para cada declaración C.


Otros compiladores:

Ver todos los 4 de los principales compiladores x86 en Godbolt.

Clang es similar a gcc, pero con una reserva muerta de 0 a otro lugar en la pila, así como el 10 para zz. clang -O0 a menudo está más cerca de una transcripción de C en asm, por ejemplo, usará div para x / 2 en lugar de un turno, mientras que gcc usa un inverso multiplicativo para la división por una constante incluso en { {X6}}. Pero en este caso, el sonido metálico también decide que no hay instrucciones suficientes para una condición siempre cierta.

ICC y MSVC emiten asm para la rama, pero en lugar de mov $2, %ecx / cmp $3, %ecx, ambos realmente emiten 0 != 1 sin razón aparente:

# ICC18
    pushq     %rbp                                          #6.1
    movq      %rsp, %rbp                                    #6.1
    subq      $16, %rsp                                     #6.1

    movl      $0, %eax                                      #7.5
    cmpl      $1, %eax                                      #7.5
    je        ..B1.3        # Prob 100%                     #7.5

    movl      $10, -16(%rbp)                                #9.16
..B1.3:                         # Preds ..B1.2 ..B1.1
    movl      $0, %eax                                      #11.12
    leave                                                   #11.12
    ret                                                     #11.12

MSVC utiliza la optimización de mirilla de reducción a cero, incluso sin la optimización habilitada.

Es un poco interesante observar lo que hacen los compiladores de optimizaciones locales / mirillas incluso en -O0, pero no le dice nada fundamental sobre las reglas del lenguaje C o su código, solo le informa sobre las partes internas del compilador y las compensaciones del compilador los desarrolladores eligieron entre pasar tiempo buscando optimizaciones simples o compilar aún más rápido en el modo sin optimización.

El asm nunca tiene la intención de representar fielmente la fuente C de ninguna manera que permita que un descompilador la reconstruya. Solo para implementar una lógica equivalente.

3
Peter Cordes 22 feb. 2018 a las 20:04

Es simple. No está aquí. El compilador lo optimizó.

Aquí está el ensamblaje al compilar con gcc sin optimización:

    .file   "k.c"
    .text
    .globl  test
    .type   test, @function
test:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    nop
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   test, .-test
    .globl  main
    .type   main, @function
main:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $10, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   main, .-main
    .ident  "GCC: (Debian 6.3.0-18) 6.3.0 20170516"
    .section    .note.GNU-stack,"",@progbits

Y aquí está con optimización:

    .file   "k.c"
    .text
    .p2align 4,,15
    .globl  test
    .type   test, @function
test:
.LFB11:
    .cfi_startproc
    rep ret
    .cfi_endproc
.LFE11:
    .size   test, .-test
    .section    .text.startup,"ax",@progbits
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB12:
    .cfi_startproc
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE12:
    .size   main, .-main
    .ident  "GCC: (Debian 6.3.0-18) 6.3.0 20170516"
    .section    .note.GNU-stack,"",@progbits

Como puede ver, no solo la comparación está optimizada. Casi todo el principal está optimizado, ya que no produce nada visible. La variable zz nunca se usa. Lo único que hace su código es devolver 0.

2
klutt 22 feb. 2018 a las 11:10

2 siempre es menos que 3, por lo tanto, como el compilador sabe que el resultado de 2 <3 es siempre cierto, no hay necesidad de una decisión if en el ensamblador.

La optimización significa generar menos tiempo / menos código.

1
J_P 22 feb. 2018 a las 11:04

La condición if (2<3) es siempre verdadera. Entonces, un compilador decente detectaría que esto genera el código como si la condición no existiera. De hecho, si lo optimiza con -O3, godbolt.org genera solo:

test():
  rep ret
main:
  xor eax, eax
  ret

Esto es nuevamente válido porque un compilador puede optimizar y transformar el código siempre que comportamiento observable se conserva.

0
usr 22 feb. 2018 a las 11:08
if (2<3)

Siempre es cierto, por lo tanto, el compilador no emite ningún código de operación.

0
notan 22 feb. 2018 a las 11:04

No lo ves, porque no está allí. El compilador pudo realizar análisis y ver fácilmente que esta rama siempre se ingresará.

En lugar de emitir un cheque que no hará más que desperdiciar los ciclos de la CPU, emite una versión del código fácilmente optimizada.

Un programa en C no es una secuencia de instrucciones para que la CPU realice. Ese es el código de máquina emitido. Un programa en C es una descripción del comportamiento que su programa compilado debería tener . Un compilador es libre de traducirlo de la forma que quiera, siempre y cuando obtenga ese comportamiento .

Se conoce como "la regla como si".

7
StoryTeller - Unslander Monica 22 feb. 2018 a las 11:05