Подтвердить что ты не робот

Почему 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  

Это похоже на большую оптимизацию, но это не относится к условным соглашениям вообще, поэтому тест нечестный.

4b9b3361

Ответ 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 и найдите различия в частоте кадров. И получайте метрики от любых встроенных функций отчетности о производительности и др.