Концепція умовних конструкцій знайомить із jcc, родиною інструкцій для розгалуження за умовою.
jcc передає виконання в іншу точку програми лише тоді, коли його умова виконується.
Однак сучасні процесори швидкі й здатні виконувати багато інструкцій паралельно. Було б надто дорого зупиняти всю роботу, поки процесор чекає на перевірку умови під час виконання.
Саме тому всередині процесор не чекає. Він використовує передбачувач розгалужень, щоб угадати результат умови, і починає спекулятивно виконувати передбачений шлях.
Якщо передбачення правильне, виконання продовжується так, ніби розгалуження не було. Однак якщо передбачення хибне, процесор мусить відкинути виконану роботу на передбаченому шляху й почати заново на правильному.
Це називається помилкою передбачення розгалуження, і вона дорого коштує, спричиняючи затримку.
Коли розгалуження важко передбачити, такі помилки накопичуються.
У таких випадках іноді краще взагалі уникнути розгалуження, щоб ті самі інструкції виконувалися незалежно від умови.
Код, який вибирає між значеннями без jcc, називають безрозгалуженим.
Є кілька способів структурувати код так, щоб він розгалужувався рідше, або щоб наявні розгалуження були передбачуванішими. Зокрема, x86-64 надає дві родини інструкцій, які часто використовують для написання коду без розгалужень.
Родина інструкцій cmovcc копіює операнд джерела в операнд призначення лише тоді, коли виконується певна умова.
Інакше операнд призначення залишається без змін.
Суфікс cc має ті самі назви, що й у jcc, з тим самим значенням:
| інструкція | переміщує, якщо |
|---|---|
cmove |
A == B після cmp A, B
|
cmovne |
A != B після cmp A, B
|
cmovl |
A < B (зі знаком) після cmp A, B
|
cmovb |
A < B (без знака) після cmp A, B
|
cmovg |
A > B (зі знаком) після cmp A, B
|
cmova |
A > B (без знака) після cmp A, B
|
Правила схожі на jcc:
l і g використовують для порівнянь зі знаком, а b і a - для порівнянь без знака.e до суфікса додає рівність.n заперечує умову.cmovz і cmovc.Розгляньмо обчислення абсолютного значення цілого числа зі знаком у rdi, яке повертається в rax.
Записана через jcc, функція вибирає один із двох шляхів:
abs_branch:
mov rax, rdi
cmp rax, 0
jge .done
neg rax
.done:
ret
Записана через cmovcc, функція обчислює обидва можливі значення безумовно, а умовне переміщення вибирає одне з них:
abs_branchless:
mov rax, rdi
neg rax ; rax = -rdi
cmp rdi, 0
cmovge rax, rdi ; the value in `rdi` is moved to `rax` if rdi >= 0
; otherwise, it stays the same, i.e., -rdi
; now rax = abs(rdi)
ret
Версія без розгалужень завжди виконує ті самі інструкції.
Тут немає jcc, яке міг би вгадати передбачувач.
Операнд призначення cmovcc має бути регістром.
Операнд джерела може бути регістром або коміркою памʼяті, але не безпосереднім значенням.
У наведеному вище коді інструкція neg встановлює кілька прапорців залежно від результату, зокрема прапорець знака (SF).
Це означає, що cmp rdi, 0 можна взагалі прибрати:
abs_branchless:
mov rax, rdi
neg rax ; rax = -rdi. `neg` sets SF = 1 if the result is negative
cmovs rax, rdi ; if SF == 1 (i.e., rax < 0), replace rax with rdi
; otherwise rax stays = -rdi, which is non-negative
; now rax = abs(rdi)
ret
Для додатного rdi інструкція neg дає відʼємний результат і SF == 1, тож cmovs відновлює rdi.
Для нуля або відʼємного rdi інструкція neg дає невідʼємний результат і SF == 0, тож rax зберігає заперечене значення (яке і є правильним абсолютним значенням).
Хоча cmp - основний спосіб порівнювати значення в асемблерному коді, багато інструкцій також встановлюють прапорці залежно від свого результату.
Повний список прапорців, на які впливає інструкція, зазвичай наведено в її довідці.
Для neg зокрема це SF, ZF, CF, OF і PF.
Родина інструкцій setcc встановлює операнд призначення в 1, якщо виконується певна умова, і в 0 в іншому разі.
Операнд призначення завжди є 8-бітним операндом.
Суфікс cc має ті самі назви, що й у jcc та cmovcc.
Наприклад, setz встановлює операнд призначення в 1, якщо ZF == 1, і в 0 в іншому разі.
Оскільки операнд призначення 8-бітний, зазвичай після setcc ставлять movzx, коли потрібне ширше значення:
cmp rdi, rsi
setg al ; al = 1 if rdi > rsi (signed), 0 otherwise
movzx eax, al ; eax (and rax) = 1 or 0, with the upper bits cleared
Це поширений прийом, щоб перетворити результат порівняння на ціле число 0 або 1.
Ми пишемо прошивку для системи підрахунку очок у ретро кишеньковій консолі. Процесор машини доволі скромний, а табло з результатами оновлюється з фіксованою частотою. Щоб зображення залишалося плавним, рутини підрахунку очок мають виконуватися за передбачувану кількість тактів, незалежно від того, що робить гравець.
На нас чекають чотири завдання.
Код для завдань має бути без розгалужень.
Використовуйте cmovcc, setcc та арифметику замість умовних переходів.
Ділянка результатів на РК-дисплеї консолі вміщує шість десяткових цифр.
Вона не може показати жодного числа, більшого за 999999.
Коли бонус штовхає поточний підсумок за цю межу, показаний підсумок утримується на максимумі, а не переходить через нуль.
Визначте функцію add_bonus, яка, отримавши поточний загальний результат і бонус для додавання, повертає новий підсумок, обмежений значенням 999999.
Аргументи в такому порядку:
total: поточний загальний результат (завжди від 0 до 999999)bonus: бонус для додавання (завжди невідʼємний)Повертається total + bonus, якщо ця сума менша або дорівнює 999999, і 999999 в іншому разі:
add_bonus(500, 100);
// => 600
add_bonus(999990, 50);
// => 999999
add_bonus(999999, 0);
// => 999999
І аргументи, і повернене значення - 64-бітні знакові цілі числа.
Можна припустити, що total + bonus не переповнює 64-бітне знакове ціле число.
Консоль зберігає невелику таблицю лідерів із найкращими недавніми результатами гравця у своєму файлі збереження. Після кожної гри новий результат вставляється в таблицю лідерів на потрібну позицію. Рутина сортування запитує, котрий із двох результатів випереджає, і приймає відповідь у вигляді невеликого цілого числа.
Визначте функцію compare_scores, яка, отримавши два результати, повертає:
+1, якщо перший результат вищий
-1, якщо перший результат нижчий
0, якщо обидва результати рівні
Приклад:
compare_scores(500, 300);
// => 1
compare_scores(300, 500);
// => -1
compare_scores(500, 500);
// => 0
І аргументи, і повернене значення - 64-бітні знакові цілі числа.
Консоль може зʼєднатися з іншим пристроєм через кабель звʼязку, щоб обмінятися результатами після матчу на кількох гравців. Кабель електрично шумний, і вхідні байти час від часу псуються, даючи значення далеко нижчі за допустимий мінімум або далеко вищі за допустимий максимум. Валідатор обмежує кожен вхідний результат допустимим діапазоном, перш ніж його побачить решта системи.
Визначте функцію validate_score, яка, отримавши необроблений результат і допустимі межі, повертає результат, обмежений діапазоном [min, max].
Аргументи в такому порядку:
score: необроблений результат, отриманий кабелем звʼязкуmin: найменший дозволений результатmax: найбільший дозволений результатПовертається min, якщо score < min, max, якщо score > max, і score в іншому разі.
Можна припустити, що min <= max.
Приклад:
validate_score(450, 0, 500);
// => 450
validate_score(-50, 0, 1530);
// => 0
validate_score(1234567, 0, 2999);
// => 2999
Усі аргументи й повернене значення - 64-бітні знакові цілі числа.
Наприкінці кожного ігрового сеансу консоль сканує журнал недавніх ігор у своєму файлі збереження, щоб знайти два найвищі результати за весь час. Журнал - це звичайний масив 64-бітних знакових цілих чисел у памʼяті. Сканування проходить масивом один раз і тримає два поточні максимуми: найвищий, побачений досі, і другий за висотою.
Звернімо увагу, що обидва числа мають бути не меншими за 0.
Будь-яке відʼємне число ігнорується.
Першу версію функції було написано з каскадом умовних переходів. Для кожного кандидата код зʼясовує, куди він потрапляє (вище першого, між першим і другим або нікуди), і відповідно зсуває поточні максимуми, відкидаючи будь-який відʼємний результат.
Це працює. Однак, оскільки вхідні значення випадкові, кожен умовний перехід спричиняє багато помилок передбачення переходів. Саме тому ми вирішили переробити головний цикл так, щоб він був без розгалужень.
Нижче наведено функцію, у якій секцію з розгалуженнями закоментовано:
; rdi = output buffer for two elements, rsi = input array address, rdx = number of elements in array
top_two:
xor r8d, r8d ; first = 0
xor r9d, r9d ; second = 0
xor ecx, ecx ; index = 0
test rdx, rdx
jz .done
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; BRANCHY CODE TO REFACTOR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;.loop:
; mov rax, qword [rsi + 8*rcx] ; candidate
; inc rcx
; cmp rax, r8
; jle .check_second ; candidate <= first, try second
; mov r9, r8 ; second = first
; mov r8, rax ; first = candidate
; cmp rcx, rdx
; jb .loop ; go to next iteration
; jmp .done ; otherwise, we are done
;.check_second:
; cmp rax, r9
; jle .loop ; candidate <= second, go to next iteration
; mov r9, rax ; second = candidate
; cmp rcx, rdx
; jb .loop ; go to next iteration
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.done:
mov qword [rdi], r8 ; save first
mov qword [rdi + 8], r9 ; save second
ret
Замініть закоментовану секцію реалізацією без розгалужень. Єдиний перехід, дозволений у новому коді, - це той, що повертається на початок циклу, щоб перевірити новий елемент масиву.
Функція не має поверненого значення та приймає ті самі аргументи, що й версія з розгалуженнями:
out: буфер вихідних даних, куди функція запише два найвищі невідʼємні результати в порядку спадання, як 64-бітні знакові цілі числаarray: вхідний масив 64-бітних знакових цілих чиселlength: кількість елементів у масиві, як 64-бітне беззнакове ціле числоЯкщо масив містить однакові значення, то дублікати можуть потрапити в обидві комірки вихідних даних, якщо вони є двома найвищими результатами.
Зареєструйтеся на Exercism, щоб вивчати й опановувати x86-64 Assembly, а також 22 концепції130 вправ та справжнє наставництво від людей, і все це безкоштовно.