У чому полягає перевага __builtin_expect GCC в інших операторах?


144

Я натрапив на те, #defineв якому вони використовують __builtin_expect.

Документація говорить:

Вбудована функція: long __builtin_expect (long exp, long c)

Ви можете використовувати __builtin_expectдля надання компілятору інформації про передбачення галузей. Взагалі, вам слід віддати перевагу реальним відгукам профілів для цього ( -fprofile-arcs), оскільки програмісти, як відомо, погано прогнозують, як реально працюють їхні програми. Однак є програми, в яких ці дані важко зібрати.

Повернене значення - це значення exp, яке має бути інтегральним виразом. Семантика вбудованого полягає в тому, що цього очікується exp == c. Наприклад:

      if (__builtin_expect (x, 0))
        foo ();

вказувало б на те, що ми не очікуємо дзвонити foo, оскільки ми очікуємо xнуля.

То чому б безпосередньо не використовувати:

if (x)
    foo ();

замість складного синтаксису з __builtin_expect?



3
Я думаю, що ваш прямий код повинен був бути if ( x == 0) {} else foo();.. або просто if ( x != 0 ) foo();що еквівалентно коду з документації GCC.
Наваз

Відповіді:


187

Уявіть код складання, який би генерувався з:

if (__builtin_expect(x, 0)) {
    foo();
    ...
} else {
    bar();
    ...
}

Я думаю, це має бути щось на кшталт:

  cmp   $x, 0
  jne   _foo
_bar:
  call  bar
  ...
  jmp   after_if
_foo:
  call  foo
  ...
after_if:

Видно, що інструкції розташовані в такому порядку, що barвипадок передує fooсправі (на відміну від коду С). Це може краще використовувати конвеєр процесора, оскільки стрибок перекручує вже отримані інструкції.

Перед виконанням стрибка вказівки під ним ( barкорпус) висуваються на трубопровід. Оскільки fooвипадок малоймовірний, стрибки теж малоймовірні, отже, розтрощення трубопроводу малоймовірне.


1
Це справді працює так? Чому визначення foo не може бути першим? Порядок визначення функцій не має значення, наскільки у вас є прототип, так?
kingsmasher1

63
Йдеться не про визначення функцій. Йдеться про перестановку машинного коду таким чином, що CPU зменшує ймовірність отримання інструкцій, які не виконуються.
Благовест Буюклієв

4
Ой, я розумію. Отже, ви маєте на увазі, оскільки існує велика ймовірність для того, x = 0щоб планка була надана першою. І foo, визначається пізніше, оскільки шансів (скоріше використання ймовірності) менше, правда?
kingsmasher1

1
Аааа ... дякую. Це найкраще пояснення. Асемблерний код справді зробив хитрість :)
kingsmasher1

5
Це також може
вкласти

50

Давайте декомпілюємо, щоб побачити, що з цим робить GCC 4.8

Благовест згадав про інверсію гілки для покращення конвеєра, але чи справді компілятори це роблять? Давай дізнаємось!

Без __builtin_expect

#include "stdio.h"
#include "time.h"

int main() {
    /* Use time to prevent it from being optimized away. */
    int i = !time(NULL);
    if (i)
        puts("a");
    return 0;
}

Компілюйте та декомпілюйте за допомогою GCC 4.8.2 x86_64 Linux:

gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o

Вихід:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       75 0a                   jne    1a <main+0x1a>
  10:       bf 00 00 00 00          mov    $0x0,%edi
                    11: R_X86_64_32 .rodata.str1.1
  15:       e8 00 00 00 00          callq  1a <main+0x1a>
                    16: R_X86_64_PC32       puts-0x4
  1a:       31 c0                   xor    %eax,%eax
  1c:       48 83 c4 08             add    $0x8,%rsp
  20:       c3                      retq

Порядок інструкцій у пам'яті був незмінним: спочатку повернення, putsа потім retqповернення.

З __builtin_expect

Тепер замініть if (i)на:

if (__builtin_expect(i, 0))

і ми отримуємо:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       74 07                   je     17 <main+0x17>
  10:       31 c0                   xor    %eax,%eax
  12:       48 83 c4 08             add    $0x8,%rsp
  16:       c3                      retq
  17:       bf 00 00 00 00          mov    $0x0,%edi
                    18: R_X86_64_32 .rodata.str1.1
  1c:       e8 00 00 00 00          callq  21 <main+0x21>
                    1d: R_X86_64_PC32       puts-0x4
  21:       eb ed                   jmp    10 <main+0x10>

putsБуло перенесено на самий кінець функції, в retqвідповідь!

Новий код в основному такий же, як:

int i = !time(NULL);
if (i)
    goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;

Ця оптимізація не була зроблена -O0.

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

C ++ 20 [[likely]]і[[unlikely]]

C ++ 20 стандартизував ці вбудовані C ++: Як використовувати атрибут C ++ 20's вероятно / малоймовірно в операторі if-else Вони, ймовірно, (каламбур!) Зроблять те саме.


1
Перевірте функцію dispatch_once libdispatch, яка використовує __builtin_expect для практичної оптимізації. Повільний шлях проходить одноразово і використовує __builtin_expect, щоб натякнути передбачуваному гілку, що швидкий шлях слід пройти. Швидкий шлях проходить без використання замків взагалі! mikeash.com/pyblog/…
Адам Каплан

Зрозуміло, що GCC 9.2 не має ніяких змін: gcc.godbolt.org/z/GzP6cx (насправді вже в 8.1)
Руслан

40

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

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

На жаль, якщо ситуація не дуже добре зрозуміла (можливо, вони нічого подібного не зробили), цілком може погіршити ситуацію. Документація навіть говорить:

Взагалі, вам слід віддати перевагу реальним відгукам профілів для цього ( -fprofile-arcs), оскільки програмісти, як відомо, погано прогнозують, як реально працюють їхні програми. Однак є програми, в яких ці дані важко зібрати.

Загалом, ви не повинні використовувати, __builtin_expectякщо:

  • У вас дуже реальна проблема ефективності
  • Ви вже оптимізували алгоритми в системі
  • У вас є дані про ефективність, щоб підкріпити ваше твердження, що конкретний випадок є найбільш імовірним

7
@Michael: Це насправді не опис прогнозування галузей.
Олівер Чарльворт

3
"Більшість програмістів - BAD" або все одно не кращі за компілятор. Будь-який ідіот може сказати, що в циклі for for, умова продовження, ймовірно, справдиться, але компілятор знає, що теж немає користі. Якщо з якоїсь - то причини ви написали цикл , який майже завжди зламатися відразу, і якщо ви не можете надати дані профілю компілятора для ЙОГО, то , може бути , програміст знає , що - то компілятор не робить.
Стів Джессоп

15
У деяких ситуаціях не має значення, яка галузь є більшою ймовірністю, а скоріше, яка галузь має значення. Якщо несподівана гілка призводить до переривання (), ймовірність не має значення, і очікуваній гілці слід надавати пріоритет ефективності при оптимізації.
Neowizard

1
Проблема з вашою заявою полягає в тому, що оптимізація, яку може виконати процесор відносно ймовірності гілки, майже обмежена одним: передбачення гілки, і ця оптимізація відбувається, використовуєте ви її __builtin_expectчи ні . З іншого боку, компілятор може здійснити багато оптимізацій на основі ймовірності гілки, наприклад організацію коду, щоб гарячий шлях був суміжним, переміщення коду навряд чи буде оптимізовано далі або зменшило його розмір, приймаючи рішення про те, які гілки векторизувати, краще планувати гарячий шлях тощо.
BeeOnRope

1
... без інформації розробника, він сліпий і вибирає нейтральну стратегію. Якщо розробник має рацію щодо ймовірностей (і в багатьох випадках нереально розуміти, що гілка зазвичай береться / не береться) - ви отримуєте ці переваги. Якщо ви не отримаєте певну пеню, але вона не є якось значно більшою, ніж переваги, і що найважливіше, нічого з цього якимось чином не перекриває передбачення гілки процесора.
BeeOnRope

13

Ну, як йдеться в описі, перша версія додає передбачувальний елемент до побудови, повідомляючи компілятору, що x == 0гілка є найбільш ймовірною - тобто саме гілка буде частіше брати вашу програму.

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

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

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


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

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

Дякую, у вас і Майкла, я думаю, схожі погляди, але висловлюйте різні слова :-) Я розумію, що точні внутрішні відомості про компілятор про тест-і-гілка тут пояснити неможливо :)
kingsmasher1,

Про них також дуже легко дізнатися, шукаючи Інтернет :-)
Kerrek SB

Я краще повернусь до своєї колегіальної книги compiler design - Aho, Ullmann, Sethi:-)
kingsmasher1

1

Я не бачу жодної відповіді, що стосується питання, яке, я думаю, ви задавали, перефразовано:

Чи є більш портативний спосіб натякнути прогнозування гілок на компілятор.

Назва вашого запитання змусила мене зробити це так:

if ( !x ) {} else foo();

Якщо компілятор припускає, що "true" є більш імовірним, він може оптимізувати для не викликає foo().

Проблема тут полягає лише в тому, що ви, як правило, не знаєте, що передбачає компілятор - тому будь-який код, який використовує подібну техніку, потрібно ретельно виміряти (і, можливо, відслідковувати з часом, якщо зміниться контекст).


Насправді це могло бути саме те, що ОР спочатку мала намір ввести (як зазначено в назві), але чомусь використання elseбуло залишено поза тією посадою.
Брент Бредберн

1

Я тестую його на Mac за версією @Blagovest Buyukliev та @Ciro. Збірки виглядають чітко, і я додаю коментарів;

Команди є gcc -c -O3 -std=gnu11 testOpt.c; otool -tVI testOpt.o

Коли я використовую -O3 ,, це виглядає так само, незалежно від того, існують чи ні __builtin_expect (i, 0).

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp     
0000000000000001    movq    %rsp, %rbp    // open function stack
0000000000000004    xorl    %edi, %edi       // set time args 0 (NULL)
0000000000000006    callq   _time      // call time(NULL)
000000000000000b    testq   %rax, %rax   // check time(NULL)  result
000000000000000e    je  0x14           //  jump 0x14 if testq result = 0, namely jump to puts
0000000000000010    xorl    %eax, %eax   //  return 0   ,  return appear first 
0000000000000012    popq    %rbp    //  return 0
0000000000000013    retq                     //  return 0
0000000000000014    leaq    0x9(%rip), %rdi  ## literal pool for: "a"  // puts  part, afterwards
000000000000001b    callq   _puts
0000000000000020    xorl    %eax, %eax
0000000000000022    popq    %rbp
0000000000000023    retq

При компіляції з -O2 , це виглядає інакше з і без __builtin_expect (i, 0)

Спочатку без

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    xorl    %edi, %edi
0000000000000006    callq   _time
000000000000000b    testq   %rax, %rax
000000000000000e    jne 0x1c       //   jump to 0x1c if not zero, then return
0000000000000010    leaq    0x9(%rip), %rdi ## literal pool for: "a"   //   put part appear first ,  following   jne 0x1c
0000000000000017    callq   _puts
000000000000001c    xorl    %eax, %eax     // return part appear  afterwards
000000000000001e    popq    %rbp
000000000000001f    retq

Тепер із __builtin_expect (i, 0)

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    xorl    %edi, %edi
0000000000000006    callq   _time
000000000000000b    testq   %rax, %rax
000000000000000e    je  0x14   // jump to 0x14 if zero  then put. otherwise return 
0000000000000010    xorl    %eax, %eax   // return appear first 
0000000000000012    popq    %rbp
0000000000000013    retq
0000000000000014    leaq    0x7(%rip), %rdi ## literal pool for: "a"
000000000000001b    callq   _puts
0000000000000020    jmp 0x10

Підводячи підсумок, __builtin_expect працює в останньому випадку.

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