Запретить 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..)

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