Почему бы не сохранить параметры функции в плавающих регистрах?
В настоящее время я читаю книгу: "Компьютерные системы - перспектива для программистов". Я обнаружил, что в архитектуре x86-64 мы ограничены 6 интегральными параметрами, которые будут переданы функции в регистрах. Следующие параметры будут переданы в стек.
Почему бы не использовать плавающие регистры для хранения следующих параметров, даже если параметры не являются переменными одинарной / двойной точности? Насколько я понял, было бы гораздо эффективнее хранить данные в регистрах, чем сохранять их в памяти, а затем считывать из памяти.
2 ответа
Большинство функций имеют не более 6 целочисленных параметров, так что это действительно угловой случай. Передача некоторых избыточных целочисленных параметров в регистрах xmm усложнит правила поиска аргументов с плавающей запятой, что принесет мало пользы. Помимо того факта, что он, вероятно , не сделает код быстрее.
Еще одна причина для хранения избыточных параметров в памяти заключается в том, что вы, вероятно, не будете использовать их сразу. Если вы хотите вызвать другую функцию, вы должны сохранить эти параметры из регистров xmm в памяти, потому что вызываемая вами функция уничтожит любые регистры передачи параметров. (И все регистры xmm все равно сохраняются вызывающими.) Таким образом, вы можете потенциально получить код, который вставляет параметры в векторные регистры, где их нельзя использовать напрямую, и оттуда сохраняет их в памяти перед вызовом другой функции, и только затем загружает их обратно в целочисленные регистры. Или даже если функция не вызывает другие функции, возможно, ей нужны векторные регистры для собственного использования, и ей придется хранить параметры в памяти, чтобы освободить их для запуска векторного кода! Было бы проще просто push
параметры в стек, потому что push
по понятным причинам очень сильно оптимизирован, чтобы хранить и модифицировать RSP за один раз, примерно так же дешево, как mov
,
Существует один регистр целых чисел, который не используется для передачи параметров, но также не сохраняется в ABI SysV Linux/Mac x86-64 (r11). Полезно иметь пустой регистр для использования ленивого динамического кода компоновщика без сохранения (поскольку такие функции shim должны передавать все свои аргументы динамически загружаемой функции) и аналогичные функции-оболочки.
Таким образом, AMD64 могла бы использовать больше целочисленных регистров для параметров функции, но только за счет количества регистров, которые вызванные функции должны сохранить перед использованием. (Или двойного назначения r10 для языков, которые не используют указатель "статическая цепочка", или что-то в этом роде.)
В любом случае, больше параметров, передаваемых в регистрах, не всегда лучше.
Регистры xmm нельзя использовать в качестве регистров указателей или индексов, а перемещение данных из регистров xmm обратно в целочисленные регистры может замедлить окружающий код больше, чем загрузка только что сохраненных данных. (Если какой-либо ресурс выполнения будет узким местом, а не ошибками кэширования или ошибочными прогнозами ветвлений, это, скорее всего, будет исполнительными блоками ALU, а не загрузкой / хранением блоков. Перемещение данных из регистров xmm в gp требует ALU uop, в Intel и текущие проекты AMD.)
Кэш-память первого уровня действительно быстрая, а пересылка в режиме store-> load приводит к общей задержке при обращении к памяти примерно в 5 циклов, например в Intel Haswell. (Задержка такой инструкции, как inc dword [mem]
6 циклов, включая один цикл АЛУ.)
Если все, что вы собирались сделать, - это перенести данные из регистров 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, где два целочисленных ядра совместно используют вектор / FP, перемещение данных непосредственно между регистрами GP и векторными регистрами на самом деле происходит довольно медленно (8 или 10 циклов в одну сторону, или вдвое меньше, чем в Steamroller). Память туда и обратно составляет всего 8 циклов.
32-битный код работает достаточно хорошо, хотя все параметры передаются в стек и должны быть загружены. Процессоры очень хорошо оптимизированы для хранения параметров в стеке и последующей их загрузки, потому что старый 32-битный ABI по-прежнему используется для большого количества кода, особенно. на винде. (Большинство систем Linux в основном используют 64-битный код, в то время как большинство настольных систем Windows используют 32-битный код, потому что многие программы Windows доступны только в виде предварительно скомпилированных 32-битных двоичных файлов.)
См. http://agner.org/optimize/ для получения руководств по микроархитектуре ЦП, чтобы узнать, как выяснить, сколько циклов что-то действительно займет. В вики x86 есть и другие хорошие ссылки, в том числе документация по x86-64 ABI, ссылка на которую приведена выше.
Я думаю, что это не очень хорошая идея, потому что:
Вы не можете использовать регистры FPU/SSE в качестве регистров общего назначения. Я имею в виду, этот код не является правильным (NASM):
mov byte[st0], 0xFF
Если сравнить отправку данных в / из FPU/SSE с регистрами / памятью общего назначения, FPU/SSE работает очень медленно.
РЕДАКТИРОВАТЬ: Помните, я могу быть не прав.