¿Qué es más rápido: if (bool) o if (int)?

94

¿Qué valor es mejor usar? ¿Booleano verdadero o entero 1?

El tema anterior me hizo hacer algunos experimentos con booly inten ifcondiciones. Así que por curiosidad escribí este programa:

int f(int i) 
{
    if ( i ) return 99;   //if(int)
    else  return -99;
}
int g(bool b)
{
    if ( b ) return 99;   //if(bool)
    else  return -99;
}
int main(){}

g++ intbool.cpp -S genera código ASM para cada función de la siguiente manera:

  • código asm para f(int)

    __Z1fi:
       LFB0:
             pushl  %ebp
       LCFI0:
              movl  %esp, %ebp
       LCFI1:
              cmpl  $0, 8(%ebp)
              je    L2
              movl  $99, %eax
              jmp   L3
       L2:
              movl  $-99, %eax
       L3:
              leave
       LCFI2:
              ret
  • código asm para g(bool)

    __Z1gb:
       LFB1:
              pushl %ebp
       LCFI3:
              movl  %esp, %ebp
       LCFI4:
              subl  $4, %esp
       LCFI5:
              movl  8(%ebp), %eax
              movb  %al, -4(%ebp)
              cmpb  $0, -4(%ebp)
              je    L5
              movl  $99, %eax
              jmp   L6
       L5:
              movl  $-99, %eax
       L6:
              leave
       LCFI6:
              ret

¡Sorprendentemente, g(bool)genera más asminstrucciones! ¿Significa que if(bool)es un poco más lento que if(int)? Solía ​​pensar que boolestá especialmente diseñado para usarse en declaraciones condicionales como if, por lo que esperaba g(bool)generar menos instrucciones ASM, lo que las hace g(bool)más eficientes y rápidas.

EDITAR:

No estoy usando ningún indicador de optimización a partir de ahora. Pero incluso en su ausencia, ¿por qué genera más asm? g(bool)Es una pregunta para la que estoy buscando una respuesta razonable. También debería decirte que la -O2bandera de optimización genera exactamente el mismo asm. Pero esa no es la cuestión. La pregunta es lo que he preguntado.


Nawaz
fuente
32
También es una prueba injusta a menos que los compare con optimizaciones razonables habilitadas.
Daniel Pryden
9
@Daniel: No estoy usando ningún indicador de optimización con ninguno de ellos. Pero incluso en su ausencia, ¿por qué genera más asm? g(bool)Es una pregunta para la que estoy buscando una respuesta razonable.
Nawaz
8
¿Por qué se tomaría la molestia de leer el ASM, pero no solo ejecutar el programa y cronometrar el resultado ? El número de instrucciones no dice mucho sobre el rendimiento. Debe tener en cuenta no solo la longitud de las instrucciones, sino también las dependencias y los tipos de instrucciones (algunas de ellas se decodifican utilizando la ruta de microcódigo más lenta, qué unidades de ejecución requieren, cuál es la latencia y el rendimiento de la instrucción, ¿es una branch? ¿Un acceso a la memoria?
jalf
2
@usuario desconocido, y @Malvolio: Eso es obviamente; No estoy haciendo todo esto para el código de producción. Como ya mencioné al principio de mi publicación, "Así que por curiosidad escribí este programa" . Así que sí, es puramente hipotético .
Nawaz
3
Es una pregunta legítima. Son equivalentes o uno es más rápido. El ASM probablemente se publicó en un intento de ser útil o pensar en voz alta, así que en lugar de usarlo como una forma de eludir la pregunta y decir "solo escribe un código legible", solo responde la pregunta o STFU si no lo sabes o no tengo nada útil que decir;) Mi contribución es que la pregunta es respondible, y "simplemente escribe un código legible" no es más que eludir la pregunta.
Triynko

Respuestas:

99

Tiene sentido para mi. Su compilador aparentemente define a boolcomo un valor de 8 bits, y su sistema ABI requiere que "promueva" argumentos enteros pequeños (<32 bits) a 32 bits cuando los empuja a la pila de llamadas. Entonces, para comparar a bool, el compilador genera código para aislar el byte menos significativo del argumento de 32 bits que recibe g, y lo compara cmpb. En el primer ejemplo, el intargumento usa los 32 bits completos que se insertaron en la pila, por lo que simplemente se compara con el conjunto cmpl.

Sherm Pendley
fuente
4
Estoy de acuerdo. Esto ayuda a aclarar que al elegir un tipo de variable, lo está eligiendo para dos propósitos potencialmente competitivos, espacio de almacenamiento versus rendimiento computacional.
Triynko
3
¿Esto también se aplica a los procesos de 64 bits, que __int64es más rápido que int? ¿O la CPU trata un entero de 32 bits con conjuntos de instrucciones de 32 bits por separado?
Crend King
1
@CrendKing, ¿quizás valga la pena hacer otra pregunta?
Nombre
81

Compilar con me -03da lo siguiente:

F:

    pushl   %ebp
    movl    %esp, %ebp
    cmpl    $1, 8(%ebp)
    popl    %ebp
    sbbl    %eax, %eax
    andb    $58, %al
    addl    $99, %eax
    ret

gramo:

    pushl   %ebp
    movl    %esp, %ebp
    cmpb    $1, 8(%ebp)
    popl    %ebp
    sbbl    %eax, %eax
    andb    $58, %al
    addl    $99, %eax
    ret

.. por lo que compila a esencialmente el mismo código, a excepción de cmplfrente cmpb. Esto significa que la diferencia, si la hay, no importa. No es justo juzgar por código no optimizado.


Edite para aclarar mi punto. El código no optimizado es para depuración simple, no para velocidad. Comparar la velocidad del código no optimizado no tiene sentido.

Alexander Gessler
fuente
8
Por mucho que esté de acuerdo con su conclusión, creo que se está saltando la parte interesante. ¿ Por qué se usa cmplpara uno y cmpbpara el otro?
jalf
22
@jalf: Porque a booles un solo byte y an intes cuatro. No creo que haya nada más especial que eso.
CB Bailey
7
Creo que otras respuestas prestaron mayor atención a las razones: es porque la plataforma en cuestión se trata boolcomo un tipo de 8 bits.
Alexander Gessler
9
@Nathan: No. C ++ no tiene tipos de datos de bits. El tipo más pequeño es char, que es un byte por definición, y es la unidad direccionable más pequeña. boolEl tamaño está definido por la implementación y puede ser 1, 4 u 8, o lo que sea. Sin embargo, los compiladores tienden a convertirlo en uno.
GManNickG
6
@Nathan: Bueno, eso también es complicado en Java. Java dice que los datos que representa un booleano son el valor de un bit, pero la forma en que se almacena ese bit sigue estando definida por la implementación. Las computadoras pragmáticas simplemente no abordan los bits.
GManNickG
26

Cuando compilo esto con un conjunto sensato de opciones (específicamente -O3), esto es lo que obtengo:

Para f():

        .type   _Z1fi, @function
_Z1fi:
.LFB0:
        .cfi_startproc
        .cfi_personality 0x3,__gxx_personality_v0
        cmpl    $1, %edi
        sbbl    %eax, %eax
        andb    $58, %al
        addl    $99, %eax
        ret
        .cfi_endproc

Para g():

        .type   _Z1gb, @function
_Z1gb:
.LFB1:
        .cfi_startproc
        .cfi_personality 0x3,__gxx_personality_v0
        cmpb    $1, %dil
        sbbl    %eax, %eax
        andb    $58, %al
        addl    $99, %eax
        ret
        .cfi_endproc

Todavía usan instrucciones diferentes para la comparación ( cmpbpara booleano frente cmpla int), pero por lo demás los cuerpos son idénticos. Un vistazo rápido a los manuales de Intel me dice: ... no mucho de nada. No existe tal cosa como cmpbni cmplen los manuales de Intel. Están todos cmpy no puedo encontrar las tablas de tiempo en este momento. Sin embargo, supongo que no hay diferencia de reloj entre comparar un byte inmediato y comparar un inmediato largo, por lo que para todos los propósitos prácticos el código es idéntico.


editado para agregar lo siguiente en función de su adición

La razón por la que el código es diferente en el caso no optimizado es que no está optimizado. (Sí, es circular, lo sé). Cuando el compilador recorre el AST y genera código directamente, no "sabe" nada excepto lo que está en el punto inmediato del AST en el que se encuentra. En ese momento, carece de toda la información contextual necesaria saber que en este punto específico puede tratar el tipo declarado boolcomo un int. Obviamente, un booleano se trata de forma predeterminada como un byte y, al manipular bytes en el mundo de Intel, debe hacer cosas como firmar-extender para llevarlo a ciertos anchos para ponerlo en la pila, etc. (No puede presionar un byte .)

Cuando el optimizador ve el AST y hace su magia, sin embargo, mira el contexto circundante y "sabe" cuándo puede reemplazar el código con algo más eficiente sin cambiar la semántica. Entonces "sabe" que puede usar un número entero en el parámetro y, por lo tanto, perder las conversiones y ampliaciones innecesarias.

SOLO MI CORRECTA OPINIÓN
fuente
1
Jaja, me gusta cómo el compilador simplemente devolvió 99, o 99 + 58 = 157 = -99 (desbordamiento de 8 bits firmados) ... interesante.
caviar desacelerado
@Daniel: Incluso a mí me gustó. Al principio, dije "dónde está -99" e inmediatamente me di cuenta de que está haciendo algo muy pervertido.
Nawaz
7
ly bson sufijos utilizados únicamente en la sintaxis de AT&T. Simplemente se refieren a versiones de cmputilizar operandos de 4 bytes (largo) y 1 byte (byte) respectivamente. Donde hay alguna ambigüedad en la sintaxis de Intel, convencionalmente el operando de memoria se etiqueta con BYTE PTR, WORD PTRo en DWORD PTRlugar de poner un sufijo en el código de operación.
CB Bailey
Tablas de tiempo: agner.org/optimize Ambos tamaños de operandos de cmptienen el mismo rendimiento y no hay penalizaciones de registro parcial para la lectura %dil . (Pero eso no impide que el clang cree de manera divertida un bloqueo de registro parcial al usar el tamaño de byte anden AL como parte del cambio de mayúsculas y minúsculas sin ramificaciones entre 99 y -99)
Peter Cordes
13

Con GCC 4.5 en Linux y Windows al menos sizeof(bool) == 1,. En x86 y x86_64, no puede pasar menos que el valor de un registro de propósito general a una función (ya sea a través de la pila o un registro, según la convención de llamada, etc.).

Entonces, el código para bool, cuando no está optimizado, en realidad llega hasta cierto punto para extraer ese valor bool de la pila de argumentos (usando otra ranura de pila para guardar ese byte). Es más complicado que simplemente extraer una variable nativa del tamaño de un registro.

Estera
fuente
Del estándar C ++ 03, §5.3.3 / 1: " sizeof(bool)y sizeof(wchar_t)están definidos por la implementación " . Por lo tanto, decirlo sizeof(bool) == 1no es estrictamente correcto a menos que esté hablando de una versión específica de un compilador específico.
ildjarn
9

A nivel de máquina no existe tal cosa como bool

Muy pocas arquitecturas de conjuntos de instrucciones definen algún tipo de operando booleano, aunque a menudo hay instrucciones que desencadenan una acción en valores distintos de cero. Para la CPU, por lo general, todo es uno de los tipos escalares o una cadena de ellos.

Un compilador y una ABI determinados deberán elegir tamaños específicos para inty boolcuando, como en su caso, estos sean tamaños diferentes, pueden generar código ligeramente diferente y, en algunos niveles de optimización, uno puede ser un poco más rápido.

¿Por qué bool es de un byte en muchos sistemas?

Es más seguro elegir un chartipo para bool porque alguien podría hacer una gran variedad de ellos.

Actualización: por "más seguro", quiero decir: para el compilador y los implementadores de la biblioteca. No estoy diciendo que las personas necesiten volver a implementar el tipo de sistema.

DigitalRoss
fuente
2
+1 Imagine la sobrecarga en x86 si boolestuviera representada por bits; por lo que el byte será una buena compensación por la compacidad de velocidad / datos en muchas implementaciones.
hardmath
1
@Billy: Creo que no estaba diciendo "usar en charlugar de bool", sino que simplemente usó " chartipo" para significar "1 byte" cuando se refería al tamaño que el compilador elige para los boolobjetos.
Dennis Zickefoose
Oh, claro, no quise decir que cada programa deba elegir, solo estaba avanzando una justificación de por qué el tipo bool del sistema es de 1 byte.
DigitalRoss
@Dennis: Ah, eso tiene sentido.
Billy ONeal
7

Sí, la discusión es divertida. Pero solo pruébalo:

Código de prueba:

#include <stdio.h>
#include <string.h>

int testi(int);
int testb(bool);
int main (int argc, char* argv[]){
  bool valb;
  int  vali;
  int loops;
  if( argc < 2 ){
    return 2;
  }
  valb = (0 != (strcmp(argv[1], "0")));
  vali = strcmp(argv[1], "0");
  printf("Arg1: %s\n", argv[1]);
  printf("BArg1: %i\n", valb ? 1 : 0);
  printf("IArg1: %i\n", vali);
  for(loops=30000000; loops>0; loops--){
    //printf("%i: %i\n", loops, testb(valb=!valb));
    printf("%i: %i\n", loops, testi(vali=!vali));
  }
  return valb;
}

int testi(int val){
  if( val ){
    return 1;
  }
  return 0;
}
int testb(bool val){
  if( val ){
    return 1;
  }
  return 0;
}

Compilado en una computadora portátil Ubuntu 10.10 de 64 bits con: g ++ -O3 -o / tmp / test_i /tmp/test_i.cpp

Comparación basada en enteros:

sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.203s
user    0m8.170s
sys 0m0.010s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.056s
user    0m8.020s
sys 0m0.000s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.116s
user    0m8.100s
sys 0m0.000s

Prueba booleana / impresión sin comentar (y entero comentado):

sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.254s
user    0m8.240s
sys 0m0.000s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.028s
user    0m8.000s
sys 0m0.010s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m7.981s
user    0m7.900s
sys 0m0.050s

Son iguales con 1 asignación y 2 comparaciones en cada bucle de más de 30 millones de bucles. Encuentre algo más para optimizar. Por ejemplo, no use strcmp innecesariamente. ;)

dannysauer
fuente
0

Abordar su pregunta de dos maneras diferentes:

Si está hablando específicamente de C ++ o de cualquier lenguaje de programación que produzca código ensamblador, estamos sujetos a qué código generará el compilador en ASM. También estamos vinculados a la representación de verdadero y falso en c ++. Un número entero tendrá que almacenarse en 32 bits, y podría simplemente usar un byte para almacenar la expresión booleana. Fragmentos de código asm para declaraciones condicionales:

Para el entero:

  mov eax,dword ptr[esp]    ;Store integer
  cmp eax,0                 ;Compare to 0
  je  false                 ;If int is 0, its false
  ;Do what has to be done when true
false:
  ;Do what has to be done when false

Para el bool:

  mov  al,1     ;Anything that is not 0 is true
  test al,1     ;See if first bit is fliped
  jz   false    ;Not fliped, so it's false
  ;Do what has to be done when true
false:
  ;Do what has to be done when false

Entonces, es por eso que la comparación de velocidad depende tanto de la compilación. En el caso anterior, el bool sería un poco rápido ya cmpque implicaría una resta para poner las banderas. También contradice lo que generó su compilador.

Otro enfoque, mucho más simple, es mirar la lógica de la expresión por sí misma y tratar de no preocuparse por cómo el compilador traducirá su código, y creo que esta es una forma de pensar mucho más saludable. Sigo creyendo, en última instancia, que el código generado por el compilador está tratando de dar una resolución veraz. Lo que quiero decir es que, tal vez si aumenta los casos de prueba en la instrucción if y se queda con booleano en un lado y entero en otro, el compilador hará que el código generado se ejecute más rápido con expresiones booleanas en el nivel de la máquina.

Estoy considerando que esta es una pregunta conceptual, así que daré una respuesta conceptual. Esta discusión me recuerda las discusiones que tengo comúnmente sobre si la eficiencia del código se traduce o no en menos líneas de código en ensamblador. Parece que este concepto es generalmente aceptado como cierto. Teniendo en cuenta que no es viable realizar un seguimiento de la rapidez con que la ALU manejará cada declaración, la segunda opción sería centrarse en los saltos y las comparaciones en el ensamblaje. Cuando ese es el caso, la distinción entre declaraciones booleanas o enteros en el código que presentó se vuelve bastante representativa. El resultado de una expresión en C ++ devolverá un valor que luego se le dará una representación. En montaje, por otro lado, los saltos y las comparaciones se basarán en valores numéricos independientemente del tipo de expresión que se esté evaluando en su declaración if de C ++. Es importante en estas preguntas recordar que afirmaciones puramente lógicas como estas terminan con una enorme sobrecarga computacional, aunque un solo bit sería capaz de hacer lo mismo.

Artie
fuente