由于使用函数指针代替 if 块而导致的效率损失

efficiency loss due to use of function pointer in place of if-block

假设我们有一个 Fortran 函数(例如数学优化算法)作为输入,另一个 Fortran 函数:

myOptimizer(func)

现在根据用户的选择,输入函数可以来自几个不同函数的列表。这个选择列表可以通过 if 块实现:

if (userChoice=='func1') then
    myOptimizer(func1)
elseif (userChoice=='func2') then
    myOptimizer(func2)
elseif (userChoice=='func3') then
    myOptimizer(func3)
end if

或者,我也可以定义函数指针,并将其写为,

if (userChoice=='func1') then
    func => func1
elseif (userChoice=='func2') then
    func => func2
elseif (userChoice=='func3') then
    func => func3
end if
myOptimizer(func)

根据我对带有 O2 标志的英特尔 Fortran 编译器 2017 的测试,第二个实现恰好慢了几个因素(比 if 块实现慢 4-5 倍)。从软件开发的角度来看,我非常喜欢第二种方法,因为它会产生更简洁、更清晰的代码,至少在我的问题中,那里有一个固定的工作流,工作流可能有不同的输入函数。然而,性能在这个问题中也同样重要。

这种间接函数调用导致的性能损失是否符合所有 Fortran 代码的预期?还是编译器相关的问题?有没有使用间接函数调用而不损失性能的解决方案? C/C++ 等其他语言怎么样?

这纯属猜测,基于编译器的一般工作方式以及可能解释 4-5 倍性能差异的原因。

在第一个版本中,也许编译器将 myOptimizer() 内联到每个调用站点,并将 func1func2func3 内联到优化器中,所以当它运行时没有实际的函数指针或函数调用发生。

间接函数调用并不比现代 x86 硬件上的常规函数​​调用昂贵多少。真正有害的是缺乏内联,尤其是对于 FP 代码。溢出/重新加载函数调用周围的所有浮点寄存器非常昂贵,尤其是在函数相当小的情况下。

即可能伤害您的是您的第二个版本说服编译器不要撤消间接寻址。在 C / C++ 中也是如此。

让你的编译器实现快速 asm 可能意味着你必须以第一种方式编写它,除非有一个你可以使用的配置文件引导的优化选项,它可能会让编译器意识到这是一个热点并且它是值得的更加努力地使用第二种方式编写的源代码。 (抱歉,我不使用 Fortran,我只知道英特尔 C/C++ 编译器的一些选项,通过查看它的 asm 输出与 http://gcc.godbolt.org/ 上的 gcc 和 clang)


要查看我的假设是否正确,请检查编译器生成的 asm。如果第一个版本实际上没有将函数指针传递给 myOptimizer 的独立定义,但第二个版本确实如此,那可能就是它的全部。

参见 for more about looking at compiler output. Matt Godbolt's CppCon2017 talk: “What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid” 是阅读编译器输出以及您可能想要阅读的原因的一个很好的介绍。