编译器为何不优化琐碎的包装函数指针?

时间:2019-02-07 21:04:25

标签: c++ gcc assembly compiler-optimization

请考虑以下代码段

#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行的程序集here

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

随后会在call + jmp中产生结果,而不是call,而是让编译器将指针指向calculate1

请注意,我专门要求使用无内联的calculate函数进行说明;不使用noinline进行操作会导致另一种非优化的情况,其中编译器将生成两个相同的函数以由指针调用(因此仍然不会以不同的方式进行优化)。

我在这里想念什么?除了手动插入正确的函数(没有包装器)以外,还有什么方法可以指导编译器?

编辑1。遵循注释中的建议,here is a disassembly的所有函数均声明为静态,其结果完全相同(call + jmp而非{ {1}}。

编辑2。相同模式的简单示例:

call

gcc 8.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; } 直接存储在其位置(https://gcc.godbolt.org/z/nMIBeo)中,成功地优化了此代码。但是,根据注释更改行(即手动执行部分优化操作)会破坏魔术效果,并导致毫无意义的&calculate

2 个答案:

答案 0 :(得分:4)

您似乎建议将&calculate1而不是&wrapper1存储在向量中。通常这是不可能的:以后的代码可能会尝试将存储的指针与&calculate1进行比较,并且必须比较false。

我进一步假设您的建议是,编译器可能会尝试进行一些静态分析,并确定从不将向量中的函数指针值与其他函数指针进行相等性比较,实际上,其他任何操作都没有完成在向量元素上会产生可观察到的行为变化;因此,在此确切程序中,它可以存储&calculate1

通常,“为什么编译器不执行某些特定的优化”的答案是没有人想到并实现该想法。另一个常见的原因是,在通常情况下,涉及的静态分析非常困难,并且可能导致编译速度变慢,而在无法保证分析成功的实际程序中则无益。

答案 1 :(得分:0)

您在这里做了很多假设。首先,您的语法。其次,在情人眼中,编译器是完美的,可以抓住一切。现实情况是,很容易找到并手动优化编译器输出,编写小型函数来使您熟悉的编译器跳闸或编写适当大小的应用程序并不难,并且会有很多地方可以处理调。这都是已知的和预期的。然后就可以得出我的机器在哪儿比在哪儿快的信息,所以应该代替这些指令。

gcc并不是性能的出色编译器,在某些目标上,许多主要版本的性能都越来越差。它做的很好,胜过更好,它处理许多具有共同的中间语言和后端的预处理器/语言。一些后端获得更好的前后优化应用,而另一些则只是挂在后面。回来时,我曾经关心过许多其他编译器,它们可能会产生可以轻易胜过gcc的代码。

这些主要是付费编译器。超过了个人支付的二手车价格,有时每年都会重复发生。

gcc可以优化的东西简直是惊人的,而且有时它完全朝错误的方向发展。叮当声同样,通常他们做相似的工作,产出相似,有时做一些令人印象深刻的事情,有时甚至变成杂草。现在,我发现操纵优化器以使其胜任或劣等事情更有趣,而不用担心为什么它没有按照我在特定场合下应该做的事情进行操作。如果我更快地需要该代码,则将编译后的输出进行手工修复,然后将其用作汇编函数。完成。

您可以用gcc买到所需要的东西,如果您要深入研究它的肠子,您会发现它几乎没有用胶带和捆扎线固定在一起(llvm赶上了)。但是,对于免费工具而言,它的作用简直令人赞叹,它是如此广泛地使用,您几乎可以在任何地方获得免费支持。不幸的是,人们进入了这样的时代,因为gcc以某种方式解释语言,这正是该语言的定义方式,而不幸的是,这并非遥不可及。但是很多人没有尝试其他编译器来了解定义的实现真正意味着什么。

最后一个也是最重要的一个开放源代码,如果您想“修复”优化……那么就来做吧……自己保留该修复或发布或尝试将其推向上游。