Я связал некоторую сборку с некоторым c, чтобы проверить стоимость вызова функции, со следующей сборкой и c-источником (используя fasm и gcc соответственно)
сборка:
format ELF
public no_call as "_no_call"
public normal_call as "_normal_call"
section '.text' executable
iter equ 100000000
no_call:
mov ecx, iter
@@:
push ecx
pop ecx
dec ecx
cmp ecx, 0
jne @b
ret
normal_function:
ret
normal_call:
mov ecx, iter
@@:
push ecx
call normal_function
pop ecx
dec ecx
cmp ecx, 0
jne @b
ret
c источник:
#include <stdio.h>
#include <time.h>
extern int no_call();
extern int normal_call();
int main()
{
clock_t ct1, ct2;
ct1 = clock();
no_call();
ct2 = clock();
printf("\n\n%d\n", ct2 - ct1);
ct1 = clock();
normal_call();
ct2 = clock();
printf("%d\n", ct2 - ct1);
return 0;
}
Результаты, которые я получил, были удивительными. Прежде всего, скорость зависела от того порядка, в котором я связан. Если я связан как gcc intern.o extern.o
, типичный вывод
162
181
Но ссылка в обратном порядке gcc extern.o intern.o
, я получил результат больше:
162
130
То, что они разные, было очень удивительным, но это не вопрос, который я задаю. (соответствующий вопрос здесь)
Вопрос, который я задаю, заключается в том, как во втором цикле цикл с вызовом функции был быстрее, чем цикл без него, как стоимость вызова функции была явно отрицательной.
Edit: Просто чтобы упомянуть о некоторых вещах, которые пробовали в комментариях:
- В скомпилированном байт-коде вызов функции не был оптимизирован.
- Настройка выравнивания функций и циклов на все, от 4 до 64 байтовых границ, не ускоряла no_call, хотя некоторые выравнивания замедляли normal_call
- Предоставление процессору/ОС возможности разогреться, вызывая функции несколько раз, а не только один раз, не имело заметного эффекта от измеренных длительностей времени, также не меняет порядок вызовов или работает отдельно
- Запуск в течение более длительного времени не влияет на коэффициент, например, работает в 1000 раз дольше. Я получил
162.168
и131.578
секунды для моего времени выполнения.
Кроме того, после изменения кода сборки для выравнивания по байтам, я тестировал предоставление дополнительного набора функций и пришел к еще более странным выводам. Вот обновленный код:
format ELF
public no_call as "_no_call"
public normal_call as "_normal_call"
section '.text' executable
iter equ 100000000
offset equ 23 ; this is the number I am changing
times offset nop
times 16 nop
no_call:
mov ecx, iter
no_call.loop_start:
push ecx
pop ecx
dec ecx
cmp ecx, 0
jne no_call.loop_start
ret
times 55 nop
normal_function:
ret
times 58 nop
normal_call:
mov ecx, iter
normal_call.loop_start:
push ecx
call normal_function
pop ecx
dec ecx
cmp ecx, 0
jne normal_call.loop_start
ret
Мне пришлось вручную (и не переносимо) принудительно выравнивать 64 байта, так как FASM не поддерживает более 4 байтовых выравниваний для исполняемого раздела, по крайней мере, на моей машине. Смещение программы на offset
байт, вот что я нашел.
if (20 <= offset mod 128 <= 31) then we get an output of (approximately):
162
131
else
162 (+/- 10)
162 (+/- 10)
Не уверен, что с этим делать, но тем, что я обнаружил до сих пор
Изменить 2:
Еще одна вещь, которую я заметил, заключается в том, что если вы удаляете push ecx
и pop ecx
из обеих функций, выход становится
30
125
что указывает на то, что это самая дорогая его часть. Выравнивание стека одно и то же, поэтому это не является причиной расхождения. Мое лучшее предположение заключается в том, что каким-то образом аппаратное обеспечение оптимизировано для ожидания вызова после нажатия или чего-то подобного, но я не знаю ничего подобного