Що швидше: if (bool) або if (int)?


94

Яке значення краще використовувати? Логічне значення true або ціле число 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)більш ефективним і швидким.

РЕДАГУВАТИ:

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



32
Це також несправедливий тест, якщо ви не порівняєте їх із увімкненою розумною оптимізацією.
Даніель Приден

9
@Daniel: Я не використовую жоден прапор оптимізації з жодним з них. Але навіть відсутність цього, чому це породжує більше побоювань, g(bool)- це питання, на яке я шукаю розумну відповідь.
Nawaz

8
Чому б вам неприємно читати asm, а не просто запускати програму та синхронізувати результат ? Кількість інструкцій насправді не багато говорить про продуктивність. Вам потрібно врахувати не тільки довжини інструкцій, але також залежності та типи інструкцій (чи декодуються вони з декодування за допомогою повільнішого шляху мікрокоду, яких одиниць виконання вони вимагають, яка затримка та пропускна здатність інструкції, чи це відділення? Доступ до
пам'яті

2
@user unknown, і @Malvolio: Це очевидно; Я не роблю все це для виробничого коду. Як я вже згадував на початку мого допису, "Отже, я просто з цікавості написав цю програму" . Так що, це суто гіпотетичний .
Nawaz

3
Це законне запитання. Вони або еквівалентні, або один швидший. ASM, ймовірно, був розміщений в спробі допомогти або подумати вголос, тому замість того, щоб використовувати його як спосіб ухилитися від питання і сказати "просто напиши читабельний код", просто відповідай на запитання або STFU, якщо ти не знаєш або не маю нічого корисного сказати;) Мій внесок полягає в тому, що питання відповідає, а "просто написати читабельний код" - це не що інше, як ухилення від питання.
Трійко

Відповіді:


99

Для мене це має сенс. Очевидно, що ваш компілятор визначає a boolяк 8-бітове значення, і ваша системна система ABI вимагає від нього "просування" малих (<32-бітових) цілочисельних аргументів до 32-бітових, коли надсилає їх у стек викликів. Тож для порівняння abool , компілятор генерує код, щоб виділити найменш значущий байт 32-розрядного аргументу, який отримує g, і порівнює його з cmpb. У першому прикладі intаргумент використовує всі 32 біти, які були висунуті на стек, тому він просто порівнюється з цілим cmpl.


4
Я згоден. Це допомагає висвітлити, що, вибираючи тип змінної, ви обираєте його для двох потенційно конкуруючих цілей, місця для зберігання та обчислювальної продуктивності.
Трійко

3
Чи стосується це також 64-розрядних процесів, __int64це швидше, ніж int? Або процесор продає 32-розрядні цілі числа з 32-розрядними наборами команд окремо?
Crend King

1
@CrendKing, можливо, варто звернути ще одне запитання?
Відображуване ім’я

81

Компіляція з -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проти cmpb. Це означає, що різниця, якщо така є, не має значення. Судячи з неоптимізованого коду, це не справедливо.


Редагувати щоб пояснити мою думку. Неоптимізований код призначений для простої налагодження, а не для швидкості. Порівнювати швидкість неоптимізованого коду безглуздо.


8
Наскільки я згоден з вашим висновком, я думаю, ви пропускаєте цікаву частину. Чому він використовується як cmplдля одного, так і cmpbдля іншого?
jalf

22
@jalf: Оскільки a bool- це один байт, а an int- чотири. Я не думаю, що є щось особливіше за це.
CB Bailey

7
Я думаю, що інші відповіді приділяли більше уваги причинам: це тому, що дана платформа розглядається boolяк 8-бітний тип.
Олександр Гесслер,

9
@Nathan: Ні. C ++ не має бітових типів даних. Найменший тип - charце байт за визначенням і є найменшою адресною одиницею. boolРозмір визначається реалізацією і може становити 1, 4 або 8 або що завгодно. Однак упорядники, як правило, роблять це єдиним.
GManNickG

6
@Nathan: Ну, це теж складно в Java. Java каже, що дані, які представляє булеве значення, є значенням одного біта, але те, як цей біт зберігається, все ще визначено реалізацією. Прагматичні комп’ютери просто не звертаються до бітів.
GManNickG

26

Коли я компілюю це з розумним набором параметрів (зокрема -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для логічного проти cmplint для int), але в іншому тілі ідентичні. Швидкий огляд посібників Intel підказує мені: ... нічого не багато. Немає такої речі, як у посібниках Intel cmpbабо cmplв них. Вони всі, cmpі наразі я не можу знайти таблиці синхронізації. Однак я здогадуюсь, що між порівнянням байту безпосереднього та порівнянням довгого безпосереднього байту немає різниці в тактовій частоті, тому для всіх практичних цілей код ідентичний.


відредаговано, щоб додати наступне на основі вашого додавання

Причина, по якій код відрізняється в неоптимізованому випадку, полягає в тому, що він неоптимізований. (Так, це кругово, я знаю.) Коли компілятор проходить AST і генерує код безпосередньо, він нічого не знає, крім того, що знаходиться в безпосередній точці AST, в якому він знаходиться. На той момент йому бракує всієї контекстуальної інформації, необхідної знати, що в цей конкретний момент він може розглядати заявлений тип boolякint . Булеве значення, очевидно, за замовчуванням трактується як байт, і при маніпулюванні байтами в світі Intel вам потрібно робити такі дії, як sign-extension, щоб довести його до певної ширини, щоб поставити його в стек тощо (Ви не можете натиснути байт .)

Однак, коли оптимізатор переглядає AST і робить його магію, він дивиться на навколишній контекст і "знає", коли може замінити код чимось більш ефективним, не змінюючи семантики. Тому він "знає", що може використовувати ціле число в параметрі і тим самим втрачати непотрібні перетворення та розширення.


1
Ха-ха, мені подобається, як компілятор просто повернув 99, або 99 + 58 = 157 = -99 (переповнення підписаних 8 бітів) ... цікаво.
уповільнена

@Daniel: Навіть мені це сподобалось. Спочатку я сказав "де -99" і відразу зрозумів, що це робить щось дуже дивне.
Nawaz

7
lі bє суфіксами, що використовуються лише в синтаксисі AT&T. Вони просто посилаються на версії cmpвикористання 4 байтових (довгих) та 1 байтових (байтових) операндів відповідно. Там, де у синтаксисі intel є будь-яка неоднозначність, зазвичай операнд пам’яті помічається BYTE PTR, WORD PTRабо DWORD PTRзамість того, щоб наносити суфікс на код коду.
CB Bailey

Таблиці синхронізації: agner.org/optimize Обидва розміри операндів cmpмають однакову продуктивність, і немає покарань за частковий регістр за читання %dil . (Але це не заважає клангу забавно створювати стійло з частковим реєстром, використовуючи розмір байтів andна AL як частину перегортання випадків між 99 і -99.)
Пітер Кордес,

13

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

Отже, код для bool, коли він не оптимізований, насправді досягає певної довжини, щоб витягти це значення bool із стеку аргументів (використовуючи інший слот стека, щоб зберегти цей байт). Це складніше, ніж просто витягування змінної розміру власного реєстру.


Зі стандарту C ++ 03, §5.3.3 / 1: " sizeof(bool)і sizeof(wchar_t)визначаються реалізацією ". Таким чином, висловлювання sizeof(bool) == 1не є суворо правильним, якщо ви не говорите про конкретну версію певного компілятора.
ildjarn

9

На рівні машини немає такого поняття, як bool

Дуже мало архітектур набору інструкцій визначають будь-який тип логічного типу операнда, хоча часто є інструкції, які ініціюють дію на ненульові значення. Для центрального процесора, як правило, все є одним із скалярних типів або їх рядком.

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

Чому bool є одним байтом у багатьох системах?

Вибрати безпечніше char тип для bool оскільки хтось може створити з них справді великий масив.

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


2
+1 Уявіть собі накладні витрати на x86, якщо вони boolбули представлені бітами; тому байт буде приємним компромісом для швидкості / компактності даних у багатьох реалізаціях.
hardmath

1
@Billy: Я думаю, він не говорив "використовувати charзамість bool", а просто використовував " chartype", щоб означати "1 байт", посилаючись на розмір, який компілятор вибирає для boolоб'єктів.
Dennis Zickefoose

Звичайно, я не мав на увазі, що кожна програма повинна вибирати, я просто висунув обґрунтування, чому тип системного bool становить 1 байт.
DigitalRoss,

@ Денніс: Ах, це має сенс.
Billy ONeal

7

Так, дискусія весела. Але просто протестуйте:

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

#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

Порівняння на основі цілих чисел:

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

Логічний тест / друк не коментується (і ціле число коментується):

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

Вони однакові з 1 призначенням і 2 порівняннями в кожному циклі понад 30 мільйонів циклів. Знайдіть щось інше для оптимізації. Наприклад, не використовуйте strcmp без потреби. ;)



0

Підхід до вашого питання двома різними способами:

Якщо ви конкретно говорите про C ++ або будь-яку мову програмування, яка створить код збірки, ми зв’язані з тим, який код компілятор буде створювати в ASM. Ми також пов'язані з поданням істинного та хибного в c ++. Ціле число повинно зберігатися в 32 бітах, і я міг би просто використовувати байт для зберігання булевого виразу. Фрагменти ASM для умовних операторів:

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

  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

Для буля:

  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

Отже, тому порівняння швидкості настільки залежить від компіляції. У наведеному вище випадку бул буде трохи швидким, оскільки cmpпередбачає віднімання для встановлення прапорів. Це також суперечить тому, що створив ваш компілятор.

Інший підхід, набагато простіший, полягає в тому, щоб розглянути логіку виразу самостійно і намагатися не турбуватися про те, як компілятор перекладе ваш код, і я думаю, що це набагато здоровіший спосіб мислення. Я все ще вірю, зрештою, що код, який генерується компілятором, насправді намагається дати правдиве вирішення. Я маю на увазі, що, можливо, якщо ви збільшите тестові випадки в операторі if і дотримуєтеся булевого значення в одній стороні та цілим числом в іншій, компілятор зробить це так, щоб згенерований код виконувався швидше з логічними виразами на рівні машини.

Я вважаю, що це концептуальне питання, тому я дам концептуальну відповідь. Ця дискусія нагадує мені дискусії, які я часто веду, про те, чи ефективність коду перекладається на менше рядків коду в збірці. Здається, що ця концепція загальновизнана як істинна. Враховуючи, що відстеження того, як швидко ALU буде обробляти кожне твердження, є нежиттєздатним, другим варіантом було б зосередити увагу на стрибках та порівнянні в збірці. Коли це так, різниця між логічними операторами або цілими числами в представленому вами коді стає досить репрезентативною. Результат виразу на C ++ поверне значення, яке потім отримає представлення. У зборі, з іншого боку, стрибки та порівняння базуватимуться на числових значеннях, незалежно від того, який тип виразу обчислювався у вас на C ++ if. У цих питаннях важливо пам’ятати, що суто логічні твердження, подібні до цих, закінчуються величезними обчислювальними витратами, навіть якщо один біт міг би зробити те саме.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.