为什么 gcc 不能确定可以确定的内联函数指针?

Why gcc can't inline function pointers that can be determined?

本文关键字:函数 指针 gcc 不能 为什么      更新时间:2023-10-16

以下程序在带有-O3的centos上根据gcc 4.6.2编译:

#include <iostream>
#include <vector>
#include <algorithm>
#include <ctime>
using namespace std;
template <typename T>
class F {
public:
     typedef void (T::*Func)();
     F(Func f) : f_(f) {}
     void operator()(T& t) {
         (t.*f_)();
     }
private:
     Func f_;
};
struct X {
    X() : x_(0) {}
    void f(){
        ++x_;
    }
    int x_;
};
int main()
{
     const int N = 100000000;
     vector<X> xv(N);
     auto begin = clock();
     for_each (xv.begin(), xv.end(), F<X>(&X::f));
     auto end = clock();
     cout << end - begin << endl;
}

objdump -D显示为循环生成的代码为:

  40097c:       e8 57 fe ff ff          callq  4007d8 <clock@plt>
  400981:       49 89 c5                mov    %rax,%r13
  400984:       0f 1f 40 00             nopl   0x0(%rax)
  400988:       48 89 ef                mov    %rbp,%rdi
  40098b:       48 83 c5 04             add    $0x4,%rbp
  40098f:       e8 8c ff ff ff          callq  400920 <_ZN1X1fEv>
  400994:       4c 39 e5                cmp    %r12,%rbp
  400997:       75 ef                   jne    400988 <main+0x48>
  400999:       e8 3a fe ff ff          callq  4007d8 <clock@plt>

显然,gcc没有内联函数。为什么gcc不能进行这种优化?是否有任何编译器标志可以使gcc进行所需的优化?

我认为GCC试图优化整个main函数,但失败了(大量间接调用全局函数为xv分配/释放内存,获取定时器值、输入/输出等)。因此,您可以尝试将代码拆分为两个(或更多)独立的部分,如下所示:

inline
void foobar(vector<X>& xv)
{
  for_each (xv.begin(), xv.end(), F<X>(&X::f));
}
int main()
{
  const int N = 100000000;
  vector<X> xv(N);
  auto begin = clock();
  foobar(xv);
  auto end = clock();
  cout << end - begin << endl;
}

所以,现在我们有了和以前一样"等价"的代码,但GCC的优化器现在有了更容易完成的任务。我现在没有在汇编程序列表中看到任何对ZN1X1fEv的调用。

关于这方面的一些好的阅读材料是ScottAdamsMeyers的Effective C++(第三版)第30项:理解内联的来龙去脉,他声称对函数指针的调用永远不会内联。第三版发布于2008年,我确实能够从2011年(可能是2010年)发布的gcc 4.6开始,通过编译时常量指针来实现gcc到内联函数的调用。然而,这是用C编写的,非常棘手。在一个场景中,我必须在调用函数__attribute__((flatten))内联调用之前声明它(在这种情况下,我将函数指针作为结构的成员传递,然后我将其指针传递给内联函数,该函数将通过内联的指针进行函数调用)。

简而言之,不,这不是gcc的bug,但这并不意味着gcc(和/或其他编译器)有朝一日可能无法内联它。但我认为,真正的问题是,你不了解这里到底发生了什么。为了理解这一点,你必须像一个汇编程序员或编译器程序员一样思考。

您正在传递一个类型为F<X>的对象,并使用指向另一个类的成员函数的指针对其进行初始化。您还没有将实例F<X>对象声明为常量,它是Func f_成员,也没有将void F::operator()(T& t)成员声明为常量。在C++语言级别,编译器必须将其视为非常量。这仍然不意味着它以后在优化阶段不能确定你的函数指针没有改变,但你在这一点上让它变得非常困难。但至少它是本地的。如果您的F<X>对象是全局的,并且没有声明为static,那么它将完全禁止将其视为常量。

希望您这样做是在函数指针内联的练习中,而不是作为间接寻址的真正解决方案。当你想让C++产生真正的性能时,你需要使用类型的力量。具体来说,当我将模板参数声明为成员函数指针时,它不仅仅是一个常量,它也是类型的一部分。我从未见过这种技术生成函数调用的情况。

#include <iostream>
#include <vector>
#include <algorithm>
#include <ctime>
using namespace std;
template <typename T, void (T::*f_)()>
class F {
public:
     void operator()(T& t) {
         (t.*f_)();
     }
};
struct X {
    X() : x_(0) {}
    void f(){
        ++x_;
    }
    int x_;
};
int __attribute__((flatten)) main()
{
     const int N = 100000000;
     vector<X> xv(N);
     auto begin = clock();
     for_each (xv.begin(), xv.end(), F<X, &X::f>());
     auto end = clock();
     cout << end - begin << endl;
}

您可以将inline __attribute__((__always_inline__))添加到函数中,并将-Winline标志添加到编译器中,因此当编译器无法内联函数时,您会注意到这一点。

不幸的是,属性不会使函数内联,并且Winline不会发出警报。取消键入4.8。但是!!!从4.9开始,这个问题似乎已经解决了!

所以,获取您的gcc 4.9,添加always_inline标志,将优化器设置为-O3级别。快乐吧!

证明:http://goo.gl/kkuXzb