Запретить GCC использовать динамические переходы / вызовы функций
Я пытаюсь написать модуль инструментария сборки для приложений, скомпилированных GCC, как часть инфраструктуры безопасности. Чтобы повысить производительность модуля, мне нужно уменьшить как можно больше динамических переходов / вызовов динамических функций. В основном они используют некоторый динамический указатель (например, регистр) для выполнения перехода или вызова функции.
Текущий компилятор GCC, когда он имеет несколько вызовов одной и той же функции (определенная метка в коде), загружает метку в регистр, а затем переходит к этому регистру всякий раз, когда ему нужно вызвать функцию. Это, конечно, гораздо более быстрый подход, чем каждый раз переходить к одной и той же метке (меньший код и меньший такт), но, как я уже упоминал, это было бы неэффективно с моей структурой. Чтобы дать вам пример того, чего я бы хотел избежать, вот фрагмент кода:
MOV #function_label, R10. #Copy the label to the R10 register
CALL R10
...
...
CALL R10
...
...
CALL R10
Хотя я бы хотел, чтобы GCC сделал следующее:
CALL #label_function
...
...
CALL #label_function
...
...
CALL #label_function
Помните, что я на самом деле использую mspgcc, компилятор GCC для семейства микроконтроллеров MSP430, но он не должен иметь большого значения, поскольку он основан на GCC.
Как вы думаете, есть ли что-нибудь, что можно сделать (кроме переписывания компилятора GCC)? Большое спасибо за Вашу помощь
1 ответ
Использовать
-fno-function-cse
чтобы не выполнять устранение общих-подвыражений в адресах функций. Руководство GCC:
-fno-function-cse
Не помещайте адреса функций в регистры; сделать так, чтобы каждая инструкция, вызывающая постоянную функцию, явно содержала адрес функции.
Эта опция приводит к менее эффективному коду, но некоторые странные хаки, которые изменяют вывод ассемблера, могут быть сбиты с толку оптимизацией, выполняемой, когда эта опция не используется.
По умолчанию -ffunction-cse
Как найти конкретные параметры GCC
я смотрел на
gcc -O1 -fverbose-asm
asm, чтобы увидеть все параметры оптимизации, которые
-O1
подразумевает (который GCC перечисляет в комментариях asm).
-O1 -fno-...
версии всего скомпилированы всего в 3
call
инструкции с именем символа на каждом, подтверждающие, что один из них был тем, который я хотел, поэтому мне просто пришлось сузить его, разделив этот список пополам
-fno-
параметры
Я использовал обозреватель компилятора Godbolt с MSP430 GCC6.2.1, тестовый код + asm. Я отключил опцию фильтра "комментарии", чтобы видеть в выводе asm строки с чистыми комментариями.
Поскольку вариантов было множество, я использовал
tr ' ' '\n' | sed -e 's/-f/-fno-/' -e '/;/d'
превратить
-f
варианты в их отрицательную форму. Я скопировал / вставил весь блок комментариев asm в эту команду в терминале и скопировал / вставил результат в поле параметров GCC на Godbolt. (Вместе с
-O1
.
-O0
- это специальный антиоптимизированный режим для последовательной отладки, поэтому оптимизация между операторами может никогда не быть активной в
-O0
даже при правильном варианте. Вот почему мне нужно было отрицать варианты вместо того, чтобы пробовать положительную форму без
-O1
)
Затем я выбрал и удалил несколько опций, чтобы посмотреть, изменилось ли это в asm. Если нет, продолжайте. Когда я нашел блок, который сделал это, я знал, что там есть нужный мне вариант, поэтому я мог отменить (control-z) и удалить все остальные
-f
варианты, а затем сузьте его до одного. (Как только я увидел имя
-fno-function-cse
в этой группе я подумал, что это звучит правильно. К счастью, параметры GCC имеют значимые имена, если вы знаете терминологию компилятора / оптимизации.)
Это было быстрее, чем рассматривать по одному варианту за раз или пролистывать руководство, потому что я даже не был уверен, что какой-либо из этих вариантов будет управлять этим.
Кстати, GCC не выполняет эту оптимизацию размера кода для большинства других ISA, потому что для них это не выигрыш в производительности. Размер кода - не самый важный фактор для производительности на x86-64 или даже большом пальце ARM; дополнительные затраты на возможное неверное предсказание переходов для косвенных переходов (и дополнительное загрязнение предикторов перехода) перевешивают затраты на размер кода.
Это является код размера выиграть на x86, где 5-байтовый
mov
-медленный или 7-байтовый RIP-относительный
lea
(x86-64) можно настроить для нескольких 2-байтовых
call
инструкции.
Обычно это даже не выигрыш в размере кода для многих ISA с фиксированной шириной инструкций, таких как AArch64 или ARM (кроме режима Thumb), где стандартная модель кода предполагает, что функции будут находиться в диапазоне друг от друга для относительного перехода и ссылки. (вызов) инструкции. Таким образом, для вызова любой функции требуется одна инструкция того же размера, что и любая другая инструкция.
Даже с
-ffunction-cse
При явном включении GCC просто не выполняет эту оптимизацию для x86-64 или ARM, даже в том случае, если он уже использует указатель на функцию из GOT. (x86-64 gcc
-Os -fPIE -fno-plt -ffunction-cse
на Godbolt. Я даже сказал GCC оптимизировать размер кода; сохранение / восстановление регистра с сохранением вызовов, например RBX, для использования с 2-байтовым
call rbx
вместо 6-байтового
call [RIP+rel32]
сохранит размер даже после дополнительных инструкций, необходимых для push/pop RBX (по 1 байту каждая) и загрузки в RBX (один mov с режимом адресации по RIP).)
Это можно считать пропущенной оптимизацией для
-Os
, особенно для ARM Thumb для "простых" ядер вроде
-mcpu=cortex-m3
который может даже не иметь предсказателя ветвления.
(AArch64 будет загрузить функцию-указатель в регистр с
-fPIE -fno-plt
, для функции без "скрытой" видимости, т.е. когда функция может находиться только в разделяемой библиотеке. Это бывает даже с
-fno-function-cse
. https://godbolt.org/z/f3MP56..)