【问题标题】:Limits to inlining function wrappers in C++C++ 中内联函数包装的限制
【发布时间】:2012-09-29 16:33:22
【问题描述】:

我的问题涉及 C++ 中函数包装器的内联优化应用,请考虑以下代码,WorkerStructure 对象是使用封装了一些功能块的函数包装器初始化的。然后在调用 WorkerStructure::doSomeWork 方法时使用函数包装器。

workerFunction对象封装的功能在WorkerStructure::doSomeWork方法上应用时会内联吗?显然如果功能定义在其他翻译单元中,workerFunction对象只封装了一个函数指针,有吗任何其他无法内联的情况?

当通过函数包装器传递在不同翻译单元中定义的 lambda 函数时,它是否等效于传递函数指针?

struct WorkerStructure
{
    WorkerStructure(std::function <bool(float)> &f):workerFunction(f) {}

    void doSomeWork(float inputValue)
    {
        if(workerFunction(inputValue))
        {
            //do some conditional operation
        }
    }
    std::function <bool(float)> workerFunction ;
};

【问题讨论】:

  • 这在很大程度上取决于编译器和优化标志。仅供参考,最近的 GCC(即 4.7)具有链接时优化能力(编译 链接gcc -flto -O2
  • 酷!,不知道 flto,我正在尝试很多 lambda 函数,我真的很喜欢他们给我的灵活性,但我不愿意接受任何性能下降,因此问题.
  • 我最近将我的大部分代码切换到在 gcc 4.7.0 中使用 lambda,而没有遇到性能损失(我基本上替换了 for() 在一些自酿的类似迭代器的对象上循环以访问复杂的带有for_each_obj(lambda)的数据结构,其中for_each_obj模板由数据结构提供,可以更好地优化循环逻辑)。
  • 但是,使用 lambdas 当然,意味着使用 std::function。后者很可能会导致性能下降。

标签: c++ optimization c++11 inlining


【解决方案1】:

std::function 的多态性本质上使得非常非常难以实际内联调用。因为std::function 可以记录任何可调用的实体;你将如何编写内联代码?

这有点像内联虚函数,通过基指针调用而没有其他可用信息(也就是在调用之前没有从派生到基指针的赋值,编译器可能会使用它来启用内联)。

大多数时候,std::function 是用一个 void* 指针和一个指向模板化函数的特化的函数指针来实现的,它执行实际的调用和强制转换等。当然有一些变体使用虚函数来做到这一点,而且它们更清楚为什么它非常困难。即使是链接时优化也无济于事,因为没关系,您已经拥有了可以在调用站点获得的所有信息(这并不多)。

这是std::function非常粗略版本,使用指向模板函数版本的指针,仅处理存储和调用方面(省略内存管理、复制、移动、重置、空间优化等.):

template<class Sig>
class function;

template<class R, class... Args>
class function<R(Args...)>{
  typedef R (*call_type)(void*, Args...);
  void* _obj;
  call_type _caller;

public:
  template<class F>
  function(F f)
    : _obj(new F(f))
    , _caller([](void* p, Args... args){ return (*static_cast<F*>(p))(args...); })
  {}

  R operator()(Args... args) const{
    return _caller(_obj, args...);
  }
};

Live example. 我认为很难检查_obj_caller 内部的实际内容以及调用function 的位置。

仅供参考,here's the version with virtual functions

【讨论】:

  • 令人高兴的是,似乎通过将 lambda 作为模板参数传递,内联被正确应用,我通过观察优化的反汇编目标代码验证了这一点。
  • @Gearoid:哦,那很酷。 :) 我想它只有在你调用 std::function 时才有效,但你在其中存储了一些东西。
【解决方案2】:

有趣的是,就在今天,我在 the mailing list 上询问了 Clang/LLVM 中虚函数的内联。 std::function 的动态特性使其本质上是一个虚拟调用,因为virtual 调用只不过是指向函数的指针。

以 LLVM 为例,让我们玩一下下面的程序:

#include <cstdio>

typedef void (*Function)();

void donothing() {}
void print() { printf("Hello World!"); }

Function get(int i) {
    if (i % 2 == 0) { return donothing; }
    return print;
}

int main() {
    Function f = get(0);
    f();
}

发出的主要函数:

define i32 @main() uwtable readnone {
  ret i32 0
}

因此,编译器能够了解选择了哪个函数(结合内联和常量传播),并进行了内联调用。

不幸的是,我在我的电子邮件中证明,通过虚拟表这是行不通的(优化器不知何故丢失了信息并且无法内联调用)。因此,虽然内联完全有可能通过std::function 工作,但它很可能不仅取决于编译器,还取决于您碰巧使用的std::function 的特定实现。恐怕您需要对您的应用程序进行试验。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-10
    • 2011-03-07
    • 2019-03-24
    • 2017-01-05
    相关资源
    最近更新 更多