Чи є підказка компілятора для GCC, щоб змусити передбачення гілок завжди йти певним шляхом?


118

Що стосується архітектур Intel, чи є спосіб доручити компілятору GCC генерувати код, який завжди змушує передбачити гілки певним чином у моєму коді? Чи підтримує апаратне забезпечення Intel це? Що з іншими компіляторами або апаратними засобами?

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

for (;;) {
  if (normal) { // How to tell compiler to always branch predict true value?
    doSomethingNormal();
  } else {
    exceptionalCase();
  }
}

Як наступне питання щодо Евджана Мустафи, чи може просто натяк вказати підказку вперше, коли процесор стикається з інструкцією, і все наступне передбачення гілок, нормально функціонує?


може також кинути виняток, якщо щось стане ненормальним (що не залежить від компілятора)
Shep

Відповіді:


9

Станом на C ++ 20 ймовірні та малоймовірні атрибути повинні бути стандартизовані та вже підтримуються в g ++ 9 . Отже, як тут обговорювалося , ви можете написати

if (a>b) {
  /* code you expect to run often */
  [[likely]] /* last statement */
}

наприклад, у наступному коді інший блок стає вбудованим завдяки [[unlikely]]блоку if

int oftendone( int a, int b );
int rarelydone( int a, int b );
int finaltrafo( int );

int divides( int number, int prime ) {
  int almostreturnvalue;
  if ( ( number % prime ) == 0 ) {
    auto k                         = rarelydone( number, prime );
    auto l                         = rarelydone( number, k );
    [[unlikely]] almostreturnvalue = rarelydone( k, l );
  } else {
    auto a            = oftendone( number, prime );
    almostreturnvalue = oftendone( a, a );
  }
  return finaltrafo( almostreturnvalue );
}

Godbolt-посилання, що порівнює наявність / відсутність атрибута


Навіщо використовувати [[unlikely]]в ifпорівнянні [[likely]]з в else?
WilliamKF

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

Дуже здорово. Шкода, що метод не застосовується до старих версій C ++.
Максим Єгорушкін

Фантастичне посилання на боболиста
Льюїс Келсі

87

GCC підтримує функцію __builtin_expect(long exp, long c)надання такого роду функцій. Ви можете перевірити документацію тут .

Де expвикористовується умова і cочікуване значення. Наприклад, у вашому випадку ви хочете

if (__builtin_expect(normal, 1))

Через незручний синтаксис це зазвичай використовується шляхом визначення двох власних макросів

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

просто полегшити завдання.

Майте на увазі, що:

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

3
Чи є причина, що ви показуєте макрос, а не constexprфункцію?
Коламбо

22
@Columbo: Я не думаю, що constexprфункція може замінити цей макрос. Це повинно бути в ifзаяві безпосередньо, я вірю. Ця ж причина assertніколи не може бути constexprфункцією.
Mooing Duck

1
@MooingDuck Я згоден, хоча причин для твердження є більше .
Шафік Ягмур

7
@Columbo однією з причин використовувати макрос було б тому, що це одне з небагатьох місць у C або C ++, де макрос є більш семантично правильним, ніж функція. Функція з'являється тільки на роботу з - за оптимізації (це є оптимізація: constexprговорить тільки про вартість семантиці, а не вбудовування в конкретної реалізації збірки); прямолінійна інтерпретація коду без сенсу. Немає жодної причини використовувати функцію для цього.
Левшенко

2
@ Левшенко Подумайте, що __builtin_expectсаме по собі є підказкою щодо оптимізації, тому стверджувати, що метод, що спрощує його використання, залежить від оптимізації, є… не переконливим. Крім того, я не додав constexprспецифікатор, щоб змусити його працювати в першу чергу, а змусити його працювати в постійних виразах. І так, є причини використовувати функцію. Наприклад, я не хотів би забруднювати всю свою область імен милим маленьким іменем, таким як likely. Мені доведеться використовувати, наприклад LIKELY, щоб підкреслити, що це макрос і уникати зіткнень, але це просто некрасиво.
Коламбо

46

gcc має довгий __builtin_expect (довгий досвід, довгий с) ( моє наголос ):

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

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

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

вказує на те, що ми не очікуємо дзвінка foo, оскільки ми очікуємо, що x дорівнює нулю. Оскільки ви обмежені інтегральними виразами для exp, вам слід використовувати такі конструкції, як

if (__builtin_expect (ptr != NULL, 1))
   foo (*ptr);

при тестуванні значень вказівника або плаваючої крапки.

Як зазначається в документації, вам слід віддати перевагу реальним відгукам профілів, і ця стаття показує практичний приклад цього, і як це, в їхньому випадку, принаймні, вдосконалюється в порівнянні з використанням __builtin_expect. Також див. Як використовувати оптимізації з керуванням профілю в g ++? .

Ми також можемо знайти статтю для початківців ядра Linux про макроси ядра ймовірно () та малоймовірно (), які використовують цю функцію:

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

Зауважте, що !!використовується в макросі, ми можемо знайти пояснення цього в розділі Чому використовувати !! (умова) замість (умова)? .

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

Зауважте, хоча вбудовані файли не є портативними, а також підтримує __builtin_expect .

Також у деяких архітектурах це може не змінити значення .


Що достатньо для ядра Linux, для C ++ 11 недостатньо.
Максим Єгорушкін

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

44

Ні, немає. (Принаймні, на сучасних процесорах x86.)

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

Дивіться це запитання для більш детальної інформації: Насправді використовується прогноз префіксальної гілки Intel x86 0x2E / 0x3E?

Щоб було зрозуміло, __builtin_expectта / або використання -fprofile-arcs може покращити продуктивність вашого коду, як даючи підказки передбачувачу гілки за допомогою макета коду (див. Оптимізація продуктивності складання x86-64 - Вирівнювання та передбачення гілок ), а також покращуючи поведінку кешу зберігаючи "малоймовірний" код подалі від "ймовірного" коду.


9
Це неправильно. У всіх сучасних версіях x86 алгоритм прогнозування за замовчуванням полягає в тому, щоб передбачити, що передні гілки не взяті і що відсталі гілки є (див. Software.intel.com/en-us/articles/… ). Таким чином, переставляючи код, ви можете ефективно підказати процесору. Це саме те, що робить GCC під час використання __builtin_expect.
Немо

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

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

ІМО це не марно; це корисне роз'яснення того, як насправді працюють процесори та компілятори, що може бути актуальним для аналізу продуктивності з / без цих параметрів. наприклад, ви зазвичай не можете використовувати __builtin_expectтривіально для створення тестового випадку, який ви можете виміряти perf stat, матиме дуже високий показник неправильного прогнозування галузі. Це просто впливає на компонування гілок . І BTW, Intel, оскільки Sandybridge або, принаймні, Haswell, не використовують статичне передбачення багато / зовсім; у БТГ завжди є якісь прогнози, будь то несвіжий псевдонім чи ні. xania.org/201602/bpu-part-two
Пітер Кордес

24

Правильний спосіб визначення ймовірних / малоймовірних макросів у C ++ 11 є наступним:

#define LIKELY(condition) __builtin_expect(static_cast<bool>(condition), 1)
#define UNLIKELY(condition) __builtin_expect(static_cast<bool>(condition), 0)

Цей метод сумісний з усіма версіями C ++, на відміну від цього [[likely]], але покладається на нестандартне розширення __builtin_expect.


Коли ці макроси визначали так:

#define LIKELY(condition) __builtin_expect(!!(condition), 1)

Це може змінити значення ifвисловлювань і порушити код. Розглянемо наступний код:

#include <iostream>

struct A
{
    explicit operator bool() const { return true; }
    operator int() const { return 0; }
};

#define LIKELY(condition) __builtin_expect((condition), 1)

int main() {
    A a;
    if(a)
        std::cout << "if(a) is true\n";
    if(LIKELY(a))
        std::cout << "if(LIKELY(a)) is true\n";
    else
        std::cout << "if(LIKELY(a)) is false\n";
}

І його вихід:

if(a) is true
if(LIKELY(a)) is false

Як бачимо, визначення LIKELY, що використовується !!як акторський boolсклад, розбиває семантику if.

Суть тут не в тому operator int()і operator bool()повинна бути пов’язана. Яка хороша практика.

Скоріше, якщо використання !!(x)замість цього static_cast<bool>(x)втрачає контекст для контекстних перетворень C ++ 11 .


Зауважте, що контекстні перетворення відбулися через дефект у 2012 році, і навіть наприкінці 2014 року все ще спостерігалось розбіжність у впровадженні. Насправді це виглядає як випадок, з яким я пов’язаний, все ще не працює для gcc.
Шафік Ягмур

@ShafikYaghmour Це цікаве спостереження щодо контекстуального перетворення, в якому бере участь switch, дякую. Тут включено контекстне перетворення, яке залежить від типу boolта п'яти конкретних контекстів, перелічених там , які не включають switchконтекст.
Максим Єгорушкін

Це впливає лише на C ++, правда? Тому немає причин переходити та змінювати існуючі проекти на (_Bool)(condition)C, оскільки вони не мають перевантаження оператора.
Пітер Кордес

2
У своєму прикладі ви використовували просто (condition), а не !!(condition). Обидва trueпісля зміни цього (тестуються з g ++ 7.1). Чи можете ви побудувати приклад, який насправді демонструє проблему, про яку ви говорите, коли ви використовуєте !!булеанізацію?
Пітер Кордес

3
Як зазначив Пітер Кордес, ви говорите: "Коли ці макроси [визначені] визначені таким чином:", а потім показують макрос, використовуючи "!!", "може змінити значення, якщо заяви і порушити код. Розгляньте наступний код:" ... а потім ви показуєте код, який не використовує "!!" взагалі - який, як відомо, був зламаний ще до C ++ 11. Будь ласка, змініть відповідь, щоб показати приклад, коли даний макрос (використовуючи !!) піде не так.
Карло Вуд

18

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

Згідно з аналогічними рядками, але ще не згаданими, є специфічним для GCC способом змусити компілятор генерувати код на "холодному" шляху. Це передбачає використання атрибутів noinlineта coldатрибутів, які виконують саме те, що вони звучать, як і вони. Ці атрибути можна застосувати лише до функцій, але за допомогою C ++ 11 ви можете оголосити вбудовані лямбда-функції, і ці два атрибути також можуть бути застосовані до лямбда-функцій.

Хоча це все ще належить до загальної категорії мікрооптимізації, і тому застосовується стандартна порада - тест не здогадується - я вважаю, що це в цілому корисніше, ніж __builtin_expect. Навряд чи будь-яке покоління процесора x86 використовує підказки для прогнозування гілок ( посилання ), тому єдине, на що ви все одно зможете вплинути - це порядок складання коду складання. Оскільки ви знаєте, що таке керування помилками або код "крайнього регістру", ви можете використовувати цю примітку, щоб переконатися, що компілятор ніколи не передбачить гілку до неї і зв’яже її від "гарячого" коду при оптимізації для розміру.

Використання зразка:

void FooTheBar(void* pFoo)
{
    if (pFoo == nullptr)
    {
        // Oh no! A null pointer is an error, but maybe this is a public-facing
        // function, so we have to be prepared for anything. Yet, we don't want
        // the error-handling code to fill up the instruction cache, so we will
        // force it out-of-line and onto a "cold" path.
        [&]() __attribute__((noinline,cold)) {
            HandleError(...);
        }();
    }

    // Do normal stuff
    
}

Ще краще, GCC автоматично ігнорує це на користь зворотного зв’язку з профілем, коли він доступний (наприклад, при його компілюванні -fprofile-use).

Дивіться офіційну документацію тут: https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes


2
Префікси підказки для гілок ігноруються, оскільки вони не потрібні; ви можете досягти такого ж ефекту, просто перепорядкувавши свій код. (Алгоритм прогнозування гілок за замовчуванням полягає в тому, щоб здогадатися, що відсталі гілки прийняті, а гілки вперед не є.) Таким чином, ви можете, насправді, дати підказку процесору, і це те, що __builtin_expectробить. Це зовсім не марно. Ви праві, що coldатрибут також корисний, але ви недооцінюєте корисність, як __builtin_expectя думаю.
Немо

Сучасні процесори Intel не використовують прогнозування статичних гілок. Алгоритм, який ви описуєте, @Nemo, де передні гілки прогнозуються прийнятими, а передні гілки прогнозуються як не взяті, використовувались у попередніх процесорах і вгору через Pentium M або близько того, але сучасні проекти просто в основному здогадуються випадковим чином, індексуючи їх гілку столи в якому було б очікувати , щоб знайти інформацію про цієї галузі і використання будь-якої інформації , є (хоча це може бути по суті сміття). Тож підказки галузевого прогнозування теоретично були б корисними, але, можливо, не на практиці, саме тому Intel їх усунула.
Коді Грей

Зрозуміло, реалізація галузевого передбачення надзвичайно складна, і просторові обмеження в коментарях змусили мене значно спростити. Це справді буде цілком відповідь сама по собі. У сучасних мікроархітектурах, як Хасвелл, все ще можуть бути залишки прогнозу статичних гілок, але це не так просто, як раніше.
Коді Грей

Чи є у вас посилання на те, що "сучасні процесори Intel не використовують статичне передбачення гілок"? У власній статті Intel ( software.intel.com/en-us/articles/… ) йдеться про інше ... Але це з 2011 року
Немо

Насправді не маєте офіційної довідки, @Nemo. Intel надзвичайно стислий щодо алгоритмів прогнозування галузей, що використовуються в його мікросхемах, трактуючи їх як комерційну таємницю. Більшість того, що відомо, було з’ясовано за допомогою емпіричного тестування. Як завжди, матеріали Агнера Фога є найкращими ресурсами, але навіть він каже: "Здається, передбачувач гілок був перероблений в Хасвелл, але про його будівництво відомо мало". На жаль, я не можу згадати, де я вперше побачив тести, що демонструють статичний БП, вже не використовувався.
Коді Грей

5

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

if (__builtin_expect (x == 0, 0)) ++count;
if (__builtin_expect (y == 0, 0)) ++count;
if (__builtin_expect (z == 0, 0)) ++count;

компілятор генерує подібний код

if (x == 0) goto if1;
back1: if (y == 0) goto if2;
back2: if (z == 0) goto if3;
back3: ;
...
if1: ++count; goto back1;
if2: ++count; goto back2;
if3: ++count; goto back3;

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

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


2

Що стосується ОП, то ні, в GCC немає способу сказати процесору, щоб він завжди припускав, що гілка є або не береться. У вас є __builtin_expect, який робить те, що кажуть інші. Крім того, я думаю, ви не хочете повідомляти процесору, чи гілка взята чи не завжди . Сьогоднішні процесори, такі як архітектура Intel, можуть розпізнати досить складні зразки та ефективно адаптуватися.

Однак є випадки, коли ви хочете взяти контроль над тим, чи за замовчуванням гілка передбачена прийнятою чи ні: Коли ви знаєте, код буде називатися "холодним" щодо статистики розгалуження.

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

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

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