Что быстрее: if (bool) или if (int)?

Какое значение лучше использовать? Boolean true или Integer 1?

В приведенной выше теме я сделал несколько экспериментов с bool и int в if. Так что из любопытства я написал эту программу:

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 генерирует код asm для каждой функции следующим образом:

  • код asm для 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
    
  • код asm для 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
    

Удивительно, g(bool) генерирует больше инструкций asm! Означает ли это, что if(bool) немного медленнее, чем if(int)? Я думал, что bool специально разработан для использования в условном выражении, таком как if, поэтому я ожидал, что g(bool) будет генерировать меньше инструкций asm, тем самым делая g(bool) более эффективным и быстрым.

EDIT:

Я пока не использую флаг оптимизации. Но даже отсутствие этого, почему он генерирует больше asm для g(bool), является вопросом, для которого я ищу разумный ответ. Я также должен сказать вам, что флаг оптимизации -O2 генерирует точно такой же asm. Но это не вопрос. Вопрос в том, что я спросил.


Ответ 1

Имеет смысл для меня. Ваш компилятор, по-видимому, определяет bool как 8-битное значение, и ваша система ABI требует, чтобы он "продвигал" небольшие (& 32-разрядные) целые аргументы до 32-битных при нажатии их на стек вызовов. Поэтому для сравнения a bool компилятор генерирует код для выделения младшего значащего байта 32-битного аргумента, который получает g, и сравнивает его с cmpb. В первом примере аргумент int использует полные 32 бита, которые были вставлены в стек, поэтому он просто сравнивает все это с cmpl.

Ответ 2

Компиляция с помощью -03 дает мне следующее:

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

.. поэтому он компилируется по существу по одному и тому же коду, за исключением cmpl vs cmpb. Это означает, что разница, если она есть, не имеет значения. Судя по неоптимизированному коду, это несправедливо.

Изменить, чтобы прояснить мою точку зрения. Неоптимизированный код предназначен для простой отладки, а не для скорости. Сравнение скорости неоптимизированного кода бессмысленно.

Ответ 3

Когда я скомпилирую это с помощью разумного набора параметров (в частности -O3), вот что я получаю:

Для 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

Для 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

Они по-прежнему используют разные инструкции для сравнения (cmpb для boolean vs. cmpl для int), но в остальном тела идентичны. Быстрый взгляд на руководства Intel говорит мне:... не много чего. В руководствах Intel нет такой вещи, как cmpb или cmpl. Они все cmp, и я не могу найти таблицы времени на данный момент. Я предполагаю, однако, что нет разницы в частотах между сопоставлением байта и сравнением длинного немедленного, поэтому для всех практических целей код идентичен.


отредактирован, чтобы добавить следующее на основе вашего добавления

Причина, по которой код в неоптимизированном случае отличается от другого, заключается в том, что он неоптимизирован. (Да, это круговой, я знаю.) Когда компилятор ходит по AST и генерирует код напрямую, он не "знает" ничего, кроме того, что находится в непосредственной близости от АСТ. В этот момент ему не хватает всей необходимой контекстуальной информации знать, что в этой конкретной точке он может рассматривать объявленный тип bool как int. Логическое значение, очевидно, по умолчанию рассматривается как байт, и при манипулировании байтами в мире Intel вы должны делать такие вещи, как sign-extend, чтобы довести его до определенной ширины, чтобы поместить его в стек и т.д. (Вы не можете нажать байт.)

Когда оптимизатор просматривает AST и делает свою магию, он смотрит на окружающий контекст и "знает" , когда он может заменить код чем-то более эффективным без изменения семантики. Поэтому он "знает" , что может использовать целое число в параметре и тем самым потерять ненужные преобразования и расширение.

Ответ 4

С GCC 4.5 в Linux и Windows, по крайней мере, sizeof(bool) == 1. На x86 и x86_64 вы не можете передавать меньше, чем регистр общего назначения, который стоит для функции (будь то через стек или регистр в зависимости от вызывающего соглашения и т.д.).

Таким образом, код для bool, когда он не оптимизирован, фактически доходит до некоторой длины, чтобы извлечь это значение bool из стека аргументов (используя другой слот стека для сохранения этого байта). Это сложнее, чем просто вытащить собственную переменную размера регистра.

Ответ 5

На уровне машины нет такой вещи, как bool

Очень немногие архитектуры наборов инструкций определяют любой тип типа булевых операндов, хотя часто есть инструкции, которые вызывают действие при ненулевых значениях. Обычно для ЦП все это один из скалярных типов или их строк.

Для данного компилятора и заданного ABI необходимо будет выбрать конкретные размеры для int и bool, и когда, как и в вашем случае, это разные размеры, они могут генерировать несколько другой код, а на некоторых уровнях оптимизации один может быть немного быстрее.

Почему bool один байт на многих системах?

Безопаснее выбрать тип char для bool, потому что кто-то может создать очень большой массив из них.

Обновление: "безопаснее", я имею в виду: для разработчиков компилятора и библиотеки. Я не говорю, что людям нужно переопределить тип системы.

Ответ 6

Да, обсуждение весело. Но просто протестируйте его:

Тестовый код:

#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;
}

Скомпилирован на 64-битном ноутбуке Ubuntu 10.10 с:   g++ -O3 -o/tmp/test_i/tmp/test_i.cpp

Целочисленное сравнение:

[email protected]:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.203s
user    0m8.170s
sys 0m0.010s
[email protected]:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.056s
user    0m8.020s
sys 0m0.000s
[email protected]:/tmp$ time /tmp/test_i 1 > /dev/null

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

Логический тест/печать без комментирования (и целочисленный комментарий):

[email protected]:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.254s
user    0m8.240s
sys 0m0.000s
[email protected]:/tmp$ time /tmp/test_i 1 > /dev/null

real    0m8.028s
user    0m8.000s
sys 0m0.010s
[email protected]:/tmp$ time /tmp/test_i 1 > /dev/null

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

Они одинаковы с 1 присваиванием и 2 сравнения каждого цикла за 30 миллионов циклов. Найдите что-то еще для оптимизации. Например, не используйте strcmp без необходимости.;)

Ответ 8

Подойдя к вашему вопросу двумя способами:

Если вы конкретно говорите о С++ или любом языке программирования, который будет генерировать код сборки, это связано с тем, какой код будет генерировать компилятор в ASM. Мы также привязаны к представлению true и false в С++. Целое число должно быть сохранено в 32 бита, и я мог бы просто использовать байт для хранения логического выражения. Асмограммы для условных операторов:

Для целого числа:

  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

Для 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 false
  ;Do what has to be done when true
false:
  ;Do what has to be done when false

Итак, почему сравнение скорости настолько зависит от компиляции. В приведенном выше примере bool будет немного быстрым, поскольку cmp будет означать вычитание для установки флагов. Это также противоречит тому, что генерировал ваш компилятор.

Другой подход, гораздо более простой, - это взглянуть на логику выражения на нем и попытаться не беспокоиться о том, как компилятор переведет ваш код, и я думаю, что это гораздо более здоровый способ мышления. Я все же верю, в конечном счете, что код, создаваемый компилятором, на самом деле пытается дать правдивое разрешение. Я имею в виду, что, может быть, если вы увеличите тестовые примеры в инструкции if и будете придерживаться логического значения в одной стороне и целого числа в другом, компилятор сделает так, чтобы сгенерированный код выполнялся быстрее с булевыми выражениями на уровне машины.

Я рассматриваю это концептуальный вопрос, поэтому я дам концептуальный ответ. Это обсуждение напоминает мне о дискуссиях, которые я обычно рассказываю о том, означает ли эффективность кода для меньших строк кода в сборке. Похоже, эта концепция общепризнана как истина. Учитывая, что отслеживание того, насколько быстро ALU будет обрабатывать каждое утверждение, не является жизнеспособным, второй вариант заключается в том, чтобы сосредоточиться на прыжках и сравнивать в сборке. В этом случае различие между булевыми операторами или целыми числами в представленном вами кодексе становится довольно представительным. Результат выражения в С++ вернет значение, которое затем получит представление. С другой стороны, в сборке скачки и сравнения будут основываться на числовых значениях независимо от того, какой тип выражения оценивался обратно в вашем выражении С++ if. На этих вопросах важно помнить, что чисто логические утверждения, такие как эти, приводят к огромным вычислительным накладным расходам, хотя один бит способен на одно и то же.