Почему fastcall медленнее, чем stdcall?
Я нашел следующий вопрос: Быстро ли работает fastcall?
Никаких четких ответов для x86 не было дано, поэтому я решил создать тест.
Вот код:
#include <time.h>
int __fastcall func(int i)
{
return i + 5;
}
int _stdcall func2(int i)
{
return i + 5;
}
int _tmain(int argc, _TCHAR* argv[])
{
int iter = 100;
int x = 0;
clock_t t = clock();
for (int j = 0; j <= iter;j++)
for (int i = 0; i <= 1000000;i++)
x = func(x & 0xFF);
printf("%d\n", clock() - t);
t = clock();
for (int j = 0; j <= iter;j++)
for (int i = 0; i <= 1000000;i++)
x = func2(x & 0xFF);
printf("%d\n", clock() - t);
printf("%d", x);
return 0;
}
В случае отсутствия результата оптимизации в MSVC 10:
4671
4414
С максимальной оптимизацией fastcall
иногда бывает быстрее, но я думаю, что это многозадачный шум. Вот средний результат (с iter = 5000
)
6638
6487
stdcall
выглядит быстрее!
Вот результаты для GCC: http://ideone.com/hHcfP
Опять же, fastcall
потерянная раса.
Вот часть разборки в случае fastcall
:
011917EF pop ecx
011917F0 mov dword ptr [ebp-8],ecx
return i + 5;
011917F3 mov eax,dword ptr [i]
011917F6 add eax,5
это для stdcall
:
return i + 5;
0119184E mov eax,dword ptr [i]
01191851 add eax,5
i
передается через ECX
вместо стека, но сохраняется в стеке в теле! Таким образом, весь эффект пренебрегают! эта простая функция может быть рассчитана с использованием только регистров! И между ними нет реальной разницы.
Может ли кто-нибудь объяснить, что является причиной для fastcall
? Почему это не ускоряет?
Изменить: С оптимизацией оказалось, что обе функции встроены. Когда я обернулся, они оба скомпилированы для:
00B71000 add eax,5
00B71003 ret
Это похоже на большую оптимизацию, но это не относится к условным соглашениям вообще, поэтому тест нечестный.
Ответы
Ответ 1
__fastcall
был введен давно. В то время Watcom С++ избивал Microsoft для оптимизации, и ряд рецензентов выбрали свою конвенцию на основе регистров как одну из возможных причин.
Microsoft ответила добавлением __fastcall
, и с тех пор они сохранили его, но я не думаю, что они когда-либо делали намного больше, чем достаточно, чтобы иметь возможность сказать: "У нас также есть конвенция на основе регистров..." Их предпочтение (особенно, поскольку 32-битная миграция), кажется, для __stdcall
. Они приложили немало усилий для улучшения их генерации кода, но (по-видимому) не так много с __fastcall
. С кэшированием на кристалле выигрыш от передачи вещей в регистрах не так велик, как в любом случае.
Ответ 2
Ваш микро-бенчмарк дает нерелевантные результаты. __fastcall
имеет специфическое использование с инструкциями SSE (см. XNAMath), clock()
даже не является удаленным подходящим таймером для бенчмаркинга и __fastcall
существует для нескольких платформ, таких как Itanium и некоторые другие, а не только для x86, и, кроме того, вся ваша программа может быть эффективно оптимизирована ни к чему, кроме операторов printf
, что делает относительную производительность __fastcall
или __stdcall
очень неуместно.
Наконец, вы забыли осознать основную причину, что многие вещи выполняются так, как они есть - наследие. __fastcall
вполне может быть значительным до того, как встраивание компилятора станет таким же агрессивным и эффективным, как сегодня, и ни один компилятор не удалит __fastcall
, так как будут программы, зависящие от него. Это делает __fastcall
фактом жизни.
Ответ 3
Несколько причин
- По крайней мере, в большинстве достойных реализаций x86, переименование регистров действует - усилие, которое похоже на сохранение с использованием регистра вместо памяти, может не делать ничего на аппаратном уровне.
- Конечно, вы сохраняете усилия по перемещению стека с помощью
__fastcall
, но вы уменьшаете количество доступных для использования функций в функции без изменения стека.
В большинстве случаев, когда __fastcall
будет быстрее, эта функция достаточно проста, чтобы быть встроенной в любом случае, а это значит, что это действительно не имеет значения в реальном программном обеспечении. (Что является одной из основных причин, почему __fastcall
не часто используется)
Боковое примечание: что случилось с аноном?
Ответ 4
Fastcall действительно имеет смысл только в том случае, если вы используете полную оптимизацию (в противном случае его эффекты будут похоронены другими артефактами), но, как вы заметили, при полной оптимизации функции будут встраиваться, и вы не увидите эффекта вызова соглашений на всех.
Чтобы на самом деле протестировать это, вам нужно сделать объявления extern
с фактическими определениями в отдельном исходном файле, который вы компилируете отдельно и ссылаетесь на свою основную процедуру. Когда вы это сделаете, вы увидите, что __fastcall последовательно на 25% быстрее с такими небольшими функциями.
В результате получается, что __fastcall действительно полезен, если у вас много вызовов крошечных функций, которые не могут быть встроены, потому что их нужно отдельно компилировать.
Edit
Итак, с отдельной компиляцией и gcc -O3 -fomit-frame-pointer -m32
я вижу совершенно другой код для двух функций:
func:
leal 5(%ecx), %eax
ret
func2:
movl 4(%esp), %eax
addl $5, %eax
ret
Выполнение этого с помощью iter = 5000 последовательно дает мне результаты, близкие к
9990000
14160000
что указывает на то, что версия fastcall имеет более чем 40% оттенок.
Ответ 5
Я скомпилировал две функции с помощью i686-w64-mingw32-gcc -O2 -fno-inline fastcall.c
. Это сборка, сгенерированная для func
и func2
:
@[email protected]:
leal 5(%ecx), %eax
ret
[email protected]:
movl 4(%esp), %eax
addl $5, %eax
ret $4
__ fastcall действительно выглядит быстрее для меня. func2
необходимо загрузить входной параметр из стека. func
может просто выполнить a %eax := %ecx + 5
и затем вернуться к вызывающему.
Кроме того, вывод вашего программирования обычно такой, как в моей системе:
2560
3250
154
Итак, __fastcall работает не только быстрее, но и быстрее.
Также обратите внимание, что на x86_64 (или x64, когда Microsoft называет его), __fastcall является стандартным, а старое не-fastcall convetion больше не существует.
http://en.wikipedia.org/wiki/X86_calling_conventions#x86-64_calling_conventions
Применив значение __fastcall по умолчанию, x86_64 догоняет другие архитектуры (например, ARM), где также передаются аргументы в регистрах.
Ответ 6
Fastcall сам по себе как соглашение о назначении на основе регистров не очень велико на x86, поскольку доступно не так много названных регистров, и с помощью ключевых регистров для передачи значений все, что вы делаете, потенциально может заставить вызывающий код нажать другие значения в стек и принуждение вызываемой функции, если она имеет достаточную сложность сделать то же самое. По существу с точки зрения ассемблера вы увеличиваете давление на эти названные регистры и явно используете операции стека для компенсации. Поэтому, даже если у процессора имеется гораздо больше регистров, доступных для переименования, он не собирается реорганизовывать явные операции стека, которые необходимо вставить.
С другой стороны, на более "богатых регистрами" архитектурах, таких как x86-64, условные соглашения на основе регистров (не точно такие же, как fastcall старой, но той же концепции) являются нормой и используются по всем направлениям. Другими словами, как только мы вышли из нескольких названных регистров, таких как x86, к чему-то с большим количеством регистров, fastcall вернулся в большой путь и стал стандартным и действительно единственным способом, который используется сегодня.
Ответ 7
Не похоже, что __fastcall фактически указывает, что он будет быстрее. Кажется, что все, что вы делаете, перемещает первые переменные вида в регистры перед вызовом функции. Это, скорее всего, приводит к замедлению функции, поскольку она должна сначала перенести переменные в эти регистры. В Википедии была довольно хорошая запись о том, что именно "Быстрый вызов" и как оно реализовано.
Ответ 8
Примечание: даже отредактированный в мае 2017 года ФП, этот вопрос и ответы, скорее всего, устареют и больше не будут актуальны к 2019 году (если не несколькими годами ранее).
A) По крайней мере, на MSVC 2017 (и 2019 выпущен недавно). во всяком случае, большая часть кода будет встроена в оптимизированные сборки релизов. Вероятно, единственное тело функции, которое вы увидите во всем примере сейчас, это "_tmain()".
Это если вы специально не сделаете несколько трюков, таких как объявление функций как "volatile" и/или оборачивание тестовых функций в прагмы, которые отключают некоторые оптимизации.
Б) Последнее поколение процессоров для настольных ПК (допущение здесь) значительно улучшилось с поколения 2010 года. Они намного лучше кешируют стек, выравнивание памяти менее важно и т.д.
Но не верьте мне на слово. Загрузите свой исполняемый файл в распространитель (IDA Pro, MSVC отладчик и т.д.) И найдите себя (хороший способ обучения).
Теперь было бы интересно посмотреть, какова будет производительность по сравнению с большим 32-битным приложением. Например, возьмите последний выпуск игры DOOM с открытым исходным кодом и постройте сборки с использованием stdcall и _fastcall и найдите различия в частоте кадров. И получайте метрики от любых встроенных функций отчетности о производительности и др.