O que é mais rápido: if (bool) ou if (int)?

94

Qual valor é melhor usar? Boolean true or Integer 1?

O tópico acima me fez fazer alguns experimentos com boole intem ifcondições. Então, apenas por curiosidade, escrevi 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 gera código ASM para cada função da seguinte maneira:

  • 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

Surpreendentemente, g(bool)gera mais asminstruções! Isso significa que if(bool)é um pouco mais lento do que if(int)? Eu costumava pensar que boolera especialmente projetado para ser usado em instruções condicionais como if, portanto, esperava g(bool)gerar menos instruções asm, tornando-as g(bool)mais eficientes e rápidas.

EDITAR:

Não estou usando nenhum sinalizador de otimização no momento. Mas mesmo a ausência dela, por que gera mais g(bool)questionamento? É uma questão para a qual estou procurando uma resposta razoável. Devo também dizer que o -O2sinalizador de otimização gera exatamente o mesmo asm. Mas essa não é a questão. A questão é o que eu perguntei.


Nawaz
fonte
32
Também é um teste injusto, a menos que você os compare com otimizações razoáveis ​​habilitadas.
Daniel Pryden
9
@Daniel: Não estou usando nenhum sinalizador de otimização com nenhum deles. Mas mesmo a ausência dela, por que gera mais g(bool)questionamento? É uma questão para a qual procuro uma resposta razoável.
Nawaz
8
Por que você se daria ao trabalho de ler o conjunto, mas não apenas executar o programa e cronometrar o resultado ? O número de instructiosn realmente não diz muito sobre o desempenho. Você precisa levar em consideração não apenas os comprimentos das instruções, mas também as dependências e os tipos de instruções (alguns deles são decodificados usando o caminho do microcódigo mais lento, quais unidades de execução eles exigem, qual é a latência e o rendimento da instrução, é um branch? Um acesso à
memória
2
@ usuário desconhecido e @Malvolio: Isso é obviamente; Não estou fazendo tudo isso para código de produção. Como já mencionei no início do meu post que "Então só por curiosidade eu escrevi este programa" . Então, sim, é puramente hipotético .
Nawaz
3
É uma pergunta legítima. Eles são equivalentes ou um é mais rápido. O ASM provavelmente foi postado na tentativa de ser útil ou pensar em voz alta, então ao invés de usá-lo como uma forma de se esquivar da pergunta e dizer "apenas escreva um código legível", apenas responda a pergunta ou STFU se você não souber ou não tenho nada útil a dizer;) Minha contribuição é que a pergunta é respondível e "apenas escreva um código legível" nada mais é do que esquivar-se da pergunta.
Triynko

Respostas:

99

Faz sentido para mim. Aparentemente, seu compilador define a boolcomo um valor de 8 bits e o ABI do sistema exige que ele "promova" argumentos inteiros pequenos (<32 bits) para 32 bits ao colocá-los na pilha de chamadas. Portanto, para comparar a bool, o compilador gera código para isolar o byte menos significativo do argumento de 32 bits que g recebe e o compara cmpb. No primeiro exemplo, o intargumento usa os 32 bits completos que foram colocados na pilha, então ele simplesmente compara com o todo com cmpl.

Sherm Pendley
fonte
4
Concordo. Isso ajuda a esclarecer que, ao escolher um tipo de variável, você o está escolhendo para dois propósitos potencialmente concorrentes, espaço de armazenamento vs. desempenho computacional.
Triynko
3
Isso também se aplica a processos de 64 bits, que __int64são mais rápidos do que int? Ou a CPU lida com inteiros de 32 bits com conjuntos de instruções de 32 bits separadamente?
Crend King
1
@CrendKing talvez valha a pena fazer outra pergunta?
Nome de exibição
81

Compilar com -03oferece o seguinte para mim:

f:

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

g:

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

.. por isso compila para essencialmente o mesmo código, exceto para cmplvs cmpb. Isso significa que a diferença, se houver, não importa. Julgar por código não otimizado não é justo.


Edite para esclarecer meu ponto. O código não otimizado é para depuração simples, não para velocidade. Comparar a velocidade de um código não otimizado não faz sentido.

Alexander Gessler
fonte
8
Por mais que eu concorde com sua conclusão, acho que você está pulando a parte interessante. Por que usa cmplpara um e cmpbpara o outro?
jalf
22
@jalf: Porque a boolé um único byte e an inté quatro. Não acho que haja nada mais especial do que isso.
CB Bailey
7
Acho que outras respostas deram mais atenção aos motivos: é porque a plataforma em questão trata boolcomo um tipo de 8 bits.
Alexander Gessler
9
@Nathan: Não. C ++ não tem tipos de dados de bits. O menor tipo é char, que é um byte por definição e é a menor unidade endereçável. boolO tamanho de é definido pela implementação e pode ser 1, 4 ou 8 ou qualquer outro. No entanto, os compiladores tendem a torná-lo um.
GManNickG
6
@Nathan: Bem, isso também é complicado em Java. Java diz que os dados que um booleano representa são o valor de um bit, mas como esse bit é armazenado ainda é definido pela implementação. Computadores pragmáticos simplesmente não endereçam bits.
GManNickG
26

Quando eu compilo isso com um conjunto razoável de opções (especificamente -O3), eis o que obtenho:

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

Eles ainda usam instruções diferentes para a comparação ( cmpbpara booleano vs. cmplpara int), mas fora isso os corpos são idênticos. Uma rápida olhada nos manuais da Intel me diz: ... quase nada. Não existe tal coisa como cmpbou cmplnos manuais da Intel. Eles são todos cmpe não consigo encontrar as tabelas de tempo no momento. Estou supondo, entretanto, que não há diferença de clock entre comparar um imediato byte e um imediato longo, portanto, para todos os fins práticos, o código é idêntico.


editado para adicionar o seguinte com base na sua adição

A razão pela qual o código é diferente no caso não otimizado é que ele não é otimizado. (Sim, é circular, eu sei.) Quando o compilador percorre o AST e gera o código diretamente, ele não "sabe" nada, exceto o que está no ponto imediato do AST em que está. Nesse ponto, faltam todas as informações contextuais necessárias saber que, neste ponto específico, ele pode tratar o tipo declarado boolcomo um int. Um booleano é obviamente tratado por padrão como um byte e ao manipular bytes no mundo da Intel você tem que fazer coisas como estender o sinal para trazê-lo a certas larguras para colocá-lo na pilha, etc. (Você não pode empurrar um byte .)

Quando o otimizador visualiza o AST e faz sua mágica, entretanto, ele olha para o contexto circundante e "sabe" quando pode substituir o código por algo mais eficiente sem alterar a semântica. Portanto, ele "sabe" que pode usar um número inteiro no parâmetro e, assim, perder as conversões e ampliações desnecessárias.

APENAS MINHA OPINIÃO correta
fonte
1
Haha, gosto de como o compilador simplesmente retornou 99 ou 99 + 58 = 157 = -99 (estouro de 8 bits assinados) ... interessante.
deceleratedcaviar
@Daniel: Até eu gostei disso. No começo, eu disse "onde está -99" e imediatamente percebi que estava fazendo algo muito bizarro.
Nawaz
7
le bsão sufixos usados ​​apenas na sintaxe da AT&T. Eles apenas se referem a versões de cmpoperandos de 4 bytes (longo) e 1 byte (byte), respectivamente. Onde houver qualquer ambigüidade na sintaxe intel, convencionalmente o operando de memória é marcado com BYTE PTR, WORD PTRou em DWORD PTRvez de colocar um sufixo no opcode.
CB Bailey
Tabelas de tempo: agner.org/optimize Os dois tamanhos de operandos cmptêm o mesmo desempenho e não há penalidades de registro parcial para leitura %dil . (Mas isso não impede o clang de criar divertidamente uma paralisação de registro parcial usando o tamanho do byte andem AL como parte da inversão de caixa sem ramificação entre 99 e -99.)
Peter Cordes
13

Com GCC 4.5 em Linux e Windows, pelo menos sizeof(bool) == 1,. No x86 e no x86_64, você não pode passar menos do que o valor de um registro de uso geral para uma função (seja por meio da pilha ou de um registro, dependendo da convenção de chamada, etc ...).

Portanto, o código para bool, quando não otimizado, vai até certo ponto para extrair esse valor bool da pilha de argumentos (usando outro slot de pilha para salvar esse byte). É mais complicado do que apenas puxar uma variável de tamanho de registro nativo.

Esteira
fonte
Do padrão C ++ 03, §5.3.3 / 1: “ sizeof(bool)e sizeof(wchar_t)são definidos pela implementação. ” Portanto, dizer sizeof(bool) == 1não é estritamente correto, a menos que você esteja falando sobre uma versão específica de um compilador específico.
ildjarn
9

No nível da máquina, não existe bool

Muito poucas arquiteturas de conjunto de instruções definem qualquer tipo de operando booleano, embora haja frequentemente instruções que disparam uma ação em valores diferentes de zero. Para a CPU, geralmente, tudo é um dos tipos escalares ou uma string deles.

Um determinado compilador e uma determinada ABI precisarão escolher tamanhos específicos para inte boole quando, como no seu caso, esses tamanhos forem diferentes, eles podem gerar códigos ligeiramente diferentes e, em alguns níveis de otimização, um pode ser um pouco mais rápido.

Por que o bool tem um byte em muitos sistemas?

É mais seguro escolher um chartipo de bool porque alguém pode fazer uma grande variedade deles.

Atualização: por "mais seguro", quero dizer: para o compilador e implementadores de biblioteca. Não estou dizendo que as pessoas precisam reimplementar o tipo de sistema.

DigitalRoss
fonte
2
+1 Imagine a sobrecarga em x86 se boolfossem representados por bits; então, byte será uma boa troca para velocidade / compactação de dados em muitas implementações.
hardmath
1
@Billy: Acho que ele não estava dizendo "usar em charvez de bool", mas simplesmente usou " chartipo" para significar "1 byte" ao se referir ao tamanho que o compilador escolhe para os boolobjetos.
Dennis Zickefoose
Oh, claro, eu não quis dizer que cada programa deveria escolher, eu estava apenas apresentando uma justificativa de por que o tipo de bool do sistema é de 1 byte.
DigitalRoss
@Dennis: Ah, isso faz sentido.
Billy ONeal
7

Sim, a discussão é divertida. Mas apenas teste:

Código de teste:

#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 em um laptop Ubuntu 10.10 de 64 bits com: g ++ -O3 -o / tmp / test_i /tmp/test_i.cpp

Comparação baseada em inteiros:

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

Teste / impressão booleano sem comentário (e inteiro 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

Eles são iguais com 1 atribuição e 2 comparações, cada loop em 30 milhões de loops. Encontre algo mais para otimizar. Por exemplo, não use strcmp desnecessariamente. ;)

Dannysauer
fonte
0

Abordando sua pergunta de duas maneiras diferentes:

Se você está falando especificamente sobre C ++ ou qualquer linguagem de programação que irá produzir código assembly para esse assunto, estamos limitados a qual código o compilador irá gerar no ASM. Também estamos limitados à representação de verdadeiro e falso em c ++. Um inteiro terá que ser armazenado em 32 bits e eu poderia simplesmente usar um byte para armazenar a expressão booleana. Snippets de Asm para declarações condicionais:

Para o inteiro:

  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 o 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

Então, é por isso que a comparação de velocidade é tão dependente da compilação. No caso acima, o bool seria um pouco rápido, pois cmpimplicaria em uma subtração para definir os sinalizadores. Também contradiz o que o seu compilador gerou.

Outra abordagem, muito mais simples, é olhar para a lógica da expressão por conta própria e tentar não se preocupar em como o compilador traduzirá seu código, e acho que essa é uma maneira muito mais saudável de pensar. Ainda acredito, em última análise, que o código gerado pelo compilador está, na verdade, tentando fornecer uma resolução verdadeira. O que quero dizer é que, talvez se você aumentar os casos de teste na instrução if e ficar com booleano em um lado e inteiro no outro, o compilador fará com que o código gerado seja executado mais rápido com expressões booleanas no nível da máquina.

Estou considerando que esta é uma questão conceitual, então darei uma resposta conceitual. Essa discussão me lembra das discussões que normalmente tenho sobre se a eficiência do código se traduz ou não em menos linhas de código em assembly. Parece que esse conceito é geralmente aceito como verdadeiro. Considerando que não é viável manter o controle da rapidez com que a ALU irá lidar com cada instrução, a segunda opção seria focar nos saltos e comparações na montagem. Quando for esse o caso, a distinção entre declarações booleanas ou inteiros no código que você apresentou torna-se bastante representativa. O resultado de uma expressão em C ++ retornará um valor que receberá uma representação. Na montagem, por outro lado, os saltos e comparações serão baseados em valores numéricos, independentemente do tipo de expressão que está sendo avaliada em sua instrução C ++ if. É importante, nessas questões, lembrar que declarações puramente lógicas como essas acabam com uma grande sobrecarga computacional, embora um único bit seja capaz da mesma coisa.

Artie
fonte