В одном из моих докладов по ассемблеру я показал список из 20 самых часто исполняемых команд на среднем десктопе x86 с Linux. Разумеется, в этом списке были привычные mov, add, lea, sub, jmp, call и так далее; неожиданным стало включение в него xor — «eXclusive OR». В эпоху, когда я занимался хакингом на 6502, наличие XOR было почти абсолютно точным указанием на то, что найдена часть кода, связанная с шифрованием, или какая-то подпрограмма обработки спрайтов. Поэтому удивительно, что машина с Linux, просто занимающаяся своими делами, выполняет такое количество этих команд.
Но потом мы вспоминаем о том, что компиляторы любят генерировать xor при присвоении регистру нулевого значения:
int main() {
return 0;
}

Мы знаем, что XOR любого значения с самим собой даёт ноль, но почему компилятор генерирует такую последовательность?
В показанном ниже примере я выполнял компиляцию с -O2 и включил опцию Compiler Explorer «Compile to binary object», чтобы можно было увидеть машинный код, который видит CPU, и в частности:
31 c0 xor eax, eax
c3 ret
Если снизить уровень оптимизации GCC до -O1, то мы увидим следующее:
b8 00 00 00 00 mov eax, 0x0
c3 ret
Гораздо более понятная и раскрывающая своё предназначение команда mov eax, 0, записывающая в регистр EAX ноль, занимает пять байт, в то время как версия с XOR занимает всего два. Благодаря использованию чуть менее понятной команды мы экономим три байта каждый раз, когда нужно присвоить регистру нулевое значение, что происходит довольно часто. Экономия байтов уменьшает размер программы и повышает эффективность использования кэша команд.
Но и это ещё не всё! Так как это очень частая операция, CPU x86 замеч��ют эту «идиому обнуления» на ранних этапах конвейера и могут оптимизироваться конкретно под неё: системы отслеживания исполнения с изменением очерёдности знают, что значение «eax» (или какого-то ещё обнуляемого регистра) не зависит от предыдущего значения eax, поэтому они могут распределить свежий, не имеющий зависимостей слот переименования нулевого регистра. И сделав это, они удаляют операцию из очереди исполнения, то есть xor занимает ноль тактов исполнения! [Однако ей всё равно нужно завершиться, поэтому некоторые ресурсы процессора всё равно распределяются для её учёта.] По сути, CPU благодаря оптимизации устраняет её!
Вы можете задаться вопросом, почему мы встречаем xor eax, eax, но никогда не видим xor rax, rax (его 64-битную версию) даже при возврате long:
long get_zero_long() {
return 0;
}

В этом случае, даже несмотря на то, что rax необходим для хранения полного 64-битного результата long, выполняя запись в eax, мы получаем удобный эффект: в отличие от других частичных записей в регистр, при записи в e-реги��тры наподобие eax архитектура без лишних затрат обнуляет старшие 32 бита. Поэтому xor eax, eax обнуляет все 64 бита.
Любопытно, что при обнулении «расширенных» нумерованных регистров наподобие (like r8) GCC использует вариант d (двойной ширины, то есть 32-битный):
extern void needs_many_longs(
long rdi, long rsi, long rdx,
long rcx, long r8, long r9);
void test() {
needs_many_longs(0, 0, 0, 0, 0, 0);
}

Обратите внимание, что используется xor r8d, r8d (32-битный вариант), хотя с префиксом REX (здесь 45) потребовалось бы то же количество байт для xor r8, r8 полной ширины. Возможно, это упрощает работу компиляторов, потому что clang поступает так же.
xor eax, eax снижает объём кода и время исполнения! Спасибо вам, компиляторы!