Чи <швидше, ніж <=?


1574

Є чи if( a < 901 )швидше if( a <= 900 ).

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


153
Я не бачу жодної причини, чому це питання слід закрити (і особливо не видалити, як показують голоси на даний момент), враховуючи його історичну значимість, якість відповіді та те, що інші головні питання у виконанні залишаються відкритими. Максимум, його слід заблокувати. Крім того, навіть якщо саме питання є дезінформованим / наївним, той факт, що він з'явився в книзі, означає, що оригінальна дезінформація існує десь у "достовірних" джерелах, і це питання є конструктивним тим, що допомагає зрозуміти це.
Джейсон C

32
Ви ніколи не казали нам, до якої книги ви звертаєтесь.
Джонатан Райнхарт

159
Введення тексту <в два рази швидше, ніж введення тексту <=.
Декін

6
Це було правдою в 8086 році.
Джошуа

7
Кількість оновлень ясно показує, що є сотні людей, які сильно переоптимізуються.
m93a

Відповіді:


1704

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

  • А testчи cmpінструкція, яка встановлюєEFLAGS
  • І Jccінструкція (стрибок) , залежно від типу порівняння (та макета коду):
    • jne - Стрибок, якщо не дорівнює -> ZF = 0
    • jz - Перейти, якщо нуль (рівний) -> ZF = 1
    • jg - Стрибок, якщо більший -> ZF = 0 and SF = OF
    • (тощо ...)

Приклад (Відредаговано для стислості) Укладено$ gcc -m32 -S -masm=intel test.c

    if (a < b) {
        // Do something 1
    }

Компілюється до:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jge     .L2                          ; jump if a is >= b
    ; Do something 1
.L2:

І

    if (a <= b) {
        // Do something 2
    }

Компілюється до:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jg      .L5                          ; jump if a is > b
    ; Do something 2
.L5:

Тож єдина різниця між ними - це jgнавпаки jge. Двоє займуть однакову кількість часу.


Я хотів би звернутися до коментаря, що ніщо не вказує на те, що різні інструкції зі стрибків займають стільки ж часу. На це трохи непросто відповісти, але ось що я можу дати: У довіднику набору інструкцій Intel всі вони об'єднані в одну загальну інструкцію Jcc(Перейти, якщо умова виконана). Це ж групування проводиться разом у Довідковому посібнику з оптимізації , в Додатку C. Затримка та пропускна здатність.

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

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

Значення для Jcc:

      Latency   Throughput
Jcc     N/A        0.5

із такою виносою Jcc:

7) Вибір умовних інструкцій зі стрибків повинен базуватися на рекомендації розділу 3.4.1 "Оптимізація прогнозування галузей" для покращення передбачуваності гілок. Коли гілки прогнозуються успішно, затримка jccфактично дорівнює нулю.

Отже, ніщо в документах Intel ніколи не трактує одну Jccінструкцію інакше, ніж інші.

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


Редагувати: Плаваюча точка

Це справедливо і для плаваючої точки x87: (Приблизно такий же код, як і вище, але doubleзамість int.)

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; Compare ST(0) and ST(1), and set CF, PF, ZF in EFLAGS
        fstp    st(0)
        seta    al                     ; Set al if above (CF=0 and ZF=0).
        test    al, al
        je      .L2
        ; Do something 1
.L2:

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; (same thing as above)
        fstp    st(0)
        setae   al                     ; Set al if above or equal (CF=0).
        test    al, al
        je      .L5
        ; Do something 2
.L5:
        leave
        ret

239
@Dyppl насправді jgі jnleтака ж інструкція, 7F:-)
Джонатан Райнхарт

17
Не кажучи вже про те, що оптимізатор може змінювати код, якщо дійсно один варіант швидший, ніж інший.
Елазар Лейбович

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

22
@jontejj Я дуже це усвідомлюю. Ви навіть читали мою відповідь? Я не зазначив нічого про однакову кількість інструкцій, я заявив, що вони складені по суті точно однакових інструкцій , за винятком того, що одна інструкція зі стрибків дивиться на один прапор, а інша інструкція зі стрибків дивиться на два прапори. Я вважаю, що я дав більш ніж адекватні докази, щоб показати, що вони семантично однакові.
Джонатан Райнхарт

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

593

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

Comparison     Subtraction
----------     -----------
A < B      --> A - B < 0
A = B      --> A - B = 0
A > B      --> A - B > 0

Тепер, коли A < Bвіднімання має запозичити високий біт, щоб віднімання було правильним, так само, як ви переносите і позичаєте при додаванні і відніманні вручну. Цей "запозичений" біт зазвичай називали бітом переносу і його можна було перевірити в гілковій інструкції. Другий біт, який називається нульовим бітом , буде встановлений, якби віднімання було однаково нульовим, що передбачало рівність.

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

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

Comparison     Subtraction  Carry Bit  Zero Bit
----------     -----------  ---------  --------
A < B      --> A - B < 0    0          0
A = B      --> A - B = 0    1          1
A > B      --> A - B > 0    1          0

Отже, реалізація гілки для A < Bможе бути виконана за однією інструкцією, оскільки біт перенесення зрозумілий лише в цьому випадку, тобто

;; Implementation of "if (A < B) goto address;"
cmp  A, B          ;; compare A to B
bcz  address       ;; Branch if Carry is Zero to the new address

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

;; Implementation of "if (A <= B) goto address;"
cmp A, B           ;; compare A to B
bcz address        ;; branch if A < B
bzs address        ;; also, Branch if the Zero bit is Set

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


10
Крім того, такі архітектури, як x86, реалізують інструкції jge, які перевіряють як нуль, так і підписують / несуть прапори.
greyfade

3
Навіть якщо це стосується даної архітектури. Які шанси, які ніхто з авторів-компіляторів ніколи не помічав, і додали оптимізацію для заміни повільнішої на швидшу?
Джон Ханна

8
Це вірно в 8080 році. У ньому є вказівки стрибати на нуль і стрибати на мінус, але жодна, яка може перевірити обидва одночасно.

4
Це також стосується процесорних процесорів 6502 та 65816, які також поширюються на Motorola 68HC11 / 12.
Лукас

31
Навіть на 8080 <=тест може бути реалізований в одній інструкції з помінявши операнди і тестування на not <(еквівалент >=) Це бажано <=з обмінюваних операндами: cmp B,A; bcs addr. Ось чому міркування цей тест було опущено Intel, вони вважали його зайвим, і ви не могли дозволити собі надлишкові інструкції в той час :-)
Gunther Piez

92

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

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


6
+1 Я згоден Ні, <ні <=швидкість, поки компілятор не вирішить, яку швидкість вони матимуть. Це дуже проста оптимізація для компіляторів, якщо врахувати, що вони, як правило, вже виконують оптимізацію мертвого коду, оптимізацію хвостових викликів, підйому циклу (і періодично розгортання), автоматичну паралелізацію різних циклів тощо. Навіщо витрачати час на обдумування передчасних оптимізацій ? Запровадьте прототип, профіліруйте його, щоб визначити, де лежать найбільш значні оптимізації, виконайте ці оптимізації в порядку значущості та профілю знову по шляху вимірювання прогресу ...
autistic

Є ще деякі крайні випадки, коли порівняння, що має одне постійне значення, може бути повільнішим при <=, наприклад, коли перетворення з (a < C)в (a <= C-1)(для деякої постійної C) Cстає складніше кодувати в наборі інструкцій. Наприклад, набір інструкцій може представляти підписані константи від -127 до 128 у компактній формі для порівнянь, але константи поза цим діапазоном повинні завантажуватися, використовуючи або довше, повільніше кодування, або іншу інструкцію цілком. Тож подібне порівняння (a < -127)може не мати прямого перетворення.
BeeOnRope

@BeeOnRope Це питання не було чи виконання операцій , які відрізнялися з - за наявності різних констант в них може вплинути на продуктивність , але чи висловити в ту ж операцію , використовуючи різні константи можуть вплинути на продуктивність. Таким чином , ми не порівнюючи a > 127з , a > 128тому що у вас немає вибору , там, ви використовуєте той , який вам потрібен. Ми по порівнянні a > 127з a >= 128, що не може вимагати від іншого кодування або різних інструкцій , тому що вони мають ту ж таблицю істинності. Будь-яке кодування одного є рівним кодуванням іншого.
Девід Шварц

Я взагалі відповідав на ваше твердження, що "Якщо була якась платформа, де [<= повільніше], компілятор завжди повинен перетворюватися <=на <константи". Наскільки мені відомо, ця трансформація передбачає зміну константи. Наприклад, a <= 42компілюється як , a < 43тому що <це швидше. У деяких крайніх випадках така трансформація не була б плідною, оскільки нова константа може зажадати більш чи повільних інструкцій. Звичайно , a > 127і a >= 128еквівалентні і компілятор повинен кодувати обидві форми в (ж) самий швидкий спосіб, але це не суперечить тому , що я сказав.
BeeOnRope

67

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

if(a < 901)
cmpl  $900, -4(%rbp)
jg .L2

if(a <=901)
cmpl  $901, -4(%rbp)
jg .L3

Мій приклад if- із GCC на платформі x86_64 в Linux.

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

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

int b;
if(a < b)
cmpl  -4(%rbp), %eax
jge   .L2

if(a <=b)
cmpl  -4(%rbp), %eax
jg .L3

9
Зауважте, що це характерно для x86.
Майкл Петротта

10
Я думаю, ви повинні використовувати це, if(a <=900)щоб продемонструвати, що він генерує абсолютно такий самий асм :)
Ліпіс,

2
@AdrianCornish Вибачте .. Я це відредагував .. це майже-менш те саме .. але якщо ви зміните другий, якщо на <= 900, то код ASM буде точно таким же :) Зараз він майже такий же .., але ви знайте .. для OCD :)
Ліпіс

3
@Boann Це може скоротитися до if (true) та повністю усунути.
Qsario

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

51

Для коду з плаваючою комою порівняння <= дійсно може бути повільнішим (за однією інструкцією) навіть у сучасних архітектурах. Ось перша функція:

int compare_strict(double a, double b) { return a < b; }

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

Тепер замість цієї функції розглянемо:

int compare_loose(double a, double b) { return a <= b; }

Для цього потрібна така ж робота, як і compare_strictвище, але зараз є два біти інтересу: "був менший за" і "був рівний". Для цього потрібна додаткова інструкція ( cror- реєструйте умовно порядок АБО), щоб об'єднати ці два біти в один. Тому compare_looseпотрібно п'ять інструкцій, тоді як compare_strictпотрібно чотири.

Ви можете подумати, що компілятор може оптимізувати другу функцію так:

int compare_loose(double a, double b) { return ! (a > b); }

Однак це буде неправильно обробляти NaN. NaN1 <= NaN2і NaN1 > NaN2потрібно обидва оцінювати до хибних.


На щастя, це не працює так на x86 (x87). fucomipнабори ZF і CF.
Джонатан Райнхарт

3
@JonathonReinhart: Я думаю , ви непорозуміння , що робить PowerPC - стан регістра cr є еквівалентом прапорів , як ZFі CFна x86. (Хоча CR є більш гнучким.) Те, про що йдеться у плакаті, - це переміщення результату до GPR: що приймає дві інструкції щодо PowerPC, але x86 має інструкцію з умовного переміщення.
Дітріх Епп

@DietrichEpp Що я мав намір додати після своєї заяви: Це ви можете негайно перейти на основі значення EFLAGS. Вибачте, що не зрозуміли.
Джонатан Райнхарт

1
@JonathonReinhart: Так, і ви також можете негайно стрибати, виходячи зі значення CR. Відповідь не говорить про стрибки, звідки беруться додаткові інструкції.
Дітріх Епп

34

Можливо, автор тієї неназваної книги прочитав, що a > 0працює швидше a >= 1і вважає, що це правда універсально.

Але це тому 0, що задіяний a (тому що CMPможе, залежно від архітектури, замінюватися, наприклад, на OR), а не через <.


1
Звичайно, у складі "налагодження", але це знадобиться поганому компілятору, (a >= 1)щоб запустити повільніше (a > 0), оскільки оптимізатор перший може бути тривіально перетворений на другий ..
BeeOnRope

2
@BeeOnRope Іноді я дивуюсь, які складні речі оптимізатор може оптимізувати та на які легкі речі він цього не робить.
glglgl

1
Дійсно, і завжди варто перевірити вихід ASM на дуже мало функцій, де це має значення. З огляду на це, перераховане вище перетворення є дуже базовим і проводиться навіть у простих укладачах протягом десятиліть.
BeeOnRope

32

Принаймні, якби це було правдою, компілятор міг би тривіально оптимізувати a <= b to! (A> b), і тому навіть якщо саме порівняння було насправді повільніше, з усіма, крім самого наївного компілятора, ви б не помітили різниці .


Чому! (A> b) - це оптимізована версія a <= b. Isnt! (A> b) 2 операції в одному?
Абхішек Сінгх

6
@AbhishekSingh NOTпросто зроблений за іншою інструкцією ( jevs. jne)
Павло Гатнар

15

Вони мають однакову швидкість. Можливо, в якійсь спеціальній архітектурі те, що він / вона сказав, є правильним, але в сім'ї x86 я принаймні знаю, що вони однакові. Оскільки для цього ЦП буде робити субстракцію (a - b), а потім перевірятиме прапори регістра прапора. Два біти цього реєстру називаються ZF (нульовий прапор) і SF (прапор знака), і це робиться за один цикл, тому що це буде робити за допомогою однієї операції маски.


14

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

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


1
ЯКЩО була різниця в кілах. 1) це не було б виявлено. 2) Будь-який компілятор, що вартує його солі, вже здійснював би перетворення з повільної форми в більш швидку форму, не змінюючи значення коду. Тож отримана інструкція посаджена була б ідентичною.
Мартін Йорк

Домовились повністю, це було б досить банальною і дурною різницею в будь-якому випадку. Безумовно, нічого не згадати в книзі, яка повинна бути платформою агностиком.
Телгін

@lttlrck: Я розумію. Взяв мене деякий час (нерозумно мене). Ні, їх не можна виявити, оскільки відбувається так багато інших речей, які роблять їх вимірювання неможливим. Проміжки процесорів / пропуски кешу / сигнали / обмін процесами. Таким чином, у нормальній ситуації з ОС речі на рівні одного циклу не можуть бути фізично вимірними. Якщо ви зможете усунути всі ці перешкоди з ваших вимірювань (запустіть їх на мікросхемі з вбудованою пам'яттю та без ОС), тоді ви все ще маєте деталізацію таймерів, про які слід турбуватися, але теоретично, якщо ви будете працювати досить довго, ви могли щось побачити.
Мартін Йорк

12

TL; DR відповідь

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

Повна відповідь

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

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

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

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

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


6

Ви не зможете помітити різницю, навіть якщо вона є. Крім того, на практиці вам доведеться зробити додатковий a + 1або a - 1зробити умову витриманою, якщо ви не збираєтесь використовувати якісь магічні константи, що є дуже поганою практикою.


1
Яка погана практика? Збільшення чи декрементація лічильника? Як тоді зберігати позначення індексів?
jcolebrand

5
Він має на увазі, якщо ви робите порівняння двох змінних типів. Звичайно, це банально, якщо ви встановлюєте значення для циклу чи чогось іншого. Але якщо у вас x <= y, а y невідомо, буде повільніше "оптимізувати" його до x <y + 1
JustinDanielson

@JustinDanielson погодився. Не кажучи вже про потворне, заплутане тощо
Джонатан Райнхарт

4

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


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

3

Коли я писав цю відповідь, я дивився тільки на титульний питання про <проти <= в цілому, а не конкретний приклад постійного a < 901VS. a <= 900. Багато укладачі завжди зменшити величину констант шляху перетворення між <і <=, наприклад , з - x86 безпосереднього операнда має більш короткий кодування 1-байтовое для -128..127.

Для ARM, і особливо для AArch64, можливість кодування як негайної залежить від того, чи зможе повернути вузьке поле в будь-яку позицію слова. Так cmp w0, #0x00f000би було кодовано, а cmp w0, #0x00effffможе і не бути. Таким чином, правило make-it-small для порівняння та константа часу компіляції не завжди застосовується для AArch64.


<vs. <= загалом, у тому числі для змінних умов виконання

У мові складання на більшості машин порівняння <=має таку саму вартість, як і порівняння <. Це стосується того, чи розгалужуєте ви його, булінізуєте його для створення цілого числа 0/1 або використовуєте його як предикат для операції вибору без гілок (наприклад, CM86 CMOV). Інші відповіді стосувались лише цієї частини питання.

Але це питання стосується операторів C ++, вхід до оптимізатора. Зазвичай вони обидва однаково ефективні; поради з книги звучать абсолютно хибно, тому що компілятори завжди можуть перетворити порівняння, яке вони реалізують, в asm. Але є принаймні один виняток, коли використання <=може випадково створити щось, що компілятор не може оптимізувати.

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

Непідписаний перелив добре визначається як базове-2 обгортання, на відміну від підписаного переповнення (UB). Підписані лічильники циклів, як правило, безпечні від цього за допомогою компіляторів, які оптимізуються на основі UB-переповнення підпису, що не відбувається: ++i <= sizeзавжди з часом стане помилковим. ( Що повинен знати кожен програміст C про не визначену поведінку )

void foo(unsigned size) {
    unsigned upper_bound = size - 1;  // or any calculation that could produce UINT_MAX
    for(unsigned i=0 ; i <= upper_bound ; i++)
        ...

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

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

У цьому випадку size=0веде до upper_bound=UINT_MAXі i <= UINT_MAXзавжди є правдою. Отже, цей цикл нескінченний size=0, і компілятор повинен поважати це, хоча ви, як програміст, ймовірно, ніколи не збираєтесь передавати розмір = 0. Якщо компілятор може вбудувати цю функцію в абонент, де він може довести, що розмір = 0 неможливий, то чудово, він може оптимізувати, як міг би i < size.

Asm like if(!size) skip the loop; do{...}while(--size);- це один нормально ефективний спосіб оптимізації for( i<size )циклу, якщо фактичне значення iне потрібне всередині циклу ( чому петлі завжди компілюються у стиль "do ... while" (стрибок хвоста)? ).

Але це робити {}, але не може бути нескінченним: якщо його ввести size==0, ми отримаємо 2 ^ n ітерацій. ( Ітерація над усіма непідписаними цілими числами в циклі for для циклу C дає змогу виразити цикл для всіх непідписаних цілих чисел, включаючи нуль, але це непросто без прапора перенесення так, як це знаходиться в ASM.)

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

Приклад: сума цілих чисел від 1 до n

Використовуючи непідписані i <= nпоразки розпізнавання ідіоми Кланг, що оптимізує sum(1 .. n)петлі із закритою формою на основі n * (n+1) / 2формули Гаусса .

unsigned sum_1_to_n_finite(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i < n+1 ; ++i)
        total += i;
    return total;
}

x86-64 asm від clang7.0 та gcc8.2 на досліднику компілятора Godbolt

 # clang7.0 -O3 closed-form
    cmp     edi, -1       # n passed in EDI: x86-64 System V calling convention
    je      .LBB1_1       # if (n == UINT_MAX) return 0;  // C++ loop runs 0 times
          # else fall through into the closed-form calc
    mov     ecx, edi         # zero-extend n into RCX
    lea     eax, [rdi - 1]   # n-1
    imul    rax, rcx         # n * (n-1)             # 64-bit
    shr     rax              # n * (n-1) / 2
    add     eax, edi         # n + (stuff / 2) = n * (n+1) / 2   # truncated to 32-bit
    ret          # computed without possible overflow of the product before right shifting
.LBB1_1:
    xor     eax, eax
    ret

Але для наївної версії ми просто отримуємо тупу петлю з ляска.

unsigned sum_1_to_n_naive(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i<=n ; ++i)
        total += i;
    return total;
}
# clang7.0 -O3
sum_1_to_n(unsigned int):
    xor     ecx, ecx           # i = 0
    xor     eax, eax           # retval = 0
.LBB0_1:                       # do {
    add     eax, ecx             # retval += i
    add     ecx, 1               # ++1
    cmp     ecx, edi
    jbe     .LBB0_1            # } while( i<n );
    ret

GCC в жодному разі не використовує закриту форму, тому вибір умови циклу насправді не шкодить цьому ; він автоматично векторизується з додаванням цілого числа SIMD, виконуючи параметри 4 iзначень паралельно в елементах регістра XMM.

# "naive" inner loop
.L3:
    add     eax, 1       # do {
    paddd   xmm0, xmm1    # vect_total_4.6, vect_vec_iv_.5
    paddd   xmm1, xmm2    # vect_vec_iv_.5, tmp114
    cmp     edx, eax      # bnd.1, ivtmp.14     # bound and induction-variable tmp, I think.
    ja      .L3 #,       # }while( n > i )

 "finite" inner loop
  # before the loop:
  # xmm0 = 0 = totals
  # xmm1 = {0,1,2,3} = i
  # xmm2 = set1_epi32(4)
 .L13:                # do {
    add     eax, 1       # i++
    paddd   xmm0, xmm1    # total[0..3] += i[0..3]
    paddd   xmm1, xmm2    # i[0..3] += 4
    cmp     eax, edx
    jne     .L13      # }while( i != upper_limit );

     then horizontal sum xmm0
     and peeled cleanup for the last n%3 iterations, or something.

Він також має просту скалярну петлю, яку, на мою думку, вона використовує для дуже малого nта / або для випадку нескінченного циклу.

До речі, обидві ці петлі витрачають інструкцію (і взагалі про процесори сімейства Sandybridge) про петлі над головою. sub eax,1/ jnzзамість add eax,1/ cmp / jcc було б більш ефективним. 1 взагалі замість 2 (після макро-злиття sub / jcc або cmp / jcc). Код після обох циклів записує EAX беззастережно, тому він не використовує остаточне значення лічильника циклу.


Приємний надуманий приклад. А як щодо Вашого іншого коментаря щодо потенційного впливу на виконання замовлень через використання EFLAGS? Це чисто теоретично чи може насправді статися, що JB призводить до кращого конвеєра, ніж JBE?
іржа

@rustyx: я коментував це десь під іншою відповіддю? Компілятори не збираються видавати код, який спричиняє часткові прапорці, і, звичайно, не для C <або <=. Але впевнено, test ecx,ecx/ bt eax, 3/ jbeстрибне, якщо встановлено ZF (ecx == 0), або якщо встановлено CF (біт 3 EAX == 1), що спричинить часткову зупинку прапора на більшості процесорів, оскільки прапорці, які він читає, не всі виходять з останньої інструкції писати будь-які прапори. У сімействі Сендібрідж вона насправді не затримується, просто потрібно вставити об'єднувальну форму. cmp/ testзапишіть усі прапори, але btзалишає ZF без змін. felixcloutier.com/x86/bt
Пітер Кордес

2

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

Кожне порівняння ( >= <= > <) може бути виконане з однаковою швидкістю.

Кожне порівняння - це лише віднімання (різниця) і бачення, чи воно є позитивним / негативним.
(Якщо значення msbвстановлено, число від’ємне)

Як перевірити a >= b? Sub a-b >= 0Перевірте, чи a-bє позитивним.
Як перевірити a <= b? Sub 0 <= b-aПеревірте, чи b-aє позитивним.
Як перевірити a < b? Sub a-b < 0Перевірте, чи a-bнегативно.
Як перевірити a > b? Sub 0 > b-aПеревірте, чи b-aнегативно.

Простіше кажучи, комп'ютер може просто зробити це під кришкою для даної опції:

a >= b== msb(a-b)==0
a <= b== msb(b-a)==0
a > b== msb(b-a)==1
a < b==msb(a-b)==1

і звичайно комп’ютеру насправді не потрібно було б робити ==0або те, або ==1інше.
для нього ==0це може просто перевернути msbз ланцюга.

У будь-якому випадку, вони, звичайно, не були a >= bб обчислені як a>b || a==blol

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