首页 > 解决方案 > 为什么编译器不优化琐碎的包装函数指针?

问题描述

考虑以下代码片段

#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,

这会导致稍后出现call+ jmp,而不是call让编译器放置一个指向 the 的指针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;
}

gcc 8.2 通过将指向包装器的指针扔掉并&calculate直接存储在其位置(https://gcc.godbolt.org/z/nMIBeo)成功地优化了此代码。但是,根据注释更改行(即手动执行部分相同优化)会破坏魔术并导致无意义的结果jmp

标签: c++gccassemblycompiler-optimization

解决方案


您似乎建议&calculate1应该将其存储在 vector 而不是&wrapper1. 通常这是不可能的:稍后的代码可能会尝试将存储的指针&calculate1与它进行比较,并且必须比较为假。

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

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


推荐阅读