为什么编译器不会优化微不足道的包装函数指针?

4
考虑以下代码片段:
#include <vector>
#include <cstdlib>

void __attribute__ ((noinline)) calculate1(double& a, int x) { a += x; };
void __attribute__ ((noinline)) calculate2(double& a, int x) { a *= x; };
void wrapper1(double& a, int x) { calculate1(a, x); } 
void wrapper2(double& a, int x) { calculate2(a, x); } 

typedef void (*Func)(double&, int);

int main()
{
    std::vector<std::pair<double, Func>> pairs = {
        std::make_pair(0, (rand() % 2 ? &wrapper1 : &wrapper2)),
        std::make_pair(0, (rand() % 2 ? &wrapper1 : &wrapper2)),
    };

    for (auto& [a, wrapper] : pairs)
        (*wrapper)(a, 5);

    return pairs[0].first + pairs[1].first;
}

使用 -O3 优化后,最新的 gcc 和 clang 版本不会对指向包装器的指针进行优化,使其指向基础函数。请参见第22行的汇编代码 这里

mov     ebp, OFFSET FLAT:wrapper2(double&, int)   # tmp118,

这段代码的问题在于编译器将指针放在了calljmp之后,而不是直接把指针放在calculate1中。请注意,我特意要求没有内联calculate函数来说明这个问题;如果没有noinline,编译器会生成两个完全相同的函数来通过指针调用(所以仍然不会优化,只是使用了不同的方式)。我错过了什么?除了手动插入正确的函数(没有包装器)外,有没有任何方法可以引导编译器?编辑1。根据评论中的建议,这里有一个反汇编代码,其中所有函数都声明为静态,结果完全相同(call+jmp而不是call)。编辑2。同样模式的更简单的示例:
#include <vector>
#include <cstdlib>

typedef void (*Func)(double&, int);

static void __attribute__ ((noinline)) calculate(double& a, int x) { a += x; };
static void wrapper(double& a, int x) { calculate(a, x); } 

int main() {
    double a = 5.0;
    Func f;
    if (rand() % 2)
        f = &wrapper; // f = &calculate;
    else
        f = &wrapper;
    f(a, 0); 
    return 0;
}

通过将指针丢弃并直接存储&calculate,gcc 8.2成功优化了此代码(https://gcc.godbolt.org/z/nMIBeo)。然而,根据注释更改行(即手动执行相同的优化的一部分)会破坏魔法并导致无意义的jmp


评论不适合进行长时间的讨论;此对话已被移至聊天室 - Samuel Liew
2个回答

4
您似乎在建议将&wrapper1替换为&calculate1并存储在向量中。一般情况下,这是不可能的:后面的代码可能会尝试将存储的指针与&calculate1进行比较,而这必须返回false。
我进一步假设您的建议是编译器可能会尝试进行静态分析,并确定向量中的函数指针值永远不会与其他函数指针进行相等性比较,事实上,在向量元素上执行的任何操作都不会产生可观察行为的更改;因此,在这个确切的程序中它可以存储&calculate1
通常,“为什么编译器不执行某些特定的优化”这个问题的答案是没有人想到并实现了那个想法。另一个常见原因是所涉及的静态分析在一般情况下相当困难,可能会导致编译速度减慢,在无法保证分析成功的实际程序中也没有好处。

1
“后续代码可能尝试将存储的指针与&calculate1进行比较,这必须比较为false。” 这是从哪里得出的?实际上,“gcc”可以很好地进行优化:https://gcc.godbolt.org/z/2TkPgE。将其与https://gcc.godbolt.org/z/1QqlDF进行比较。 - random
1
请参考@random 在这里的讨论(这超出了评论的范围)。 - M.M
如果后续代码尝试将存储的指针与&calculate1进行比较,则必须比较错误。如果Tasking汽车软件链接器检测到不同目标文件中的两个函数相等,则只会将一个函数放入二进制文件中,并且两个函数将具有相同的地址。 - Martin Rosenau
@M.M 这个链接之前已经发布过了,我不确定它是否完全符合你所说的(来自得到赞同的答案:““或者两者表示相同的地址”似乎给予足够的余地,允许编译器将两个不同的函数别名,并且不要求指向不同函数的指针比较不等。”,尽管显然作者后来对此感到遗憾);无论如何,这都是无关紧要的,因为我已经为您提供了gcc优化包装器的反汇编链接。我发布的两个代码版本之间有什么区别,一个被gcc优化,另一个没有? - random
@随机 我不想重温这个讨论(但我的看法是标准不允许优化)。对于你最后的问题,请参见我的答案的最后一段。 - M.M

0

你在这里进行了很多假设。首先是你的语法。其次是编译器在使用者眼中是完美的,可以捕捉所有问题。事实上,很容易找到并手动优化编译器的输出,编写小函数来阻止您熟悉的编译器或编写体量适中的应用程序,这些都会有一些可以手动调整的地方。这是众所周知的和预期的。然后是个人观点的问题,在我的机器上,我的blah比blah更快,所以它应该产生这些指令。

gcc不是一个性能出色的编译器,在某些目标上,它已经在多个主要版本中变得更糟。它在处理多种预处理器/语言方面相当不错,具有通用中间件和多个后端。有些后端从前往后应用更好的优化,而其他后端只是紧跟进度。还有一些编译器可以产生可以轻松超越gcc的代码。

这些大多是付费编译器。超过个人支付的金额:二手车价格,有时是每年的重复性支出。

有些时候gcc可以优化一些令人惊叹的东西,但有时它会完全走向错误的方向。clang也是如此,它们经常做着类似的工作并产生类似的输出,有时会做出一些令人印象深刻的事情,有时则会走向歧途。现在我发现操纵优化器让它做好或坏的事情比担心为什么它在特定场合没有做我“认为”应该做的事情更有趣。如果我需要更快的代码,我会取得编译后的输出并手动修复它,将其用作汇编函数。

你用gcc得到你所付出的代价,如果你深入挖掘它的内部,你会发现它几乎是用胶带和铁丝勉强维持的(llvm正在追赶)。但对于一个免费的工具来说,它做得非常出色,它被广泛使用,你几乎可以在任何地方获得免费支持。不幸的是,我们已经进入了一个时代,人们认为因为gcc以某种方式解释语言,这就是语言的定义,但不幸的是这根本不是真的。但是很多人不会尝试其他编译器,以找出“实现定义”真正的含义。

最后也是最重要的,它是开源的,如果你想“修复”一个优化,那就去做吧。保留这个修复程序供自己使用、发布它或尝试将其推向上游。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接