В начало → Изучаем параметры gcc → Параметры, относящиеся к вызову функций |
По существу, gcc предоставляет вам несколько путей управления тем, как вызывается функция. Сначала давайте рассмотрим встраивание. С помощью встраивания, вы сокращаете стоимость вызова функции, так как тело функции подставлено прямо в вызывающую функцию. Пожалуйста, учтите, что это не по умолчанию, а только когда вы используете -O3 или, по крайней мере, -finline-functions.
Как полученный бинарный файл выглядит после того, как gcc сделает встраивание? Рассмотрим следующий листинг:
#include inline test(int a, int b, int c) { int d; d=a*b*c; printf("%d * %d * %d is %d\n",a,b,c,d); } static inline test2(int a, int b, int c) { int d; d=a+b+c; printf("%d + %d + %d is %d\n",a,b,c,d); } int main(int argc, char *argv[]) { test(1,2,3); test2(4,5,6); }
Скомпилируем этот код со следующим параметром:
$
gcc -S -O3 -o
-S указывает gcc остановиться сразу после стадии компиляции (мы расскажем о ней позже в этой статье). Результат будет следующим:
.... test: pushl %ebp movl %esp, %ebp pushl %ebx .... main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) ... movl $6, 16(%esp) movl $3, 12(%esp) movl $2, 8(%esp) movl $1, 4(%esp) movl $.LC0, (%esp) call printf ... movl $15, 16(%esp) movl $6, 12(%esp) movl $5, 8(%esp) movl $4, 4(%esp) movl $.LC1, (%esp) call printf ...
И test(), и test() действительно встроены, но вы также можете видеть test(), который остался вне main(). Вот где играет роль ключевое слово static. Написав, что функция — static, вы сообщаете gcc, что эта функция не будет вызываться из какого-либо внешнего объектного файла, поэтому нет нужды порождать коды. Таким образом, это экономит размер, и если вы можете сделать функцию статичной, сделайте это где только возможно. С другой стороны, будьте благоразумны при решении, какая функция должна быть встраиваемой. Увеличение размера для небольшого увеличения скорости не всегда оправдано.С помощью некоторой эвристики, gcc решает, должна быть функция встраиваемой, или нет. Одним из таких доводов является размер функции в терминах псевдо-инструкций. По умолчанию, лимитом является 600. Вы можете поменять этот лимит, используя -finline-limit. Проэксперементируйте для нахождения лучших лимитов встраивания для вашего конкретного случая. Также возможно переделать эвристику так, чтобы gcc всегда встраивал функцию. Просто объявите вашу функцию так:
__attribute__((always_inline)) static inline test(int a, int b, int c)
Теперь перейдем к передаче параметров. На архитектуре x86, параметры помещаются в стек и позже достаются из стека для дальнейшей обработки. Но gcc дает вам возможность изменить это поведение и использовать вместо этого регистры. Функции, у которых меньше трех параметров могут использовать эту возможность указанием -mregparm=
... test: pushl %ebp movl %esp, %ebp subl $56, %esp movl %eax, -20(%ebp) movl %edx, -24(%ebp) movl %ecx, -28(%ebp) ... main: ... movl $3, %ecx movl $2, %edx movl $1, %eax call test
Вместо стека, используются EAX, EDX и ECX для хранения первого, второго и третьего параметров. Поскольку доступ к регистру происходит быстрее, чем к ОЗУ, это будет одним из способов уменьшить время работы. Хотя вы должны обратить внимание на следующие вещи:— Вы ДОЛЖНЫ компилировать весь ваш код с таким же числом -mregparm регистров. Иначе у вас будут проблемы с вызовом функций из другого объектного файла, если они будут принимать разные соглашения.— Используя -mregparm, вы разрушаете совместимый с Intel x86 бинарный интерфейс приложений (ABI). Поэтому, вы должны учитывать это, если вы распространяете свое ПО только в бинарной форме.
Возможно, вы заметили эту последовательность в начале каждой функции:
push %ebp mov %esp,%ebp sub $0x28,%esp
Эта последовательность, также известная как пролог функции, написана чтобы установить указатель фрейма (EBP). Это приносит пользу, помогая отладчику делать трассировку стека. Следующая структура поможет вам понять это [6]:[ebp-01] Последний байт последней локальной переменной
[ebp+00] Старое значение ebp
[ebp+04] Возвращает адрес
[ebp+08] Первый аргумент
Можем мы пренебречь этим? Да, с помощью -fomit-frame-pointer, пролог будет укорочен, так что функция начнется просто с выделения стека (если есть локальные переменные):
sub $0x28,%esp
Если функция вызывается очень часто, вырезание пролога спасет несколько тактов ЦПУ. Но будьте осторожны: делая это, вы также усложняете отладчику задачу по изучению стека. Например, давайте добавим test(7,7,7) в конец test2() и перекомпилируем с параметром -fomit-frame-pointer и без оптимизации. Теперь запустите gdb для исследования бинарного файла:
$ gdb inline (gdb) break test (gdb) r Breakpoint 1, 0x08048384 in test () (gdb) cont Breakpoint 1, 0x08048384 in test () (gdb) bt #0 0x08048384 in test () #1 0x08048424 in test2 () #2 0x00000007 in ?? () #3 0x00000007 in ?? () #4 0x00000007 in ?? () #5 0x00000006 in ?? () #6 0x0000000f in ?? () #7 0x00000000 in ?? ()
При втором вызове test, программа остановилась, и gdb вывел трассировку стека. В нормальной ситуации, main() должна идти в фрейме №2, но мы видим только знаки вопроса. Запомните, что я сказал про расположение стека: отсутствие указателя фрейма мешает gdb находить расположение сохраненного возвращаемого адреса в фрейме №2.
В начало → Изучаем параметры gcc → Параметры, относящиеся к вызову функций |