Як працюють імовірні / малоймовірні макроси в ядрі Linux і яка їх користь?


349

Я копав деякі частини ядра Linux і знаходив дзвінки на зразок цього:

if (unlikely(fd < 0))
{
    /* Do something */
}

або

if (likely(!err))
{
    /* Do something */
}

Я знайшов їх визначення:

#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)

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


7
Це насправді не конкретно для ядра Linux чи макросів, а для оптимізації компілятора. Чи слід це повторно позначити, щоб це відобразити?
Коді Брості

11
У статті Що повинен знати кожен програміст про пам'ять (стор. 57), міститься поглиблене пояснення.
Торстен Марек

2
дивіться такожBOOST_LIKELY
Ruggero Turra

4
Пов'язане: орієнтир щодо використання__builtin_expect іншого питання.
ВАТ

13
Немає проблеми з портативністю. Ви можете тривіальні робити такі речі , як #define likely(x) (x)і #define unlikely(x) (x)на платформах , які не підтримують цей вид натякаючи.
Девід Шварц

Відповіді:


329

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

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


3
Ці макроси переважно використовувались для перевірки помилок. Тому що помилка залишається менш вірогідною, ніж звичайна робота. Кілька людей роблять профілювання або розрахунок, щоб вирішити найбільш використовуваний лист ...
gavenkoa

51
Що стосується фрагмента "[...]that it is being run in a tight loop", то багато процесорів мають передбачувач гілок , тому використання цих макросів допомагає виконувати лише перший код часу або тоді, коли таблиця історії буде перезаписана іншою гілкою з тим самим індексом у таблицю розгалуження. У тісному циклі, і якщо припустити, що гілка проходить в одну сторону більшу частину часу, передбачувач гілки, швидше за все, почне вгадувати правильну гілку дуже швидко. - твій друг у педантичності.
Росс Роджерс

8
@RossRogers: Що насправді відбувається, компілятор впорядковує гілки, тож загальний випадок - це неприйнятий. Це швидше, навіть якщо прогнозування галузей працює. Витягнуті гілки є проблематичними для отримання інструкцій та декодування, навіть якщо вони передбачені ідеально. Деякі процесори статично прогнозують гілки, які не знаходяться в таблиці історії, як правило, з припущеннями, не взятіми для передніх гілок. Процесорні процесори Intel не працюють таким чином: вони не намагаються перевірити, чи є запис таблиці прогнозів для цієї гілки, вони просто користуються нею. Гаряча гілка та холодна гілка можуть мати псевдонім той самий запис ...
Пітер Кордес

12
Ця відповідь здебільшого застаріла, оскільки головне твердження полягає в тому, що це допомагає передбаченню гілок, і як зазначає @PeterCordes, у більшості сучасних апаратних засобів немає неявного або явного статичного прогнозування гілок. Насправді, підказник використовується компілятором для оптимізації коду, чи стосується він статичних підказів гілок чи будь-якого іншого типу оптимізації. Для більшості архітектур сьогодні важлива саме «будь-яка інша оптимізація», наприклад, зробити гарячі шляхи суміжними, краще запланувати
запланований

3
@BeeOnRope із-за попереднього вибору кешу та розміру слова, все ж є перевага для лінійного запуску програми. Наступне місце пам’яті вже буде знайдено і в кеші, ціль гілки може бути, а може й ні. За допомогою 64-бітного процесора ви одночасно захоплюєте принаймні 64 біти. Залежно від перемежування DRAM, це може бути схоплене 2x 3x або більше бітів.
Брайс

88

Давайте декомпілюємо, щоб побачити, що з цим робить 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)
        printf("%d\n", 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 14                   jne    24 <main+0x24>
  10:       ba 01 00 00 00          mov    $0x1,%edx
  15:       be 00 00 00 00          mov    $0x0,%esi
                    16: R_X86_64_32 .rodata.str1.1
  1a:       bf 01 00 00 00          mov    $0x1,%edi
  1f:       e8 00 00 00 00          callq  24 <main+0x24>
                    20: R_X86_64_PC32       __printf_chk-0x4
  24:       bf 00 00 00 00          mov    $0x0,%edi
                    25: R_X86_64_32 .rodata.str1.1+0x4
  29:       e8 00 00 00 00          callq  2e <main+0x2e>
                    2a: R_X86_64_PC32       puts-0x4
  2e:       31 c0                   xor    %eax,%eax
  30:       48 83 c4 08             add    $0x8,%rsp
  34:       c3                      retq

Порядок інструкцій у пам'яті був незмінним: спочатку printfі потім, 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 11                   je     21 <main+0x21>
  10:       bf 00 00 00 00          mov    $0x0,%edi
                    11: R_X86_64_32 .rodata.str1.1+0x4
  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
  21:       ba 01 00 00 00          mov    $0x1,%edx
  26:       be 00 00 00 00          mov    $0x0,%esi
                    27: R_X86_64_32 .rodata.str1.1
  2b:       bf 01 00 00 00          mov    $0x1,%edi
  30:       e8 00 00 00 00          callq  35 <main+0x35>
                    31: R_X86_64_PC32       __printf_chk-0x4
  35:       eb d9                   jmp    10 <main+0x10>

printf(компілює__printf_chk ) був перенесений в самому кінці функції, після того, якputs того, як і повернення , щоб поліпшити пророкування розгалужень , як згадувалося іншими відповідями.

Отже, це в основному те саме, що:

int main() {
    int i = !time(NULL);
    if (i)
        goto printf;
puts:
    puts("a");
    return 0;
printf:
    printf("%d\n", i);
    goto puts;
}

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

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

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

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


71

Це макроси, які дають підказки компілятору про те, яким шляхом може йти гілка. Макроси розширюються до специфічних розширень GCC, якщо вони доступні.

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

if (unlikely(x)) {
  dosomething();
}

return x;

Тоді він може реструктурувати цей код, щоб виглядати щось на кшталт:

if (!x) {
  return x;
}

dosomething();
return x;

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

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

Існує ряд інших стратегій, які компілятор і процесор можуть використовувати в цих сценаріях. Ви можете дізнатися більше про те, як працюють прогнози філій у Вікіпедії: http://en.wikipedia.org/wiki/Branch_predictor


3
Крім того, це впливає на сліди ікаче - утримуючи навряд чи фрагменти коду з гарячої контури.
fche

2
Точніше, він може зробити це з gotoз , не повторюючи return x: stackoverflow.com/a/31133787/895245
Чіро Сантіллі郝海东冠状病六四事件法轮功

7

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

Наприклад, на процесорі PowerPC незабарвлене відділення може зайняти 16 циклів, правильно натякане на 8 і на неправильно натякане на 24. Хороший натяк може призвести до величезної зміни.

Переносимість насправді не є проблемою - імовірно, визначення є в заголовку на платформі; ви можете просто визначити "вірогідний" і "малоймовірний" нічим для платформ, які не підтримують статичні підказки гілок.


3
Для запису x86 займає додаткове місце для натяків на гілки. Ви повинні мати однобайтовий префікс на гілках, щоб вказати відповідний підказку. Погодився, що натякання - це гарна річ (TM).
Коді Бруті

2
Данг процесорів CISC та їх інструкції зі змінною довжиною;)
місячне тінь

3
Процесори Dang RISC -
тримайтеся

7
@CodyBrocious: натяк на гілки було введено разом із P4, але було припинено разом із P4. Усі інші процесори x86 просто ігнорують ці префікси (адже префікси завжди ігноруються в контекстах, де вони безглузді). Ці макроси не призводять до того, що gcc насправді випромінює префікси підказки для гілок на x86. Вони допоможуть вам отримати gcc, щоб викласти свою функцію з меншою кількістю взятих гілок на швидкому шляху.
Пітер Кордес

5
long __builtin_expect(long EXP, long C);

Ця конструкція повідомляє компілятору, що вираз EXP, швидше за все, матиме значення C. Повернене значення - EXP. __builtin_expect призначений для використання в умовному виразі. Майже у всіх випадках він буде використовуватися в контексті булевих виразів, і в цьому випадку набагато зручніше визначити два допоміжні макроси:

#define unlikely(expr) __builtin_expect(!!(expr), 0)
#define likely(expr) __builtin_expect(!!(expr), 1)

Ці макроси потім можна використовувати як в

if (likely(a > 1))

Довідка: https://www.akkadia.org/drepper/cpumemory.pdf


1
Як було запропоновано в коментарі до іншої відповіді - в чому причина подвійної інверсії в макросах (тобто навіщо використовувати __builtin_expect(!!(expr),0)замість просто __builtin_expect((expr),0)?
Майкл Фірт

1
@MichaelFirth "подвійна інверсія" !!еквівалентна тому, щоб викинути щось на a bool. Дехто любить писати це так.
Бен XO

2

(загальний коментар - інші відповіді висвітлюють деталі)

Немає причин, щоб ви втратили портативність, використовуючи їх.

У вас завжди є можливість створити простий нульовий ефект "inline" або макрос, який дозволить вам компілювати на інших платформах з іншими компіляторами.

Ви просто не отримаєте користі від оптимізації, якщо будете на інших платформах.


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

2
Я думаю, що ви насправді погоджуєтесь між собою - це просто заплутано. (З огляду на це, коментар Ендрю говорить: "Ви можете використовувати їх, не втрачаючи портативності", але гострозуб думав, що він сказав "не використовуйте їх, оскільки вони не є портативними", і заперечив.)
Мірал

2

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

Ця особливість в Linux дещо неправильно використовується в драйверах. Як вказує osgx в семантиці гарячого атрибута , будь-яка функція hotабо coldфункція, викликана в блоці, може автоматично натякати, що умова є ймовірною чи ні. Наприклад, dump_stack()позначено coldтак, що це зайве,

 if(unlikely(err)) {
     printk("Driver error found. %d\n", err);
     dump_stack();
 }

Майбутні версії програми gccможуть вибірково вбудовувати функцію, засновану на цих підказках. Також були думки, що це не так boolean, а оцінка, як це, швидше за все , і т.д. Взагалі, слід віддавати перевагу використанню якогось альтернативного механізму, наприклад cold. Немає жодної причини використовувати його в будь-якому місці, окрім гарячих доріжок. Те, що компілятор буде робити в одній архітектурі, може бути зовсім іншим.


2

У багатьох випусках Linux ви можете знайти complier.h в / usr / linux /, ви можете включити його для використання просто. І інша думка, що навряд чи () є кориснішою, ніж вірогідною (), тому що

if ( likely( ... ) ) {
     doSomething();
}

її можна оптимізувати також у багатьох компіляторах.

І до речі, якщо ви хочете спостерігати за детальною поведінкою коду, ви можете просто так:

gcc -c test.c objdump -d test.o> obj.s

Потім, відкривши obj.s, ви можете знайти відповідь.


1

Вони підказують компілятору для створення префіксів підказки на гілках. У x86 / x64 вони займають один байт, тож ви отримаєте щонайбільше збільшення на один байт для кожної гілки. Що стосується продуктивності, то вона повністю залежить від програми - у більшості випадків передбачувач гілок на процесорі проігнорує їх у ці дні.

Редагувати: Забули про одне місце, яким вони насправді можуть реально допомогти. Це може дозволити компілятору переупорядкувати графік потоку управління, щоб зменшити кількість гілок, взятих за 'ймовірний' шлях. Це може мати помітне поліпшення циклів, коли ви перевіряєте кілька випадків виходу.


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

1

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

Спосіб побудови інструкцій гілок залежить від архітектури процесора.

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