Преимущества использования 32-битных регистров / инструкций в x86-64

Иногда gcc использует 32-битный регистр, когда я ожидал, что он будет использовать 64-битный регистр. Например, следующий код C:

unsigned long long 
div(unsigned long long a, unsigned long long b){
    return a/b;
}

компилируется с опцией -O2 для (не включая некоторые элементы):

div:
    movq    %rdi, %rax
    xorl    %edx, %edx
    divq    %rsi
    ret

Для беззнакового деления регистр %rdx должно быть 0, Это может быть достигнуто с помощью xorq %rdx, %rdx, но xorl %edx, %edx кажется, имеет тот же эффект.

По крайней мере, на моей машине не было никакого увеличения производительности (т.е. ускорения) для xorl над xorq,

У меня на самом деле больше, чем один вопрос:

  1. Почему gcc предпочитает 32-битную версию?
  2. Почему gcc останавливается на xorl и не использует xorw?
  3. Есть ли машины для которых xorl быстрее чем xorq?
  4. Следует ли всегда отдавать предпочтение 32-битному регистру / операциям, если это возможно, а не 64-битному регистру / операциям?

2 ответа

Решение

Почему gcc предпочитает 32-битную версию?

Размер кода: префикс REX не требуется.

Почему gcc останавливается на xorl и не использует xorw?

Запись 16-битного частичного регистра не распространяется на ноль до остальных регистров. Кроме того, xorw для кодирования требуется префикс размера операнда, поэтому он больше xorl, (См. Также Почему инструкции x86-64 для 32-битных регистров обнуляют верхнюю часть полного 64-битного регистра? Для исторического фона)

Смотрите также Почему GCC не использует частичные регистры? 32-битные регистры не считаются частичными регистрами, потому что их запись всегда записывает весь 64-битный регистр. (И основная проблема заключается в записи частичных регистров, а не в чтении их после записи во всю ширину.)

Существуют ли машины, для которых xorl быстрее, чем xorq?

Да, Silvermont / KNL распознают только xor обнуление как идиома обнуления (разрыв зависимости и другие хорошие вещи) с размером 32-битного операнда. Таким образом, хотя размер кода одинаков, xor %r10d, %r10d намного лучше чем xor %r10, %r10, (xor нужен префикс REX для r10 независимо от размера операнда).

На всех процессорах размер кода всегда имеет значение для декодирования и использования I-кэша (кроме случаев, когда .p2align директива просто сделает больше заполнения, если предыдущий код меньше 1). Нет недостатка в использовании 32-битного размера операнда для обнуления xor (или для неявного расширения нуля вообще вместо explict 2, включая использование AVX vpxor xmm0,xmm0,xmm0 к нулю AVX512 zmm0.)

Большинство команд имеют одинаковую скорость для всех размеров операндов, потому что современные процессоры x86 могут позволить себе транзисторный бюджет для широких ALU. Исключения включают imul r64,r64 медленнее, чем imul r32,r32 на процессорах AMD до Ryzen, и Intel Atom, и 64bit div значительно медленнее на всех процессорах. AMD пре-рыжен медленнее popcnt r64, Атом / Сильвермонт имеют медленный shld/shrd r64 против r32


Следует ли всегда отдавать предпочтение 32-битному регистру / операциям, если это возможно, а не 64-битному регистру / операциям?

Да, предпочитайте 32-битные операции по крайней мере из соображений размера кода, но учтите, что использование r8..r15 в любом месте инструкции (включая режим адресации) также потребует префикса REX. Поэтому, если у вас есть некоторые данные, с которыми вы можете использовать 32-битный размер операнда (или указатели на 8/16/32-битные данные), предпочитайте хранить их в младших 8 именованных регистрах (e/rax..), а не в высоких 8 пронумерованных регистров.

Но не тратьте лишние инструкции, чтобы это произошло; сохранение нескольких байтов размера кода обычно является наименее важным фактором. например, просто использовать r8d вместо сохранения / восстановления rbx так что вы можете использовать ebx если вам нужен дополнительный регистр, который не должен быть сохранен при вызове. Использование 32-битного r8d вместо 64-битного r8 не поможет с размером кода, но это может быть быстрее для некоторых операций на некоторых процессорах (см. выше).

Это также относится к случаям, когда вы заботитесь только о младших 16 битах регистра, но все же может быть более эффективно использовать 32-битное добавление вместо 32-битного.

Смотрите также http://agner.org/optimize/ и вики-тег x86.


Сноска 1: Есть редкие сценарии использования для создания инструкций длиннее, чем необходимо ( Какие методы могут быть использованы для эффективного увеличения длины инструкций в современной x86?)

  • Для выравнивания более поздней цели ветвления без необходимости NOP.

  • Настройка внешнего интерфейса конкретной микроархитектуры (т. Е. Оптимизация декодирования путем контроля границ команд). Вставка NOP будет стоить дополнительной полосы пропускания внешнего интерфейса и полностью разрушит всю цель.

Ассемблеры не сделают этого за вас, и выполнение этого вручную требует много времени для повторного выполнения каждый раз, когда вы что-либо меняете (и вам, возможно, придется использовать .byte директивы для ручного кодирования инструкции).

Сноска 2: Я обнаружил одно исключение из правила, согласно которому неявное расширение нуля, по меньшей мере, столь же дешево, как и более широкая операция: 128-битные загрузки Haswell / Skylake AVX, читаемые 256-битной инструкцией, имеют дополнительный 1c хранилища. задержка пересылки против использования 128-битной инструкции. (Подробности в теме на форуме блога Агнера Фога.)

В 64-битном режиме запись в 32-битный регистр обнуляет верхние 32 бита =>xorl %edx, %edx нули верхней части rdx бесплатно".

С другой стороны xor %rdx, %rdx кодируется дополнительным байтом, потому что для этого требуется префикс REX. При попытке обнулить 64-битный регистр, это чистый выигрыш, чтобы записать его как 32-битный регистр.

Другие вопросы по тегам