Сборка 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замечание 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инструкцию register-register в начало или конец функции. Это практически не влияет на скорость, но добавляет 2 байта. ( RETИнструкция также добавляет байт, и есть некоторые накладные расходы, связанные с необходимостью выполнения и возврата из вызова функции, что означает, что это один из примеров, когда встраивание дает выигрыш как в скорости, так и в размере, а не просто в классической скорости -for-space trade.) Во всем, написанном как функция, этот код увеличивается до 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действие на INCrement:
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