【问题标题】:Is there a performance penalty in accessing the Windows API through Delphi?通过 Delphi 访问 Windows API 是否存在性能损失?
【发布时间】:2014-04-01 00:29:58
【问题描述】:

几年前,我曾在 C++ 中使用过旧的 Win32 API 进行编程,最近我参与了 Delphi 的开发。我立即认出了许多来自 Windows API 的函数(例如,CreateThreadCreateWindowEx 等)。

我发现 Embarcadero 的文档不完整(至少可以这么说),并且通常参考 Microsoft 网站获取文档。在哪里,我可以补充一下,所有函数都是用 C 定义的,这让非 C 的人很难(但对我来说更容易)。

我想知道的是 - 鉴于 Delphi 函数签名与 Microsoft 提供的 C 函数签名相同 - 对 Delphi Windows API 函数的调用立即调用 Windows API 函数,还是调用一个相同的Delphi函数然后调用Windows API函数并返回结果,前者意味着与相应的C代码相比在性能上没有明显差异,而后者意味着某种性能罚款?

【问题讨论】:

  • 您可以在 Delphi 中按住 Ctrl 键并单击一个函数来查看它指向的位置。例如。大多数 WinAPI 函数都是直接从系统 DLL 调用这些函数
  • 真的吗?您担心 GUI 调用的性能?
  • 不,我不担心这个,尤其是因为它是直接调用 Windows 提供的 DLL。我正在做的是询问是否存在性能损失,部分原因是想更好地了解 Delphi 如何“在幕后”工作。
  • +1 用于搅动马蜂窝。

标签: c++ delphi winapi


【解决方案1】:

调用 Delphi Windows API 函数会立即调用 Windows API 函数吗?

不是。

为了争论,让我们考虑调用CloseHandle。这在Windows 单元中声明并使用external 实现。当您调用它时,实际上您确实调用了Windows 单元中名为CloseHandle 的函数。所以在伪汇编中它看起来像这样:

.... prepare parameters
CALL     Windows.CloseHandle

然后,Windows.CloseHandle 是这样实现的:

JMP      kernel32.CloseHandle

因此,与直接调用相比,有一个对 thunk 函数的调用,然后是一个跳转到 Win32 DLL 中。这被称为蹦床。

它可以以不同的方式实现。编译器可以发出代码直接调用 Win32 DLL。一些编译器会这样做。例如,MSVC 发出的此调用的等效 asm 将是:

CALL     DWORD PTR [__imp__CloseHandle@4]

这里,__imp__CloseHandle@4 是内存中某个位置的地址,它包含 Windows DLL 中的CloseHandle 的地址。加载器在加载时将CloseHandle的实际地址写入__imp__CloseHandle@4

哪个更有效率?没有分析就不可能肯定地说。但我相信,在极少数情况下,任何差异都会显着。

当然,也可以生成直接调用而没有间接调用的代码。这将涉及每次调用该函数时加载程序修补程序。然而,这可能是一个坏主意,因为它会导致大量加载时间修复,这将是启动时的性能问题。也就是说,它与需要在加载时重新定位的 DLL 几乎相同。无论如何,我知道没有采用此策略的工具链。

也许你关心的是这些函数是不是真正的Win32函数。或者它们周围是否有一层会改变含义。这些是真正的 Win32 函数。没有 Delphi 文档,因为这些是 Win32 函数。 MSDN 上的 Win32 文档是文档的权威来源。

正如许多人所说,调用 Win32 函数时没有中间层。因此,在这些意义上直接调用它们,即您的参数未经修改地传递给 API 函数。但是调用机制是间接的,因为它使用了蹦床。语义上没有区别。

【讨论】:

  • 这是library linking does occur 的实现细节。这是 Windows 加载其 .dll 的方式:在加载时,它解析符号名称并填充 JMP 表。然后代码中的各个调用引用这个 JMP 表。这允许仅映射内存中 .dll 的 JMP 表部分,而无需映射整个 .dll 代码,该代码保持不变。因此,Delphi 设计与原生应用程序一样高效
  • @David 我已经在之前的评论中回答了这些问题。但是,您关于“它可以让我更有效率”的断言可以解释,因为“高效”可能不仅仅是对 API 的简单调用。在我看来,Arnaud 正确地指出 Delphi 方式是最有效的方式(它还将重定位表的大小减少到每个调用的 API 函数一个,而不是每个调用一个),因此(IMO ) 你的说法“它可能更有效”是不正确的。
  • @DavidHeffernan 我不必这样做——这是不言而喻的(从某种意义上说,“高效”不仅适用于实际通话所用的时间)。通过拥有一个 JMP 表,加载程序可以加载一个包含 2.000.000 次 Windows API 调用的文件,并且只需要修补文件中的一个条目。通过在每次调用 API 函数时直接调用 API,加载程序将不得不修补文件中的 2.000.000 个位置,即使您会承认这比修补单个条目需要更长的时间。
  • 作为一个使用嵌入式系统工作的人,负载可能每月发生一次,并且“现代 CPU”很可能是基于 586 的(即 Quark),我非常倾向于同意大卫在这里。 “高效”的定义取决于上下文,如果我最关心的是执行速度,那么现在进行 CALL 而不是 JMP+CALL 对我来说比上周加载应用程序需要多长时间更重要. OP 问题专门询问了通话性能。大卫回答了这个问题。
  • @DavidHeffernan CALL DWORD PTR [__imp__CloseHandle@4] 是一个间接调用,就像CALL Windows.CloseHandle; JMP kernel32.CloseHandle。第二个可能更快,因为它不读取任何内存。唯一的直接调用可能是CALL kernel32.CloseHandle - 这可能不会比其他两个更快。
【解决方案2】:

阅读源代码。对CreateWindowEx 的调用在Windows.pas 单元中定义为对User32.DLLCreateWindowExW 函数的直接调用(来自XE5 的源代码——在所有支持的操作系统版本的Delphi 版本中都可以找到类似的定义):

function CreateWindowEx(dwExStyle: DWORD; lpClassName: LPCWSTR;
  lpWindowName: LPCWSTR; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer;
  hWndParent: HWND; hMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND;
  stdcall; external user32 name 'CreateWindowExW';

因此,您的具体问题的答案是否定的。没有性能损失。在 Delphi 中调用 WinAPI 函数不会影响性能。

【讨论】:

  • 你是对的。看起来签名只是为 DLL 提供符号解析:function CreateThread; external kernel32 name 'CreateThread'; 很高兴知道。
  • @DavidHeffernan 这个答案是正确的。 Delphi 设计与 Windows 下的本机应用程序一样高效。
  • @arnaud 不,不是。再读一遍答案。它声称调用外部声明的函数是“直接”调用。显然不是。你认为 call+jmp 是直接的吗?
  • @DavidHeffernan:你和 Ken 浪费了 3 打 cmets 挑剔 实施细节,这些细节与所提出的问题或所提供的答案无关。事实仍然存在,Delphi 静态链接到大多数 API 函数(一些 API 使用延迟加载,一些使用包装器来解决 OS 版本差异),所以谁关心静态调用 API 函数是否是一个 直接调用或IAT跳转调用。调用静态 API 函数,它最终会以任何一种方式调用系统 DLL。请先停止讨论,然后继续。
  • @DavidHeffernan Delphi 如此震撼以至于它的甲骨文被石头砸死...biblicalarchaeology.org/daily/ancient-cultures/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-29
  • 2018-04-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多