何时使用 `__fastcall` 校准约定

When to use `__fastcall` caling convention

我们有很多用 C++ 编写的基于 VCL 的应用程序。所有 VCL 方法(在 __published class 修饰符下都需要 __fastcall 调用约定。但是,无论出于何种原因,开发人员一直在向其他非 VCL 函数添加 __fastcallprivateprotectedpublic

基于 this article,这对我来说毫无意义,因为它不必要地使代码复杂化,甚至可能会影响性能(虽然可能可以忽略不计)。尽管如此,在建议我们在某些地方删除它之后,我被告知我们一直都是这样做的,所以要保持一致,这只是一个风格问题。我认为如果没有必要,它实际上会让人们感到困惑,所以这是不好的做法。

我的问题是,什么时候使用 __fastcall 调用约定合适?

支持全程序优化(又名link-时间代码生成)的良好优化编译器不关心内部函数的调用约定*。在这种情况下,它将使用任何调用约定 fastest/best,包括发明自定义调用约定或完全内联函数。

调用约定唯一重要的是构成 public API 一部分的函数。在那种情况下,__fastcall 可能是一个糟糕的选择。使用更标准的调用约定,如 __cdecl__stdcall,得到 Windows 工具链的广泛支持。 __fastcall 是互操作性特别糟糕的选择,因为它从未标准化,因此不同供应商的实现方式不同。当您尝试将 DLL 与使用不同工具链编译的应用程序一起使用时,这将成为一场噩梦,更不用说使用不同的语言了。

当然,当您使用被记录为需要 __fastcall 约定的 VCL API 时除外。例如,文档说 VCL 类 的成员函数使用 __fastcall 约定,因此您需要在所有覆盖中使用相同的调用约定。

或者当您需要清理调用者时,例如,以支持可变参数。那么你需要__cdecl.

如果你确实想对内部函数使用特定的调用约定(即那些不属于 public API 的函数),你应该更愿意指定 globally 带有编译器开关。然后,这将指定调用约定用于其原型未明确覆盖它的所有函数。这有几个优点。首先,它避免了一堆调用约定样板代码使您的代码混乱。其次,它允许您稍后轻松进行更改(例如,如果分析显示您最初选择的调用约定是优化器无法解决的瓶颈)。

有趣的是,__stdcall 优于 __cdecl 因为二进制大小的减少,这是由于被调用者而不是调用者调整堆栈(并且被调用者比调用者少)来电者),但正如您 link 编辑的文章提到的那样,__fastcall 可能并不总是比 __stdcall 快。这篇文章没有涉及任何技术细节,但问题基本上是 32 位 x86 上可用的寄存器数量极其有限。在寄存器中而不是在堆栈中传递值通常是性能上的胜利,但在某些情况下当函数很大并且用完寄存器时可能会变得悲观,迫使它将参数溢出回堆栈,做双重工作(这引起速度惩罚)并进一步膨胀代码(引起缓存惩罚,并间接导致速度惩罚)。在值 已经 在堆栈上,但需要移动到寄存器中以进行函数调用的情况下,这也是一种悲观,阻碍了两个地方的优化潜力。

请注意,当您开始针对 64 位 x86 架构时,这一切都变得无关紧要。调用约定最终在所有 Windows 应用程序中标准化,与供应商无关。 x64 调用约定有点类似于 __fastcall,但在那里工作得更好,因为可用寄存器的数量更多。优化器不需要像在 x86-32 上那样经历那么多的扭曲来释放用于传递参数的寄存器。


* 请注意,当我在这里说 "internal" 函数时,我指的不是特定的访问修饰符,而是单个编译器中的函数 and/or 那些永远不会被外部代码调用的。