【问题标题】:Why is fastcall slower than stdcall?为什么fastcall比stdcall慢?
【发布时间】:2011-03-29 21:49:34
【问题描述】:

我发现了以下问题: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  

这看起来确实是一个很好的优化,但它根本不尊重调用约定,所以测试是不公平的。

【问题讨论】:

  • 呵呵,不要指望内联代码尊重调用约定。这是公平的,不打电话是内联的重点。
  • 大多数编译器都有don't inline flag
  • @Hans Passant 我关闭了内联,编译器仍然不遵守约定
  • Andrey,你可以尝试通过以下方式调用你的函数: template __declspec(noinline) F NOIL( F f ) { return f; } 例如。 x = NOIL(func)(x & 0xFF);那么,如果您使用完全优化进行编译,fastcall 会更快! (可能是因为他们叫fastall)
  • 2019 年的结果可能不会在这里。请参阅下面的帖子。

标签: c++ optimization


【解决方案1】:

__fastcall 是在 很久 之前介绍的。当时,Watcom C++ 在优化方面击败了微软,许多评论者认为它基于寄存器的调用约定是一个(可能的)原因。

Microsoft 通过添加 __fastcall 作为回应,从那以后他们一直保留它——但我认为他们所做的还远远不够能够说“我们也有一个基于寄存器的调用约定。 .." 他们的偏好(尤其是自从 32 位迁移以来)似乎是__stdcall。他们在改进代码生成方面投入了大量工作,但(显然)__fastcall 并没有那么多。使用片上缓存,在寄存器中传递内容的收益并不像当时那么大。

【讨论】:

  • Afaik fastcall 和 Borland 的等效“注册”源自英特尔的一些(686/ppro 时代?)abi 文档。
  • @MarcovandeVoort:在那种情况下(不是粗鲁,但是......)你显然只是不知道。 fastcall 自 286 时代就已存在。 Borland 从 Turbo C 1.0 开始就支持 register(当时,register 对最多两个变量有意义,分别分配在 sidi 中)。
【解决方案2】:

您的微基准会产生不相关的结果。 __fastcall 与 SSE 指令有特定用途(参见 XNAMath),clock() 甚至远不是一个适合基准测试的计时器,__fastcall 也适用于多个平台,如安腾和其他一些平台,不仅适用于 x86,此外,除了printf 语句之外,您的整个程序都可以有效地优化到什么都没有,使得__fastcall__stdcall 的相对性能非常非常无关紧要。

最后,你忘记了很多事情都是按照它们原来的方式完成的主要原因——遗留问题。 __fastcall 在编译器内联变得像今天这样激进和有效之前可能已经很重要了,并且没有编译器会删除 __fastcall 因为会有依赖它的程序。这使得__fastcall 成为现实。

【讨论】:

  • @DeadMG 关于安腾:“在安腾处理器系列 (IPF) 和 AMD64 机器上,__fastcall 被编译器接受并忽略”。 msdn.microsoft.com/en-us/library/6xa169sk(v=VS.100).aspx
  • @Andrey: 答:__fastcall 可以将 SSE 原语(128 位值)存储在 SSE 寄存器中,而不是将它们压入堆栈。 (因此,如果使用 SSE 内在函数,__fastcall 开关影响的不仅仅是积分寄存器) B. clock 对于基准测试不需要准确,尤其是在涉及多个内核的情况下。基本上,诸如上下文切换之类的事情会因为clock 而对您不利,但对于QueryPerformanceCounter 这样的正确工具则不会。 (即基准测试应使用特定于平台的工具)
  • @Billy ONeal A:我在 MSDN 中没有找到任何参考,你有吗? B:clock 不适合微基准测试。当涉及到调用之间的几秒钟时,上下文切换噪音的影响可以忽略不计。
  • @Andrey:我以为我有参考,但我现在找不到。抱歉:(请接受我所说的话。
  • @Andrey:我无法预测x 的值,但编译器可以。
【解决方案3】:

几个原因

  1. 至少在大多数体面的 x86 实现中,寄存器重命名是有效的 - 看起来通过使用寄存器而不是内存来节省的工作可能在硬件级别上没有任何作用。
  2. 当然,您可以使用 __fastcall 节省一些堆栈移动工作,但您会减少可在函数中使用的寄存器数量,而无需修改堆栈。

大多数情况下__fastcall 会更快,该函数足够简单,可以在任何情况下内联,这意味着它在实际软件中真的无关紧要。 (这也是__fastcall不常用的主要原因之一)

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

【讨论】:

  • "Anon 的回答有什么问题?"除了它是错误的之外,没有什么。我展示了在编译的代码中没有保存任何内容(内存操作),而且速度并不快。值通过寄存器传递,但由于某种原因它被复制到内存中。净利润为零。
  • 我同意你的说法。 __fastcall 理论上只在函数简单的情况下是合理的,但是经过硬优化编译器可以自己处理。
  • 我不明白的是,如果__fastcall没有任何价值,它存在的原因是什么?
  • @Andrey:我怀疑这是因为优化器不够聪明,因为没有人使用它:P
  • @Andrey:因为其他一些编译器(例如 Borland)将其用作可能的(有时是默认的)选项——MSVC++ 和 GCC 需要能够在其他 DLL 中调用此类代码。此外,使用该开关,以前版本的编译器可能会更快。现在我们有了更好的优化器 :)
【解决方案4】:

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% 以上。

【讨论】:

  • 如果我启用完全优化并禁用内联,这两个函数都会编译到同一个程序集。关于extern:“默认情况下,文件范围内的变量和函数声明是外部的。” msdn.microsoft.com/en-us/library/0603949d(VS.80).aspx所以没意义
  • extern 关键字可能是默认的,但是如果将定义放在不同的源文件中,它会产生相当大的影响。如果您使用内联 disabled 获得相同的程序集,我怀疑 MSVC 忽略了 disable inlining 标志
【解决方案5】:

我用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),在这些架构中,在寄存器中传递参数也是默认值。

【讨论】:

  • Visual Studio 2013 为 x64 添加了 __vectorcall 约定,因此您的最后一条语句不再正确。en.wikipedia.org/wiki/…
  • 谢谢,我已经修改了答案以反映这一点。
【解决方案6】:

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

另一方面,在 x86-64 等更多“寄存器丰富”的架构上,基于寄存器的调用约定(与旧的 fastcall 不完全相同,但概念相同)是规范,并且被广泛使用。换句话说,一旦我们摆脱了一些像 x86 这样的命名寄存器架构,转向具有更多寄存器空间的架构,fastcall 又大行其道,成为了今天的默认方式,也是唯一真正使用的方式。

【讨论】:

    【解决方案7】:

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

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

    除非您专门做一些技巧,例如将函数声明为“易失性”和/或将测试函数包装在关闭某些优化的 pragma 中。

    B) 自大约 2010 年以来,最新一代的台式机 CPU(此处的假设)有了很大改进。它们更擅长缓存堆栈,内存对齐不太重要,等等。

    但不要相信我的话。在反编译器(IDA Pro、MSVC 调试器等)中加载您的可执行文件并寻找自己(学习的好方法)。

    现在,看看大型 32 位应用程序的性能如何,将会很有趣。例如,使用最新的开源 DOOM 游戏版本并使用 stdcall 和 _fastcall 进行构建,并寻找帧率差异。并从它拥有的任何内置性能报告功能等中获取指标。

    【讨论】:

      【解决方案8】:

      __fastcall 似乎并没有真正表明它会更快。似乎您所做的只是在调用函数之前将第一个 few 变量移动到寄存器中。这很可能会使您的函数调用变慢,因为它必须首先将变量移动到这些寄存器中。关于Fast Call 到底是什么以及它是如何实现的,维基百科有一篇很好的文章。

      【讨论】:

      • 那么答案是 __fastcall 没用吗? Wiki 说编译器通过寄存器传递值 - true 并且显示有问题,但没有从中获利,这让我觉得 __fastcall 没用。
      • 不完全正确;如果您传入了 1-2 个元素并且您正在它们之间进行操作(假设没有别的),您可能会在函数中看到加速,因为这两个元素已经存在于寄存器中。请注意 MIGHT 这个词,将完全取决于编译器以及它如何生成程序集。除此之外,我同意比利的观点;它并没有被太多使用,因为在极少数情况下您只对传入的变量进行操作。尝试将 i 更改为指针并执行 *i + 5 (不返回)。这样就不必重复返回。
      • “因为这两个元素都已经存在于寄存器中”由于某些原因,MSVC 不使用这个有用的假设。它将它从寄存器复制到函数内部的堆栈中,并从内存中使用它,从而使通过寄存器传递毫无用处。
      猜你喜欢
      • 2011-03-23
      • 1970-01-01
      • 2019-12-18
      • 2015-10-04
      • 2012-09-16
      • 2016-10-06
      • 2020-11-27
      • 2011-10-19
      • 2020-12-24
      相关资源
      最近更新 更多