Який ефект від замовлення, якщо… інше, якщо твердження ймовірні?


187

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

if (highly_likely)
  //do something
else if (somewhat_likely)
  //do something
else if (unlikely)
  //do something

до цього?:

if (unlikely)
  //do something
else if (somewhat_likely)
  //do something
else if (highly_likely)
  //do something

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

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

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


35
Ви можете додати зауваження, що умови взаємовиключні, інакше дві версії не рівноцінні
idclev 463035818

28
Досить цікаво, як на запитання самовідповіді за годину було отримано 20+ відгуків із досить поганою відповіддю. Нічого не кличучи на ОП, але дозволювачі повинні остерігатися стрибків на вагоні стрічки. Питання може бути цікавим, але результати сумнівні.
luk32

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

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

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

Відповіді:


96

Як правило, більшість, якщо не всі процесори Intel припускають, що передні відділення не приймаються при першому їх баченні. Дивіться роботу Годбольта .

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

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

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

Таким чином, ви повинні замовити свої гілки в порядку зменшення ймовірності, щоб отримати найкращий прогноз гілки з "першої зустрічі".

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

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

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

Природно, це все виходить у вікно, якщо деякі тести є значно дешевшими, ніж інші.


19
Також варто подумати про те, наскільки дорогі самі тести: якщо один тест є лише трохи більш імовірним, але набагато дорожчим, тоді, можливо, варто спершу поставити інший тест, оскільки економія від не дорогого тесту, швидше за все, перевищить заощадження від галузевого прогнозування тощо
psmears

Посилання ви надали не підтримують свій висновок , як правило, більшість , якщо не всі процесори Intel вважати , передні гілки не буде прийнята в перший раз , вони бачать їх . Насправді це справедливо лише для відносно незрозумілого процесора Arrendale, результати якого показані першими. Результати мейнстріму «Айві Брідж» та «Хасвелл» зовсім не підтримують це. Хасвелл виглядає дуже близько до того, що "завжди прогнозують падіння" для небачених гілок, а Айві Брідж зовсім не зрозумілий.
BeeOnRope

Як правило, прийнято розуміти, що процесори насправді не використовують статичні прогнози, як це було раніше. Дійсно, сучасний Intel, ймовірно, використовує щось на кшталт імовірнісного прогноктора TAGE. Ви просто хеш-гілки історії в різні таблиці історії і взяти таку, яка відповідає найдовшій історії. Він використовує "тег", щоб спробувати уникнути псевдоніму, але тег має лише кілька біт. Якщо ви пропустите всю історію, напевно, робиться передбачення за замовчуванням, яке не обов'язково залежить від напрямку гілки (на Haswell ми можемо сказати, що це явно немає).
BeeOnRope

44

Я склав наступний тест, щоб вчасно виконати два різних if... else ifблоки, один відсортований за ймовірністю, а інший відсортований у зворотному порядку:

#include <chrono>
#include <iostream>
#include <random>
#include <algorithm>
#include <iterator>
#include <functional>

using namespace std;

int main()
{
    long long sortedTime = 0;
    long long reverseTime = 0;

    for (int n = 0; n != 500; ++n)
    {
        //Generate a vector of 5000 random integers from 1 to 100
        random_device rnd_device;
        mt19937 rnd_engine(rnd_device());
        uniform_int_distribution<int> rnd_dist(1, 100);
        auto gen = std::bind(rnd_dist, rnd_engine);
        vector<int> rand_vec(5000);
        generate(begin(rand_vec), end(rand_vec), gen);

        volatile int nLow, nMid, nHigh;
        chrono::time_point<chrono::high_resolution_clock> start, end;

        //Sort the conditional statements in order of increasing likelyhood
        nLow = nMid = nHigh = 0;
        start = chrono::high_resolution_clock::now();
        for (int& i : rand_vec) {
            if (i >= 95) ++nHigh;               //Least likely branch
            else if (i < 20) ++nLow;
            else if (i >= 20 && i < 95) ++nMid; //Most likely branch
        }
        end = chrono::high_resolution_clock::now();
        reverseTime += chrono::duration_cast<chrono::nanoseconds>(end-start).count();

        //Sort the conditional statements in order of decreasing likelyhood
        nLow = nMid = nHigh = 0;
        start = chrono::high_resolution_clock::now();
        for (int& i : rand_vec) {
            if (i >= 20 && i < 95) ++nMid;  //Most likely branch
            else if (i < 20) ++nLow;
            else if (i >= 95) ++nHigh;      //Least likely branch
        }
        end = chrono::high_resolution_clock::now();
        sortedTime += chrono::duration_cast<chrono::nanoseconds>(end-start).count();

    }

    cout << "Percentage difference: " << 100 * (double(reverseTime) - double(sortedTime)) / double(sortedTime) << endl << endl;
}

Використовуючи MSVC2017 з / O2, результати показують, що відсортована версія стабільно приблизно на 28% швидша, ніж несортована версія. За коментарем luk32, я також змінив порядок двох тестів, що робить помітну різницю (22% проти 28%). Код запускався під Windows 7 на Intel Xeon E5-2697 v2. Це, звичайно, дуже проблематично і не повинно трактуватися як остаточна відповідь.


9
ОП повинен бути обережним, оскільки зміна if... else ifзаяви може істотно вплинути на те, як логіка протікає через код. unlikelyПеревірка не може прийти часто, але не може бути бізнес - необхідність для перевірки unlikelyстану першого перед перевіркою для інших.
Люк Т Брукс

21
На 30% швидше? Ви маєте на увазі, що це було швидше приблизно на% зайвого, якщо заяви, які він не повинен був виконувати? Здається, досить розумний результат.
UKMonkey

5
Як ви це орієнтували? Який компілятор, процесор тощо? Я впевнений, що цей результат не є портативним.
luk32

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

6
Цей орієнтир не надто надійний. Компіляція з gcc 6.3.0 : g++ -O2 -march=native -std=c++14дає незначне перевагу впорядкованих умовних висловлюваннях, але в більшості випадків відсоткова різниця між двома прогонами становила ~ 5%. Кілька разів це було насправді повільніше (через відхилення). Я досить впевнений, що замовляти ifподібне подібне не варто переживати; PGO, ймовірно, повністю впорається з будь-якими подібними випадками
Джастін

30

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

Я дуже сумніваюся у ваших результатах. Я трохи змінив ваш приклад, тому простір виконання виконання простіше. Ideone досить послідовно показує, що зворотний порядок швидше, хоча і не набагато. На певних ходах навіть це час від часу перевертається. Я б сказав, що результати є непереконливими. coliru також не повідомляє про реальну різницю. Пізніше я можу перевірити процесор Exynos5422 на своєму odroid xu4.

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

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

Код:

#include <chrono>
#include <iostream>
#include <random>
#include <algorithm>
#include <iterator>
#include <functional>

using namespace std;

int main()
{
    //Generate a vector of random integers from 1 to 100
    random_device rnd_device;
    mt19937 rnd_engine(rnd_device());
    uniform_int_distribution<int> rnd_dist(1, 100);
    auto gen = std::bind(rnd_dist, rnd_engine);
    vector<int> rand_vec(5000);
    generate(begin(rand_vec), end(rand_vec), gen);
    volatile int nLow, nMid, nHigh;

    //Count the number of values in each of three different ranges
    //Run the test a few times
    for (int n = 0; n != 10; ++n) {

        //Run the test again, but now sort the conditional statements in reverse-order of likelyhood
        {
          nLow = nMid = nHigh = 0;
          auto start = chrono::high_resolution_clock::now();
          for (int& i : rand_vec) {
              if (i >= 95) ++nHigh;               //Least likely branch
              else if (i < 20) ++nLow;
              else if (i >= 20 && i < 95) ++nMid; //Most likely branch
          }
          auto end = chrono::high_resolution_clock::now();
          cout << "Reverse-sorted: \t" << chrono::duration_cast<chrono::nanoseconds>(end-start).count() << "ns" << endl;
        }

        {
          //Sort the conditional statements in order of likelyhood
          nLow = nMid = nHigh = 0;
          auto start = chrono::high_resolution_clock::now();
          for (int& i : rand_vec) {
              if (i >= 20 && i < 95) ++nMid;  //Most likely branch
              else if (i < 20) ++nLow;
              else if (i >= 95) ++nHigh;      //Least likely branch
          }
          auto end = chrono::high_resolution_clock::now();
          cout << "Sorted:\t\t\t" << chrono::duration_cast<chrono::nanoseconds>(end-start).count() << "ns" << endl;
        }
        cout << endl;
    }
}

Я отримую таку ж ~ 30% різницю у продуктивності, коли перемикаю порядок відсортованих та зворотно-відсортованих if-блоків, як це було зроблено у вашому коді. Я не впевнений, чому Ideone та coliru не мають різниці.
Карлтон

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

Якщо питання: Який ефект? відповідь не може бути Ні !
PJTraill

Так. Але я не отримую сповіщення про оновлення початкового запитання. Вони зробили формулювання відповідей застарілим. Вибачте. Я відредагую вміст пізніше, щоб вказати, що він відповів на оригінальне запитання та показав деякі результати, які підтвердили початковий пункт.
luk32

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

26

Всього мої 5 копійок. Здається, ефект від упорядкування, якщо заяви повинні залежати від:

  1. Ймовірність кожного if твердження.

  2. Кількість ітерацій, тому передбачувач гілки може запустити.

  3. Ймовірно / малоймовірно натякає компілятор, тобто макет коду.

Щоб вивчити ці фактори, я визначив наступні функції:

order_ifs ()

for (i = 0; i < data_sz * 1024; i++) {
    if (data[i] < check_point) // highly likely
        s += 3;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (data[i] == check_point) // very unlikely
        s += 1;
}

reversed_ifs ()

for (i = 0; i < data_sz * 1024; i++) {
    if (data[i] == check_point) // very unlikely
        s += 1;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (data[i] < check_point) // highly likely
        s += 3;
}

order_ifs_with_hints ()

for (i = 0; i < data_sz * 1024; i++) {
    if (likely(data[i] < check_point)) // highly likely
        s += 3;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (unlikely(data[i] == check_point)) // very unlikely
        s += 1;
}

reversed_ifs_with_hints ()

for (i = 0; i < data_sz * 1024; i++) {
    if (unlikely(data[i] == check_point)) // very unlikely
        s += 1;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (likely(data[i] < check_point)) // highly likely
        s += 3;
}

дані

Масив даних містить випадкові числа від 0 до 100:

const int RANGE_MAX = 100;
uint8_t data[DATA_MAX * 1024];

static void data_init(int data_sz)
{
    int i;
        srand(0);
    for (i = 0; i < data_sz * 1024; i++)
        data[i] = rand() % RANGE_MAX;
}

Результати

Наведені нижче результати для Intel i5 при 3,2 ГГц та G ++ 6.3.0. Перший аргумент - контрольна точка (тобто ймовірність у %% для великої ймовірності, якщо твердження), другий аргумент - data_sz (тобто кількість ітерацій).

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/50/8                   25636 ns      25635 ns      27852
ordered_ifs/75/4                    4326 ns       4325 ns     162613
ordered_ifs/75/8                   18242 ns      18242 ns      37931
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs/100/8                   3381 ns       3381 ns     207612
reversed_ifs/50/4                   5342 ns       5341 ns     126800
reversed_ifs/50/8                  26050 ns      26050 ns      26894
reversed_ifs/75/4                   3616 ns       3616 ns     193130
reversed_ifs/75/8                  15697 ns      15696 ns      44618
reversed_ifs/100/4                  3738 ns       3738 ns     188087
reversed_ifs/100/8                  7476 ns       7476 ns      93752
ordered_ifs_with_hints/50/4         5551 ns       5551 ns     125160
ordered_ifs_with_hints/50/8        23191 ns      23190 ns      30028
ordered_ifs_with_hints/75/4         3165 ns       3165 ns     218492
ordered_ifs_with_hints/75/8        13785 ns      13785 ns      50574
ordered_ifs_with_hints/100/4        1575 ns       1575 ns     437687
ordered_ifs_with_hints/100/8        3130 ns       3130 ns     221205
reversed_ifs_with_hints/50/4        6573 ns       6572 ns     105629
reversed_ifs_with_hints/50/8       27351 ns      27351 ns      25568
reversed_ifs_with_hints/75/4        3537 ns       3537 ns     197470
reversed_ifs_with_hints/75/8       16130 ns      16130 ns      43279
reversed_ifs_with_hints/100/4       3737 ns       3737 ns     187583
reversed_ifs_with_hints/100/8       7446 ns       7446 ns      93782

Аналіз

1. Порядок має значення

Для 4K ітерацій та (майже) 100% ймовірності симпатичного твердження різниця величезна 223%:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/100/4                   1673 ns       1673 ns     417073
reversed_ifs/100/4                  3738 ns       3738 ns     188087

Для ітерацій 4K та 50% ймовірності симпатичного твердження різниця становить близько 14%:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
reversed_ifs/50/4                   5342 ns       5341 ns     126800

2. Число ітерацій має значення

Різниця між 4K та 8K ітераціями для (майже) 100% ймовірності симпатичного твердження приблизно в два рази (як очікувалося):

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs/100/8                   3381 ns       3381 ns     207612

Але різниця між 4K та 8K ітераціями для 50% ймовірності симпатичного твердження становить 5,5 разів:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/50/8                   25636 ns      25635 ns      27852

Чому так? Через відсутність галузевого прогноктора. Ось галузеві пропуски для кожного згаданих вище випадків:

ordered_ifs/100/4    0.01% of branch-misses
ordered_ifs/100/8    0.01% of branch-misses
ordered_ifs/50/4     3.18% of branch-misses
ordered_ifs/50/8     15.22% of branch-misses

Так що на моєму i5 передбачувач гілок вражає невдало для не дуже вірогідних гілок та великих наборів даних.

3. Підказки Допоможіть трохи

Для ітерацій 4K результати дещо гірші на 50% ймовірності та дещо кращі для майже 100% ймовірності:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs_with_hints/50/4         5551 ns       5551 ns     125160
ordered_ifs_with_hints/100/4        1575 ns       1575 ns     437687

Але для 8K ітерацій результати завжди дещо кращі:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/8                   25636 ns      25635 ns      27852
ordered_ifs/100/8                   3381 ns       3381 ns     207612
ordered_ifs_with_hints/50/8        23191 ns      23190 ns      30028
ordered_ifs_with_hints/100/8        3130 ns       3130 ns     221205

Отже, підказки також допомагають, але просто крихітний шматочок.

Загальний висновок такий: завжди орієнтуйте код, оскільки результати можуть здивувати.

Сподіваюся, що це допомагає.


1
i5 Негалем? i5 Skylake? Просто сказати "i5" не дуже конкретно. Також я припускаю, що ви використовували g++ -O2або -O3 -fno-tree-vectorize, але ви повинні так сказати.
Пітер Кордес

Цікаво, що with_hints все ще відрізняється для впорядкованого проти зворотного. Було б добре, якби ви десь зв’язалися з джерелом. (наприклад, посилання Godbolt, бажано повна посилання, тому скорочення посилань не може гнити.)
Пітер Кордес

1
Те, що передбачувач гілок здатний добре передбачити навіть при розмірі вхідних даних 4K, тобто здатний "зламати" показник, запам'ятовуючи результати гілки через цикл з періодом у тисячі, є свідченням сили сучасних галузеві прогнози Майте на увазі, що прогноктори в деяких випадках досить чутливі до таких речей, як вирівнювання, тому важко зробити чіткі висновки щодо деяких змін. Наприклад, ви помітили протилежну поведінку для підказки в різних випадках, але це можна пояснити натяком на довільну зміну макета коду, яка вплинула на прогноктора.
BeeOnRope

1
@PeterCordes мій головний момент, поки ми можемо спробувати передбачити результати зміни, все ж таки краще виміряти продуктивність до та після зміни ... І ви праві, я мав би зазначити, що вона була оптимізована за допомогою -O3 та процесора є i5-4460 @ 3,20 ГГц
Андрій Берестовський

19

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

  • Відносна ймовірність кожної галузі. Це оригінальне питання, яке було задано. Виходячи з існуючих відповідей, мабуть, існують деякі умови, за допомогою яких упорядкування за імовірністю допомагає, але, здається, це не завжди так. Якщо відносні ймовірності не сильно відрізняються, то навряд чи можна змінити, в якому порядку вони перебувають. Однак, якщо перша умова трапиться в 99,999% часу, а наступна - частка того, що залишилося, я б припустимо, що виставлення першої ймовірнішої першої було б корисно з точки зору часу.
  • Вартість розрахунку справжньої / хибної умови для кожної галузі. Якщо часові витрати на тестування умов дійсно високі для однієї галузі та інших, то це, ймовірно, матиме значний вплив на терміни та ефективність. Наприклад, розглянемо умову, на яку потрібно обчислити 1 одиницю часу (наприклад, перевірка стану булевої змінної) порівняно з іншою умовою, на яку потрібно обчислити десятки, сотні, тисячі або навіть мільйони одиниць часу (наприклад, перевірка вмісту файл на диску або виконання складного запиту SQL проти великої бази даних). Якщо припустимо, що код перевіряє умови для порядку кожного разу, швидше, умови, ймовірно, повинні бути першими (якщо тільки вони не залежать від інших умов, які відмовляються першими).
  • Компілятор / перекладач Деякі компілятори (або інтерпретатори) можуть включати оптимізацію одного виду іншого, що може впливати на продуктивність (а деякі з них є лише у випадку, якщо під час компіляції та / або виконання вибрано певні параметри). Тому, якщо ви не орієнтуєтесь на дві компіляції та виконання ідентичного коду в одній і тій же системі, використовуючи той самий компілятор, де єдиною різницею є порядок відповідних гілок, вам потрібно буде дати деяку свободу для варіантів компілятора.
  • Операційна система / Обладнання Як зазначали luk32 та Yakk, різні процесори мають власні оптимізації (як і операційні системи). Тож орієнтири тут знову чутливі до змін.
  • Частота виконання коду блоку коду Якщо до блоку, який включає гілки, рідко можна отримати доступ (наприклад, лише один раз під час запуску), то, мабуть, має значення дуже мало, в якому порядку ви розміщуєте гілки. З іншого боку, якщо ваш код забивається у цей блок коду під час критичної частини коду, замовлення може мати велике значення (залежно від орієнтирів).

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

Моє особисте правило (у більшості випадків за відсутності еталону) - замовлення на основі:

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

13

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

if (likely(access_ok(VERIFY_READ, from, n))) {
    kasan_check_write(to, n);
    res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
    memset(to + (n - res), 0, res);

Тут припущення полягає в тому, що перевірка доступу пройде і що помилка не повертається res. Спроба змінити порядок будь-якого з цих, якщо б пункти просто плутали код, але likely()іunlikely() макроси фактично допомагають читати, вказуючи, що це нормальний випадок, а що виняток.

Реалізація цих макросів Linux використовує особливості GCC . Здається, що кланг і компілятор Intel C підтримують один і той же синтаксис, але MSVC не має такої функції .


4
Це було б корисніше, якщо ви могли б пояснити, як визначені likely()та unlikely()макроси, і включити деяку інформацію про відповідну функцію компілятора.
Нейт Елдредж

1
AFAIK, ці підказки "лише" змінюють компонування пам'яті блоків коду і те, чи так, чи ні, призведе до стрибка. Це може мати переваги щодо продуктивності, наприклад, для необхідності (або їх відсутності) для читання сторінок пам'яті. Але це не переставляє порядок, в якому оцінюються умови довгих списків інших прихильників
Хаген фон Ейтцен

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

7

Також залежить від вашого компілятора та платформи, для якої ви компілюєте.

Теоретично найвірогідніша умова повинна зробити максимально меншим стрибок управління.

Зазвичай, найвірогідніший стан повинен бути першим:

if (most_likely) {
     // most likely instructions
} else 

Найпопулярніші асми засновані на умовних гілках, які стрибають, коли умова справжня . Цей код C, ймовірно, буде перекладений у такий псевдо асм:

jump to ELSE if not(most_likely)
// most likely instructions
jump to end
ELSE:

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


2
Ви заявляли, що умовна гілка виникає тоді, коли умова істинна, але приклад "псевдосм" показує протилежне. Крім того, не можна сказати, що умовні стрибки (набагато менше усіх стрибків) затримують конвеєр, оскільки сучасні процесори зазвичай мають прогнозування гілок. Насправді, якщо прогнозується взяття гілки, але потім її не буде взято, трубопровід буде зупинений. Я б все ще намагався сортувати умови у порядку зменшення ймовірності, але те, що компілятор та процесор складають із цього, дуже залежить від реалізації.
Арне Фогель

1
Я ставлю "не (most_likely)", тому, якщо most_likely вірно, контроль буде продовжуватися без стрибків.
NoImaginationGuy

1
"Найпопулярніші зорі базуються на умовних гілках, які стрибають, коли умова справжня". Які МСА це були б? Це, звичайно, не вірно ні для x86, ні для ARM. Пекло для базових процесорів ARM (і дуже давніх x86, навіть для складних bps, як правило, все-таки починається з цього припущення, а потім адаптується) передбачувач гілок передбачає, що передня гілка не береться, а гілки назад є завжди, тому протилежне до твердження правда.
Ву

1
Компілятори, які я намагався, здебільшого використовували підхід, про який я говорив вище, для простого тестування. Зауважимо, що clangнасправді було застосовано інший підхід test2і test3: через евристику, яка вказує на те, що a < 0або == 0тест, ймовірно, помилковий, він вирішив клонувати решту функції на обох контурах, тому він може зробити condition == falseпадіння через шлях. Це можливо лише тому, що залишок функції короткий: test4я додав ще одну операцію, і це повернувся до підходу, який я окреслював вище.
BeeOnRope

1
@ArneVogel - правильно передбачувані гілки не повністю затримують конвеєр на сучасних процесорах, але вони все ще часто значно гірші, ніж не взяті: (1) вони означають, що потік управління не є суміжним, тому решта інструкцій після цього jmpне є корисно, тому трафік пропускної здатності / декодування витрачається даремно (2) навіть при прогнозуванні сучасних великих ядер лише за один цикл, так що це встановлює жорстку межу в 1 взятій гілці / циклі (OTOH сучасний Intel може робити 2 не взяті / цикл) (3 ) для прогнозування галузей важче боротися з взятими послідовними гілками, а у випадку швидких + повільних
прогнозів

6

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

mingw32-g ++. exe -O3 -Wall -std = c ++ 11 -прийняття -g

vector<int> rand_vec(10000000);

GCC здійснив однакову трансформацію в обох оригінальних кодах.

Зауважте, що тільки дві перші умови перевіряються, оскільки третя завжди повинна бути справжньою, GCC тут є своєрідним Шерлоком.

Зворотний

.L233:
        mov     DWORD PTR [rsp+104], 0
        mov     DWORD PTR [rsp+100], 0
        mov     DWORD PTR [rsp+96], 0
        call    std::chrono::_V2::system_clock::now()
        mov     rbp, rax
        mov     rax, QWORD PTR [rsp+8]
        jmp     .L219
.L293:
        mov     edx, DWORD PTR [rsp+104]
        add     edx, 1
        mov     DWORD PTR [rsp+104], edx
.L217:
        add     rax, 4
        cmp     r14, rax
        je      .L292
.L219:
        mov     edx, DWORD PTR [rax]
        cmp     edx, 94
        jg      .L293 // >= 95
        cmp     edx, 19
        jg      .L218 // >= 20
        mov     edx, DWORD PTR [rsp+96]
        add     rax, 4
        add     edx, 1 // < 20 Sherlock
        mov     DWORD PTR [rsp+96], edx
        cmp     r14, rax
        jne     .L219
.L292:
        call    std::chrono::_V2::system_clock::now()

.L218: // further down
        mov     edx, DWORD PTR [rsp+100]
        add     edx, 1
        mov     DWORD PTR [rsp+100], edx
        jmp     .L217

And sorted

        mov     DWORD PTR [rsp+104], 0
        mov     DWORD PTR [rsp+100], 0
        mov     DWORD PTR [rsp+96], 0
        call    std::chrono::_V2::system_clock::now()
        mov     rbp, rax
        mov     rax, QWORD PTR [rsp+8]
        jmp     .L226
.L296:
        mov     edx, DWORD PTR [rsp+100]
        add     edx, 1
        mov     DWORD PTR [rsp+100], edx
.L224:
        add     rax, 4
        cmp     r14, rax
        je      .L295
.L226:
        mov     edx, DWORD PTR [rax]
        lea     ecx, [rdx-20]
        cmp     ecx, 74
        jbe     .L296
        cmp     edx, 19
        jle     .L297
        mov     edx, DWORD PTR [rsp+104]
        add     rax, 4
        add     edx, 1
        mov     DWORD PTR [rsp+104], edx
        cmp     r14, rax
        jne     .L226
.L295:
        call    std::chrono::_V2::system_clock::now()

.L297: // further down
        mov     edx, DWORD PTR [rsp+96]
        add     edx, 1
        mov     DWORD PTR [rsp+96], edx
        jmp     .L224

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

Тепер я спробував усі 6 комбінацій if’s, верхні 2 - це оригінальний зворотний і відсортований. високий> = 95, низький - 20, середній - 20-94, з 10000000 ітерацій кожен.

high, low, mid: 43000000ns
mid, low, high: 46000000ns
high, mid, low: 45000000ns
low, mid, high: 44000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 44000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 45000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 42000000ns
mid, low, high: 46000000ns
high, mid, low: 46000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 43000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 44000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 48000000ns
high, mid, low: 44000000ns
low, mid, high: 44000000ns
mid, high, low: 45000000ns
low, high, mid: 45000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 46000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 45000000ns
low, high, mid: 44000000ns

high, low, mid: 42000000ns
mid, low, high: 46000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 45000000ns
low, high, mid: 44000000ns

1900020, 7498968, 601012

Process returned 0 (0x0)   execution time : 2.899 s
Press any key to continue.

То чому порядок високий, низький, середній тоді швидший (незначно)

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

          if (i >= 95) ++nHigh;               // most predictable with 94% taken
          else if (i < 20) ++nLow; // (94-19)/94% taken ~80% taken
          else if (i >= 20 && i < 95) ++nMid; // never taken as this is the remainder of the outfalls.

Так гілки будуть прогнозовані, взяті, взяті та залишені

6% + (0,94 *) 20% неправильних прогнозів.

"Відсортовано"

          if (i >= 20 && i < 95) ++nMid;  // 75% not taken
          else if (i < 20) ++nLow;        // 19/25 76% not taken
          else if (i >= 95) ++nHigh;      //Least likely branch

Гілки будуть прогнозовані з не взяті, не взяті і Шерлок.

25% + (0,75 *) 24% неправильних прогнозів

Даючи різницю 18-23% (виміряна різниця ~ 9%), але нам потрібно обчислити цикли замість неправильного прогнозування%.

Припустимо, що 17 циклів неправильно прогнозують штраф на моєму процесорі Nehalem, і що кожна перевірка потребує 1 циклу для видачі (4-5 інструкцій), і цикл також займає один цикл. Залежності даних - це лічильники та змінні циклу, але коли помилки непередбачувані, вони не повинні впливати на терміни.

Отже, для "зворотного" ми отримуємо терміни (це має бути формула, яка використовується в архітектурі комп'ютерів: кількісний підхід IIRC).

mispredict*penalty+count+loop
0.06*17+1+1+    (=3.02)
(propability)*(first check+mispredict*penalty+count+loop)
(0.19)*(1+0.20*17+1+1)+  (= 0.19*6.4=1.22)
(propability)*(first check+second check+count+loop)
(0.75)*(1+1+1+1) (=3)
= 7.24 cycles per iteration

те ж саме для "відсортованих"

0.25*17+1+1+ (=6.25)
(1-0.75)*(1+0.24*17+1+1)+ (=.25*7.08=1.77)
(1-0.75-0.19)*(1+1+1+1)  (= 0.06*4=0.24)
= 8.26

(8,26-7,24) /8,26 = 13,8% проти ~ 9% виміряно (близько до вимірюваного!?!).

Тож очевидність ОП не очевидна.

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

Зміна порядку тесту змінила результати, але це могло бути пов'язано з різними вирівнюваннями циклу запуску, який в ідеалі повинен бути 16 байтів, вирівняних на всіх нових процесорах Intel, але це не в цьому випадку.


4

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

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


6
Невдачі в галузевому прогнозуванні дорогі. У microbenchmarks, вони під коштувало , тому що x86s мають велику таблицю галузевих провісників. З тісним циклом на тих самих умовах процесор знає краще, ніж ви, який з них найбільш імовірно. Але якщо у вас є гілки по всьому коду, у вашої кеш-пам’яті прогнозування гілок не вистачає слотів, і процесор передбачає, що за замовчуванням є все. Знаючи, що ця здогадка за замовчуванням, може економити цикли по всій базі коду.
Якк - Адам Невраумон

Відповідь @Yakk Jack тут є єдино правильною. Не робіть оптимізацій, які зменшують читабельність, якщо ваш компілятор може зробити цю оптимізацію. Ви б не робили постійного складання, усунення мертвого коду, розгортання циклу чи будь-яку іншу оптимізацію, якщо ваш компілятор робить це для вас, чи не так? Напишіть свій код, використовуйте керовану профілем оптимізацію (яка розроблена для вирішення цієї проблеми, оскільки кодери смокчуть у здогадуванні), а потім подивіться, чи оптимізує ваш компілятор чи ні. Зрештою, у будь-якому випадку ви не хочете мати будь-яких розгалужень у критичному виконанні коду.
Крістоф Дігельман

@Christoph Я б не включав код, про який я знав, що він мертвий. Я б не використовував, i++коли ++iб це зробив, тому що я знаю, що i++для деяких ітераторів важко оптимізуватись ++iі різниця (для мене) не має значення. Йдеться про уникнення песимізації; ставлення найвірогіднішого блоку спочатку як звичка за замовчуванням не призведе до помітного зниження читабельності (і може насправді допомогти!), при цьому в результаті вийде код, який є зручним для прогнозування галузей (і тим самим дасть вам рівномірний невеликий приріст продуктивності, який неможливо відновити пізнішою мікрооптимізацією)
Якк - Адам Невраумон

3

Якщо ви вже знаєте відносну ймовірність твердження if-else, то для цілей ефективності було б краще використовувати відсортований спосіб, оскільки він перевірить лише одну умову (справжню).

Неортинованим способом компілятор буде зайво перевіряти всі умови та потребуватиме часу.

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