对于实际性能敏感的情况,比如在循环中多次调用函数,性能可能完全相同。这可能听起来很奇怪,因为人们习惯将 C 代码视为由一个抽象的 C 机器执行的,这个“机器语言”与 C 语言本身非常相似。在这种情况下,“默认情况下”,与直接调用相比,间接调用函数的确要慢一些,因为它需要形式上进行额外的内存访问以确定调用目标。
然而,在现实生活中,代码是由真正的计算机执行并由优化编译器编译的,优化编译器具有对底层机器架构的相当好的了解,从而帮助它为那个具体的机器生成最优代码。在许多平台上,执行循环中的函数调用的最有效方法实际上可能导致直接和间接调用的相同代码,从而导致两者具有相同的性能。
例如,考虑 x86 平台。如果我们将直接调用和间接调用“字面上”转换为机器代码,我们可能会得到像这样的结果:
// Direct call
do-it-many-times
call 0x12345678
// Indirect call
do-it-many-times
call dword ptr [0x67890ABC]
前者在机器指令中使用立即操作数,通常比后者更快,因为后者需要从某个独立的内存位置读取数据。
此时让我们记住,x86架构实际上还有一种方法可以向call
指令提供操作数。它是在寄存器中提供目标地址。这种格式非常重要的一点是,它通常比以上两种方式都要快。这对我们意味着什么?这意味着一个优化良好的编译器必须并且将会利用这一事实。为了实现上述循环,编译器将尝试在两种情况下都使用通过寄存器调用的方式。如果成功,最终代码可能如下所示:
// Direct call
mov eax, 0x12345678
do-it-many-times
call eax
// Indirect call
mov eax, dword ptr [0x67890ABC]
do-it-many-times
call eax
请注意,现在最重要的部分——循环体中实际调用的部分——在两种情况下都是精确相同的。不用说,性能将是几乎相同的。
甚至可以说,无论听起来多么奇怪,在这个平台上,一个直接调用(使用
call
中的立即操作数进行调用)比一个间接调用更慢,只要间接调用的操作数以寄存器的形式提供(而不是存储在内存中)。
当然,在一般情况下,整个过程并不那么简单。编译器必须处理有限的寄存器可用性、别名问题等。但是,在您的示例中这样简单的情况(甚至在更复杂的情况下),好的编译器都会执行以上优化,并完全消除循环直接调用和循环间接调用之间的任何性能差异。在C++中调用虚函数时,这种优化特别有效,因为在典型实现中涉及的指针完全由编译器控制,使其完全了解别名图片和其他相关内容。
当然,总会有一个问题,那就是你的编译器是否足够聪明以优化这样的事情...