x86 Асамблея, 9 байт (для змагального вступу)
Кожен, хто намагається вирішити цю проблему на мовах високого рівня, не вистачає справжнього задоволення від маніпулювання сирими бітами. Існує так багато тонких варіацій способів зробити це, божевільно - і дуже весело думати. Ось кілька рішень, які я розробив 32-бітною мовою збірки x86.
Я заздалегідь вибачаюся, що це не типова відповідь на гольф-код. Я збираюся багато роздумувати про мислений процес ітеративної оптимізації (для розміру). Сподіваємось, це цікаво та навчально для більшої аудиторії, але якщо ти тип TL; DR, я не ображаюся, якщо пропустиш до кінця.
Очевидним та ефективним рішенням є перевірка того, чи є значення непарним чи парним (що можна зробити ефективно, переглянувши найменш значимий біт), а потім вибрати відповідно n + 1 або n − 1 відповідно. Припускаючи, що вхід передається як параметр в ECX
регістр, а результат повертається в EAX
регістр, ми отримуємо таку функцію:
F6 C1 01 | test cl, 1 ; test last bit to see if odd or even
8D 41 01 | lea eax, DWORD PTR [ecx + 1] ; set EAX to n+1 (without clobbering flags)
8D 49 FF | lea ecx, DWORD PTR [ecx - 1] ; set ECX to n-1 (without clobbering flags)
0F 44 C1 | cmovz eax, ecx ; move in different result if input was even
C3 | ret
(13 байт)
Але для цілей коду-гольфу ці LEA
інструкції не великі, оскільки для кодування вони займають 3 байти. Простий DEC
rement з ECX
буде набагато коротше (тільки один байт), але це впливає на прапори, тому ми повинні бути трохи розумний в тому , як ми організуємо код. Ми можемо зробити декремент першим , а непарний / парний тест другим , але потім нам доведеться перевернути результат непарного / парного тесту.
Крім того, ми можемо змінити інструкцію умовного переміщення на гілку, що може змусити код працювати повільніше (залежно від того, наскільки передбачувана гілка - якщо введення чергуватиметься непослідовно між непарною та парною, гілка буде повільнішою; якщо є шаблон, це буде швидше), що врятує нам ще один байт.
Фактично, за допомогою цієї редакції всю операцію можна зробити на місці, використовуючи лише один регістр. Це чудово, якщо ви десь інлігіруєте цей код (і, швидше за все, ви є, оскільки він такий короткий).
48 | dec eax ; decrement first
A8 01 | test al, 1 ; test last bit to see if odd or even
75 02 | jnz InputWasEven ; (decrement means test result is inverted)
40 | inc eax ; undo the decrement...
40 | inc eax ; ...and add 1
InputWasEven: ; (two 1-byte INCs are shorter than one 3-byte ADD with 2)
(впорядковано: 7 байт; як функція: 10 байт)
Але що робити, якщо ви хочете зробити це функцією? Жоден стандартний режим виклику не використовує той самий реєстр для передачі параметрів, як це відбувається для значення, що повертається, тому вам потрібно буде додати MOV
інструкцію реєстрації регістру до початку або в кінці функції. Це майже не має витрат на швидкість, але це додає 2 байти. ( RET
Інструкція також додає байт, і є деякий наклад, який вводиться необхідністю здійснювати і повертатись із виклику функції. Це означає, що це один із прикладів, коли вбудована лінія створює як швидкість, так і розмір, а не просто класичну швидкість -для космосу для простору.) Загалом, написаний як функція, цей код розширюється до 10 байт.
Що ще ми можемо зробити в 10 байт? Якщо ми взагалі піклуємося про ефективність (принаймні, передбачувану ефективність), було б непогано позбутися цієї гілки. Ось безгалузеве роздвоєне рішення, що має однаковий розмір у байтах. Основна передумова проста: ми використовуємо побітовий XOR, щоб перевернути останній біт, перетворивши непарне значення в парне, і навпаки. Але є одна ніггеля - для непарних входів, яка дає нам n-1 , а для парних входів - n + 1 - прямо протилежне тому, що ми хочемо. Отже, щоб виправити це, ми виконуємо операцію за від’ємним значенням, ефективно перевертаючи знак.
8B C1 | mov eax, ecx ; copy parameter (ECX) to return register (EAX)
|
F7 D8 | neg eax ; two's-complement negation
83 F0 01 | xor eax, 1 ; XOR last bit to invert odd/even
F7 D8 | neg eax ; two's-complement negation
|
C3 | ret ; return from function
(впорядковано: 7 байт; як функція: 10 байт)
Досить стрункі; важко зрозуміти, як це можна покращити. Одне, однак, привертає до мене увагу: ці дві 2-байтні NEG
інструкції. Відверто кажучи, два байти здаються одним байтом занадто багато, щоб кодувати просту заперечення, але це набір інструкцій, з яким ми повинні працювати. Чи є якісь обхідні шляхи? Звичайно! Якщо ми XOR
на -2, можемо замінити другу дію NEG
на INC
відступ:
8B C1 | mov eax, ecx
|
F7 D8 | neg eax
83 F0 FE | xor eax, -2
40 | inc eax
|
C3 | ret
(впорядковано: 6 байт; як функція: 9 байт)
Ще одна з дивацтв набору інструкцій x86 - це багатоцільова LEA
інструкція , яка може зробити переміщення регістру-реєстру, додавання реєстру-реєстру, компенсування постійною та масштабування всіх в одній інструкції!
8B C1 | mov eax, ecx
83 E0 01 | and eax, 1 ; set EAX to 1 if even, or 0 if odd
8D 44 41 FF | lea eax, DWORD PTR [ecx + eax*2 - 1]
C3 | ret
(10 байт)
AND
Інструкція як TEST
інструкції ми використовували раніше, в тому , як зробити побітову І і встановити прапори відповідно, але на AND
самому справі оновлює призначення операнда. Потім LEA
інструкція масштабує це на 2, додає початкове вхідне значення та зменшення на 1. Якщо вхідне значення було непарним, це віднімає 1 (2 × 0 - 1 = −1) від нього; якщо вхідне значення було парним, до цього додається 1 (2 × 1 - 1 = 1).
Це дуже швидкий і ефективний спосіб написання коду, оскільки велика частина виконання може бути виконана в передній частині, але це не набагато купує нас у байтах, оскільки для кодування комплексу потрібно стільки LEA
інструкція. Ця версія також не працює також для вкладених цілей, оскільки вимагає збереження вихідного значення введення як введення LEA
інструкції. Тож з цією останньою спробою оптимізації ми насправді пішли назад, припустивши, що можливо час зупинитися.
Таким чином, для остаточного конкуруючого запису ми маємо 9-байтну функцію, яка приймає вхідне значення в ECX
регістр (напівстандартне конвенційне виклик на основі регістра на 32-бітному x86) і повертає результат у EAX
регістр (як у випадку всі умови виклику x86):
SwapParity PROC
8B C1 mov eax, ecx
F7 D8 neg eax
83 F0 FE xor eax, -2
40 inc eax
C3 ret
SwapParity ENDP
Готовий до збирання з MASM; дзвінок з C як:
extern int __fastcall SwapParity(int value); // MSVC
extern int __attribute__((fastcall)) SwapParity(int value); // GNU