为什么fastcall比stdcall慢?

时间:2011-03-29 21:49:35

标签: c++ optimization

我发现了以下问题:Is fastcall really faster?

没有给出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  

这确实看起来很棒,但它根本不尊重调用约定,因此测试不公平。

8 个答案:

答案 0 :(得分:23)

__fastcall long 之前推出了。当时,Watcom C ++击败微软进行优化,许多评论家选择了基于寄存器的调用约定作为一个(可能)原因。

微软通过添加__fastcall作为回应,他们从那时起就保留了它 - 但我认为他们做得不够,以至于能够说“我们有一个基于注册的呼叫约定”也是......“他们的偏好(特别是自32位迁移以来)似乎是__stdcall。他们已经投入了相当多的工作来改进它们的代码生成,但(显然)与__fastcall的差别不大。通过片上缓存,寄存器中传递信息的收益并不像以前那么大。

答案 1 :(得分:15)

您的微观基准会产生无关的结果。 __fastcall具有SSE指令的特定用途(请参阅XNAMath),clock() 甚至远程用于基准测试的合适计时器,并且__fastcall存在多个平台,如Itanium和其他一些平台,不仅适用于x86,此外,您的整个程序可以有效地优化为除printf语句之外的任何内容,从而使__fastcall或{{1非常非常无关紧要。

最后,你忘记了很多事情按照遗产的方式完成的主要原因。在编译器内联变得像今天这样具有攻击性和有效性之前,__stdcall可能已经很重要,并且没有编译器会删除__fastcall,因为会有依赖它的程序。这使__fastcall成为事实。

答案 2 :(得分:8)

有几个原因

  1. 至少在大多数体面的x86实现中,寄存器重命名是有效的 - 通过使用寄存器而不是内存来保存的工作可能在硬件级别上没有做任何事情。
  2. 当然,您可以使用__fastcall保存一些堆栈移动工作,但是您可以在不修改堆栈的情况下减少可在函数中使用的寄存器数量。
  3. 大多数时候__fastcall会更快,函数很简单,无论如何都要内联,这意味着它在真实软件中无关紧要。 (这是不经常使用__fastcall的主要原因之一)

    旁注:Anon的回答出了什么问题?

答案 3 :(得分:8)

Fastcall只有在你使用完全优化时才有意义(否则它的效果会被其他工件掩盖),但正如你所说,通过完全优化,函数将被内联,你将看不到调用约定的效果一点都不。

因此,为了对此进行实际测试,您需要使用单独的源文件中的实际定义来创建函数extern声明,这些源文件将单独编译并与主例程链接。当你这样做时,你会发现使用像这样的小函数,__ fastcall的速度一直快〜25%。

结果是__fastcall实际上只有在你需要单独编译时才能调用无法内联的微小函数时才有用。

修改

因此,通过单独的编译和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%。

答案 4 :(得分:2)

我用i686-w64-mingw32-gcc -O2 -fno-inline fastcall.c编译了这两个函数。这是为funcfunc2生成的程序集:

@func@4:
    leal    5(%ecx), %eax
    ret
_func2@4:
    movl    4(%esp), %eax
    addl    $5, %eax
    ret $4

__ fastcall对我来说真的很快。 func2需要从堆栈加载输入参数。 func只需执行%eax := %ecx + 5,然后返回给调用者。

此外,您的编程输出在我的系统上通常是这样的:

2560
3250
154

所以__fastcall不仅看起来更快, 更快。

另请注意,在x86_64(或Microsoft称之为x64)上,__ fastcall是默认值,旧的非快速呼叫会话不再存在。 http://en.wikipedia.org/wiki/X86_calling_conventions#x86-64_calling_conventions

通过使__fastcall成为默认值,x86_64可以赶上其他架构(例如ARM),其中在寄存器中传递参数也是默认值。

答案 5 :(得分:2)

快速调用自身作为基于寄存器的调用约定在x86上并不是很好,因为没有那么多可用的命名寄存器,并且通过使用键寄存器来传递值,所有你正在做的就是可能强制调用代码推送其他值进入堆栈并强制调用被调用函数,如果它具有足够的复杂性来执行相同操作。基本上从汇编语言的角度来看,你增加了那些命名寄存器的压力,并明确地使用堆栈操作来补偿。因此,即使CPU有更多可用于重命名的寄存器,它也不会重构必须插入的显式堆栈操作。

另一方面,在更多“注册丰富”的体系结构(如x86-64)上,基于寄存器的调用约定(与旧的快速调用完全相同,但概念相同)是常态,并且全面使用。换句话说,一旦我们退出了一些像x86这样的命名寄存器架构,对于具有更多寄存器空间的东西,fastcall又回到了很大的位置,成为了今天使用的默认方式。

答案 6 :(得分:0)

__fastcall实际上并没有表明它会更快。好像你正在做的就是在调用函数之前将第一个fiew变量移动到寄存器中。这很可能会使您的函数调用变慢,因为它必须首先将变量移动到这些寄存器中。维基百科对于Fast Call到底是什么以及它是如何实现有很好的写作。

答案 7 :(得分:0)

注意:即使是由OP在2017年5月进行了编辑,该问题和答案也可能已经过时,并且到2019年(如果不是几年前)也不再相关。

A)至少达到MSVC 2017(和最近发布的2019)。无论如何,大多数代码都将内联到优化的发行版中。您现在可能在整个示例中看到的唯一函数体是“ _tmain()”。

除非您特意做一些技巧,例如将函数声明为“ volatile”和/或将测试函数包装在编译指示中以关闭某些优化。

B)自2010年左右以来,最新一代的台式机CPU(此处的假设)得到了很大的改进。它们在缓存堆栈方面更胜一筹,而内存对齐问题则更少,等等。

但是不要相信我。在反汇编器(IDA Pro,MSVC调试器等)中加载可执行文件,然后寻找自己(一种学习的好方法)。

现在,看看大型32位应用程序的性能将是有趣的。例如,获取最新的开放源代码DOOM游戏版本,并使用stdcall和_fastcall进行构建,并查找帧速率差异。并从其具有的所有内置性能报告功能中获取指标。