2010-06-10 10 views
14

Estoy programando C en cygwin windows. Después de haber hecho un poco de programación en C y de sentirme cómodo con el idioma, quise mirar debajo del capó y ver qué está haciendo el compilador para el código que escribo.Código de nivel de conjunto de caja de interruptor

Así que anotó un bloque de código que contiene sentencias de caso interruptor y las convirtió en el montaje usando:

gcc -S foo.c 

Aquí está la fuente de C:

switch(i) 
{ 
    case 1: 
    { 
     printf("Case 1\n"); 
     break; 
    } 
    case 2: 
    {   printf("Case 2\n"); 
     break; 
    } 
    case 3: 
    { 
     printf("Case 3\n"); 
     break; 
    } 
    case 4: 
    { 
     printf("Case 4\n"); 
     break; 
    } 
    case 5: 
    { 
     printf("Case 5\n"); 
     break; 
    } 
    case 6: 
    { 
     printf("Case 6\n"); 
     break; 
    } 
    case 7: 
    { 
     printf("Case 7\n"); 
     break; 
    } 
    case 8: 
    { 
     printf("Case 8\n"); 
     break; 
    } 
    case 9: 
    { 
     printf("Case 9\n"); 
     break; 
    } 
    case 10: 
    { 
     printf("Case 10\n"); 
     break; 
    } 
    default: 
    { 
     printf("Nothing\n"); 
     break; 
    } 
} 

Ahora, el conjunto resultante de la misma es :

movl $5, -4(%ebp) 
cmpl $10, -4(%ebp) 
ja L13 
movl -4(%ebp), %eax 
sall $2, %eax 
movl L14(%eax), %eax 
jmp *%eax 
.section .rdata,"dr" 
.align 4 
L14: 
.long L13 
.long L3 
.long L4 
.long L5 
.long L6 
.long L7 
.long L8 
.long L9 
.long L10 
.long L11 
.long L12 
.text 
L3: 
movl $LC0, (%esp) 
call _printf 
jmp L2 
L4: 
movl $LC1, (%esp) 
call _printf 
jmp L2 
L5: 
movl $LC2, (%esp) 
call _printf 
jmp L2 
L6: 
movl $LC3, (%esp) 
call _printf 
jmp L2 
L7: 
movl $LC4, (%esp) 
call _printf 
jmp L2 
L8: 
movl $LC5, (%esp) 
call _printf 
jmp L2 
L9: 
movl $LC6, (%esp) 
call _printf 
jmp L2 
L10: 
movl $LC7, (%esp) 
call _printf 
jmp L2 
L11: 
movl $LC8, (%esp) 
call _printf 
jmp L2 
L12: 
movl $LC9, (%esp) 
call _printf 
jmp L2 
L13: 
movl $LC10, (%esp) 
call _printf 
L2: 

Ahora, en el ensamblaje, el código es la primera comprobación del último caso (es decir, caso 10) primero. Esto es muy extraño. Y luego está copiando 'yo' en 'eax' y haciendo cosas que están más allá de mí.

He oído que el compilador implementa una tabla de salto para el interruptor ... caso. ¿Es lo que está haciendo este código? ¿O qué está haciendo y por qué? Porque en el caso de un menor número de casos, el código es bastante similar al generado para if ... else ladder, pero cuando aumenta el número de casos, se ve esta implementación inusual.

Gracias de antemano.

+0

Desafortunadamente, no se optimiza para una búsqueda de tabla del puntero de cadena y una 'llamada _printf'. Ninguno de gcc/clang/icc hace eso incluso en '-O3'. https://godbolt.org/g/JrSwU3 (Sin embargo, optimizan 'printf' para' puts' y optimizan la llamada final a 'jmp' en lugar de a' call'/'ret') –

Respuesta

21

Primero, el código compara el i con el 10 y salta al caso predeterminado cuando el valor es mayor que 10 (cmpl $10, -4(%ebp) seguido de ja L13).

El siguiente bit de código se está desplazando la entrada a la izquierda por dos (sall $2, %eax) que es el mismo que el múltiple por cuatro que genera un desplazamiento en la tabla de salto (porque cada entrada en la tabla es de 4 bytes de largo)

A continuación, carga una dirección de la tabla de salto (movl L14(%eax), %eax) y salta a ella (jmp *%eax).

La tabla de saltos es simplemente una lista de direcciones (representados en código ensamblador por etiquetas):

L14: 
.long L13 
.long L3 
.long L4 
... 

Una cosa a notar es que L13 representa el caso por defecto. Es tanto la primera entrada en la tabla de salto (para cuando i es 0) y se maneja especialmente al principio (cuando i> 10).

+0

Veo ... esto es informativo. Pero entonces, ¿por qué el compilador no genera una tabla de salto en el caso de menos casos (como 2 o 3)? – puffadder

+0

@puffadder: la mayoría de los compiladores modernos usan la heurística para determinar cuándo es más eficiente usar ramas frente a una mesa de saltos. P.ej. si los niveles de su caso fueron, digamos, 1, 100 y 1000, puede esperar que se utilicen las ramas. –

+0

No entiendo por qué cada paso salta al caso predeterminado en lugar de salir completamente del interruptor. ¿Alguien puede explicar esto? – mharris7190

2

Para [1..10] el compilador generará una tabla para que no tenga que comparar el valor para ir a alguna parte, hace directamente un: goto table[i]. De esa forma es más rápido.

Pero en el caso i > 10 salta a su estado de cuenta predeterminado. Tiene que verificar primero antes de saltar de lo contrario, el programa colapsaría miserablemente.

Si tenía valores escasos (como, 23, 9233, 91238, y no 1, 2, 3 ...), el compilador no generaría dicha tabla y compararía cada valor.

0

Sí, eax primero se calcula el valor de conmutación (sall turno como multiplicación) para obtener la dirección de la tabla de saltos (siguiente etiqueta L14:)

jmp *%eax es un salto cerca de la etiqueta de su caso. (jmp cerca de eax)

El código que sigue a las otras etiquetas solo está imprimiendo y omite los otros casos.

3

Sí, es una tabla de saltos. La primera comprobación es comprobar si el valor está en los casos y saltar a los valores predeterminados si no es así. No olvide que en dicha tabla, si% eax es 0, L14 (% eax) apunta al primer elemento de la tabla (L13). Entonces, en la tabla, el case 10: está indexado con 9, no con 10.

La manera en que se puede hacer el cambio depende de los valores que tenga en case; en este caso están en "secuencia", por lo que la tabla de salto simple es posible.

Cuestiones relacionadas