Почему бы не сохранить параметры функции в поплавковых регистрах?

Сейчас я читаю книгу: "Компьютерные системы - перспектива программистов". Я узнал, что на архитектуре x86-64 мы ограничены 6 интегральными параметрами, которые будут переданы функции в регистры. Следующие параметры будут переданы в стек.

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

Ответы

Ответ 1

В большинстве функций не более 6 целых параметров, поэтому это действительно угловой случай. Передача некоторых избыточных целых параметров в xmm-регистрах создаст правила для того, где найти сглаживание с плавающей запятой сложнее, для мало пользы. Кроме того, что он, вероятно, не сделает код быстрее.

Еще одна причина для хранения избыточных параметров в памяти заключается в том, что вы, вероятно, не будете использовать их сразу. Если вы хотите вызвать другую функцию, вам нужно сохранить эти параметры из регистров xmm в память, потому что вызываемая вами функция уничтожит любые регистры передачи параметров. (И все хмм-регионы в любом случае сохраняются.) Таким образом, вы можете в конечном итоге получить код, который заполняет параметры в векторных регистрах, где они не могут использоваться напрямую, а оттуда хранят их в памяти перед вызовом другой функции и только затем загружает их обратно в целые регистры. Или даже если функция не вызывает другие функции, возможно, ей нужны векторные регистры для собственного использования, и им придется хранить params в памяти, чтобы освободить их для запуска векторного кода! Было бы проще просто push params в стеке, потому что push очень сильно оптимизировал, по очевидным причинам, делать хранилище и модификацию RSP всего в одном uop, примерно так же дешево, как mov.

Существует один целочисленный регистр, который не используется для передачи параметров, но также не сохраняется в SysV Linux/Mac x86-64 ABI (r11). Полезно иметь регистр нуля для ленивого динамического компоновщика, который будет использоваться без сохранения (поскольку такие функции прокладки должны передавать все свои аргументы динамически загруженной функции) и аналогичные функции-обертки.

Таким образом, AMD64 может использовать более целочисленные регистры для параметров функции, но только за счет количества регистров, которые вызываемые функции должны сохранять перед использованием. (Или двойного назначения r10 для языков, которые не используют указатель "статической цепочки" или что-то в этом роде.)

В любом случае, больше параметров, передаваемых в регистры, не всегда лучше.


xmm регистры не могут использоваться в качестве указателей или индексных регистров, а перемещение данных из регистров xmm обратно в целые регистры может замедлить окружающий код больше, чем загрузка только что сохраненных данных. (Если какой-либо ресурс выполнения будет узким местом, а не ошибками кэша или неверными событиями ветвей, скорее всего, это будут исполняемые модули ALU, а не единицы загрузки/хранения. Перемещение данных из регистров xmm в gp принимает ALU uop, в Intel и AMD.)

L1-кеш работает очень быстро, а store- > load forwarding делает общую задержку для обратного перехода в память примерно 5 циклов, например. Intel Haswell. (Задержка команды типа inc dword [mem] составляет 6 циклов, включая один цикл ALU.)

Если перемещение данных из регистров xmm в gp было всем, что вы собирались делать (ничто другое не позволяло исполняющим блокам ALU работать), тогда да, на процессорах Intel, латентность в оба конца для movd xmm0, eax/movd eax, xmm0 ( 2 цикла Intel Haswell) меньше, чем латентность mov [mem], eax/mov eax, [mem] (5 циклов Intel Haswell), но целочисленный код обычно не является узким местом задержек, как часто код FP.

В процессорах AMD Bulldozer, где два целых ядра совместно используют блок vector/FP, перемещение данных непосредственно между GP regs и vector regs на самом деле довольно медленное (8 или 10 циклов в одну сторону, или половину, что на Steamroller). Промежуток памяти занимает всего 8 циклов.

32-битный код управляется достаточно хорошо, хотя все параметры передаются в стек и должны быть загружены. Процессоры очень оптимизированы для хранения параметров в стеке, а затем загружают их снова, потому что жесткий 32-битный ABI по-прежнему используется для большого количества кода, особенно. в Windows. (Большинство Linux-систем в основном работают с 64-битным кодом, в то время как большинство настольных систем Windows запускают много 32-битного кода, потому что так много программ для Windows доступны только в виде предварительно скомпилированных 32-битных двоичных файлов.)

См. http://agner.org/optimize/ для руководств по микроархитектуре ЦП, чтобы узнать, как определить, сколько циклов что-то действительно предпримет. Есть и другие хорошие ссылки в wiki, включая связанный выше документ x86-64 ABI.

Ответ 2

Я думаю, что это не очень хорошая идея, потому что:

  • Вы не можете использовать регистры FPU/SSE в качестве регистров общего назначения. Я имею в виду, что этот код неверен (NASM):

    mov byte[st0], 0xFF
    
  • Если сравнивать отправку данных в/из FPU/SSE с помощью регистров общего назначения/памяти, FPU/SSE выполняется очень медленно.

EDIT: Помните, что я могу быть не прав.