Почему GCC не использует частичные регистры?

Демонтаж write(1,"hi",3) на linux, построенный с помощью gcc -s -nostdlib -nostartfiles -O3 приводит к:

 ba03000000 mov edx, 3 ; thanks for the correction jester! bf01000000 mov edi, 1 31c0 xor eax, eax e9d8ffffff jmp loc.imp.write 

Я не в разработке компилятора, но поскольку каждое значение, перемещенное в эти регистры, является постоянным и известным временем компиляции, мне любопытно, почему gcc не использует dl , dil и al . Некоторые могут утверждать, что эта функция не будет иметь никакого значения в производительности, но существует большая разница в размере исполняемого файла между mov $1, %rax => b801000000 и mov $1, %al => b001 когда мы говорим о тысячах регистрационных запросов в программа. Не только небольшой размер, если он является частью элегантности программного обеспечения, это влияет на производительность.

Может кто-нибудь объяснить, почему «GCC решил», что это не имеет значения?

Частичные регистры влекут за собой штраф за производительность на многих процессорах x86, потому что они были переименованы в разные физические регистры со всей их копии. (Подробнее о переименовании регистров, позволяющих выполнять внеуровневое выполнение, см. Этот вопрос и ответы ).

Но когда инструкция считывает весь регистр, ЦП должен обнаружить тот факт, что он не имеет правильного значения регистра архитектуры, доступного в одном физическом регистре. (Это происходит на этапе выпуска / переименования, так как процессор готовит отправку uop в планировщик вне очереди).

Это называется частичной регистрационной стойкой . Руководство по микроархитектуре Agner Fog объясняет это довольно хорошо:

6.8 Парные регистрационные стойки (PPro / PII / PIII и ранний Pentium-M)

Столб неполного регистра – это проблема, возникающая при записи в часть 32-битного регистра и последующего чтения из всего регистра или большей части.
Пример:

 ; Example 6.10a. Partial register stall mov al, byte ptr [mem8] mov ebx, eax ; Partial register stall 

Это дает задержку в 5-6 часов . Причина в том, что временный регистр был назначен AL чтобы сделать его независимым от AH . Блок выполнения должен ждать, пока запись в AL не будет удалена, прежде чем можно будет объединить значение из AL со значением остальной EAX .

Поведение в разных процессорах :

  • Intel раннее семейство P6: см. Выше: срыв на 5-6 часов, пока частичная запись не удаляется.
  • Intel Pentium-M (модель D) / Core2 / Nehalem: срыв на 2-3 цикла при вставке слияния. (см. это Q & A для микрообъектива, записывающего AX и чтения EAX с или без обхода нуля )
  • Intel Sandybridge: вставьте слияние uop для low8 / low16 (AL / AX) без остановки, или для AH / BH / CH / DH при остановке в течение 1 цикла.
  • Intel IvyBridge (возможно), но определенно Haswell / Skylake: AL / AX не переименованы, но AH по-прежнему: Как точно выполняют частичные регистры на Haswell / Skylake? Написание AL, похоже, имеет ложную зависимость от RAX, а AH несовместимо .
  • Все остальные процессоры x86 : Intel Pentium4, Atom / Silvermont / Knight’s Landing. Все AMD (и Via и т. Д.):

    Частичные регистры никогда не переименовываются. Запись неполного регистра сливается в полный регистр, заставляя запись зависеть от старого значения полного регистра в качестве входа.

Без переименования с неполным регистром входная зависимость для записи является ложной зависимостью, если вы никогда не читаете полный регистр. Это ограничивает параллелизм на уровне инструкций, поскольку повторное использование 8 или 16-битного регистра для чего-то еще фактически не зависит от точки зрения процессора (16-разрядный код может обращаться к 32-разрядным регистрам, поэтому он должен поддерживать правильные значения в верхнем половинки). А также, это делает AL и AH не независимыми. Когда Intel разработала семейство P6 (PPro, выпущенное в 1993 году), 16-разрядный код все еще был распространен, поэтому переименование частичного регистра было важной особенностью для ускорения работы существующего машинного кода. (На практике многие двоичные файлы не перекомпилируются для новых процессоров.)

Вот почему компиляторы в основном избегают писать неполные регистры. Они используют movzx / movsx когда это возможно, для нулевых или знаковых расширений узких значений до полного регистра, чтобы избежать ложных зависимостей частичного регистра (AMD) или киосков (Intel P6-family). Таким образом, самый современный машинный код мало выгоден от переименования с частичным регистром, поэтому последние процессоры Intel упрощают свою логику переименования с частичным регистром.

Как указывает ответ @ BeeOnRope , компиляторы все еще читают частичные регистры, потому что это не проблема. (Чтение AH / BH / CH / DH может добавить дополнительный цикл латентности на Haswell / Skylake, хотя см. Предыдущую ссылку о частичных регистрах для недавних членов семейства Sandybridge.)


Также обратите внимание, что write принимает аргументы, которые для обычно сконфигурированного GCC-686 необходимы целым 32-битным и 64-битным регистрам, поэтому его нельзя просто собрать в mov dl, 3 . Размер определяется типом данных, а не значением данных.

Наконец, в определенных контекстах, C имеет объявления по умолчанию, которые нужно знать, хотя это не так .
На самом деле, как отметил RossRidge , звонок, вероятно, был сделан без видимого прототипа.


Ваша демонстрация вводит в заблуждение, как отметил @Jester.
Например, mov rdx, 3 на самом деле mov edx, 3 , хотя оба имеют одинаковый эффект, т. rdx 3 во все rdx .
Это верно, потому что сразу значение 3 не требует расширения знака и MOV r32, imm32 неявно очищает верхние 32 бита регистра.

На самом деле gcc очень часто использует неполные регистры . Если вы посмотрите сгенерированный код, вы найдете множество случаев, когда используются частичные регистры.

Короткий ответ для вашего конкретного случая состоит в том, что gcc всегда подписывает или нулевое – расширяет аргументы до 32 бит при вызове функции C ABI .

Фактически SISV x86 и x86-64 ABI, принятые gcc и clang требуют, чтобы параметры, меньшие, чем 32 бита, были равны нулю или были расширены до 32 бит. Интересно, что их не нужно расширять до 64-битных.

Итак, для такой функции, как следующая, на платформе SysV ABI на 64-битной платформе:

 void foo(short s) { ... } 

… аргумент s передается в rdi а биты s будут выглядеть следующим образом (но см. мое предостережение ниже относительно icc ):

  bits 0-31: SSSSSSSS SSSSSSSS SPPPPPPP PPPPPPPP bits 32-63: XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX where: P: the bottom 15 bits of the value of `s` S: the sign bit of `s` (extended into bits 16-31) X: arbitrary garbage 

Код для foo может зависеть от S и P бит, но не от X бит, что может быть чем угодно.

Аналогично, для foo_unsigned(unsigned short u) вас должно быть 0 в битах 16-31, но в противном случае оно было бы идентичным.

Обратите внимание, что я сказал defacto – потому что на самом деле на самом деле не документировано, что делать для меньших типов возврата, но вы можете увидеть ответ Питера здесь для деталей. Я также задал связанный с этим вопрос.

После некоторого дополнительного тестирования я пришел к выводу, что icc фактически нарушает этот стандарт defacto. gcc и clang похоже, придерживаются этого, но gcc только консервативным образом: при вызове функции он аргументы zero / sign-extend до 32-битных, но в его реализациях функции не зависит от вызывающего, делающего это , clang реализует функции, зависящие от вызывающего, расширяющего параметры до 32 бит. Поэтому на самом деле clang и icc взаимно несовместимы даже для простых функций C, если они имеют любые параметры, меньшие, чем int .

Обратите внимание, что использование -O3 явно требует компилятора для агрессивного использования производительности по размеру кода. Используйте размер -Os если вы не готовы жертвовать примерно на 20% от размера.

Что-то вроде оригинального IBM PC, если было известно, что AH содержит 0, и было необходимо загрузить AX со значением, равным 0x34, используя «MOV AL, 34h», как правило, требуется 8 циклов, а не 12, требуемых для «MOV AX, 0034h “- довольно большое улучшение скорости (любая команда может выполняться в 2 циклах, если она предварительно загружена, но на практике 8088 проводит большую часть своего времени, ожидая, что инструкции будут извлечены с затратами в четыре цикла на каждый байт). Однако на процессорах, используемых на современных компьютерах общего назначения, время, требуемое для извлечения кода, обычно не является существенным фактором общей скорости выполнения, а размер кода обычно не является особой проблемой.

Кроме того, производители процессоров стараются максимизировать производительность тех типов людей, которые, скорее всего, будут работать, а 8-разрядные инструкции по загрузке вряд ли будут использоваться почти так же часто, как и 32-разрядные инструкции по загрузке. Процессорные ядра часто include в себя логику для одновременного выполнения нескольких 32-разрядных или 64-битных команд, но не могут включать логику для выполнения 8-битной операции одновременно с чем-либо еще. Следовательно, при использовании 8-битных операций на 8088, когда это было возможно, была полезной оптимизацией на 8088, это может фактически стать значительным утечкой производительности для новых процессоров.

  • 64-разрядный формат Mach-O не поддерживает 32-разрядные абсолютные адреса. Доступ к массиву NASM
  • Как найти, если собственный DLL-файл скомпилирован как x64 или x86?
  • Что будет использоваться для обмена данными между streamами, выполняются на одном ядре с HT?
  • Печать чисел с плавающей запятой из x86-64, по-видимому, требует сохранения% rbp
  • Почему инструкции x86-64 на 32-разрядных регистрах обнуляют верхнюю часть полного 64-битного регистра?
  • Почему GCC использует умножение на странное число при реализации целочисленного деления?
  • Ориентация как 32-битной, так и 64-битной с Visual Studio в том же решении / проекте
  • Оптимизация производительности сборки x86-64 - Выравнивание и outlookирование ветвлений
  • Плавающая точка и целочисленные вычисления на современном оборудовании
  • Использование дополнительных 16 бит в 64-битных указателях
  • Последовательные sys_write syscalls не работают должным образом, ошибка NASM на OS X?
  • Давайте будем гением компьютера.