【问题标题】:Have the ideas behind the Fast Delegate (et al) been used to optimize std::function?Fast Delegate (et al) 背后的想法是否已用于优化 std::function?
【发布时间】:2012-06-22 23:37:02
【问题描述】:

已经提出了 C++“委托”的建议,其开销低于 boost::function

这些想法是否已用于实现std::function,从而获得比boost::function 更好的性能?有没有人比较过std::functionboost::function 的性能?

我想专门针对英特尔 64 位架构上的 GCC 编译器和 libstdc++ 了解这一点,但欢迎提供有关其他编译器的信息(例如 Clang)。

【问题讨论】:

  • std::function 是一个接口,而不是一个实现。如果您想专门询问 VC++ 的 stdlib、libstdc++ 或 libc++,那么这是一个有效的问题,但是您的问题过于宽泛
  • @ildjarn:阅读问题的最后一句话。他在询问具体的实现,尤其是关于 libstdc++。
  • @ildjarn:我不明白提及有关其他实现的信息如何会破坏一个原本有效的问题。如果需要,您可以随时忽略该部分并专门回答有关 libstdc++ 的问题。
  • @abarnert :我认为这将是 C++ 程序员普遍感兴趣的问题。我认为这个问题属于 StackOverflow,即使我可以自己找到答案。 StackOverflow 不仅仅是一个“修复我的错误”网站。 :-) 如果没有人知道答案,我将亲自调查并发布我的调查结果,以造福于社区。​​span>
  • 通过快速检查我拥有的 libstdc++ 和 libc++ 版本(两者都不是完全最新的),它们似乎都使用分配器为成员函数指针创建存储空间,并且我认为所有这些技巧的关键在于避免这种分配。

标签: c++ performance boost c++11 std-function


【解决方案1】:

在 libstdc++ 的std::function 中,我们使用大小合适并对齐的联合类型来存储指针、函数指针或指向成员函数的指针。我们避免为任何可以以该大小和对齐方式存储的函数对象进行堆分配,仅当它是“位置不变”时

/**
 *  Trait identifying "location-invariant" types, meaning that the
 *  address of the object (or any of its members) will not escape.
 *  Also implies a trivial copy constructor and assignment operator.
 */

代码基于std::tr1::function 实现,该部分没有显着变化。我认为这可以使用std::aligned_storage 来简化,并且可以通过专门化特征来改进,以便将更多类型识别为位置不变。

调用目标对象是在没有任何虚函数调用的情况下完成的,类型擦除是通过在std::function 中存储一个函数指针来完成的,它是函数模板特化的地址。所有操作都是通过存储的指针调用该函数模板并传入一个枚举来确定它被要求执行的操作。这意味着不需要 vtable,并且只需要在对象中存储一个函数指针。

这个设计是由原boost::function作者贡献的,我相信它接近于boost实现。有关基本原理,请参阅 Boost.Function 的 Performance 文档。这意味着 GCC 的 std::function 不太可能比 boost::function 快,因为它是同一个人的类似设计。

注意我们的std::function 还不支持使用分配器进行构造,它需要做的任何分配都将使用new 完成。


为了回应 Emile 的评论,该评论表示希望避免为 std::function 分配堆分配,该 std::function 包含指向成员函数和对象的指针,这里有一个小技巧(但你没有从我这里听到; -)

struct A {
  int i = 0;
  int foo() const { return 0; }
};

struct InvokeA
{
  int operator()() const { return a->foo(); }
  A* a;
};

namespace std
{
  template<> struct __is_location_invariant<InvokeA>
  { static const bool value = true; };
}

int main()
{
  A a;
  InvokeA inv{ &a };

  std::function<int()> f2(inv);

  return f2();
}

诀窍在于InvokeA 足够小以适合function 的小对象缓冲区,并且 trait 特化表明它可以安全地存储在其中,因此function 直接保存该对象的副本,不在堆上。这要求a 一直存在,只要指向它的指针仍然存在,但如果function 的目标是bind(&amp;A::foo, &amp;a),无论如何都会如此。

【讨论】:

  • 哇,马嘴里的海峡!如果我从这个表达式std::bind(&amp;Object::memberFunction, objectInstance) 构造一个std::function,那会构成“位置不变”吗?
  • 不,这不是位置不变的,因为绑定表达式的结果将包含指向成员函数的指针对象的副本!我计划增加联合体的大小,这样它就可以保存一个指向成员的指针和一个指针,这样bind(&amp;O::f, &amp;o) 就可以了,但这会比较棘手。
  • 是的,有一个 switch 语句。对于给定的调用,使用的枚举器在编译时是已知的。通过函数指针调用管理器函数不会比虚拟调用快,但这不是唯一的开销。避免使用 vtable 是有益的,并且只有 一个 函数来调用 all 操作可能比拥有多个虚拟函数更有优势。 TBH 我没有对它进行基准测试,我假设原来的 boost::function 作者知道他在做什么;)
  • 我认为通过std::function 调用绑定到对象实例的成员函数是一个非常常见的用例,所以我很高兴看到std::function 实现避免了堆分配情况。
  • 我刚刚提交了一个更改,使得上面的 __is_location_invariant 特化变得不必要,所以 GCC 5.0 不会为 InvokeA 分配内存
【解决方案2】:

正如 cmets 中所指出的,std::function 只是一个接口,不同的实现可能会做不同的事情,但值得注意的是,标准确实对这件事有话要说。从 20.8.11.2.1/5 开始(看起来更像是一个 IP 地址,而不是标准的一部分):

注意: 鼓励实现避免使用动态 为小的可调用对象分配的内存,例如,其中 f 的 target 是一个对象,它只包含一个对象的指针或引用 和一个成员函数指针。 ——尾注

这是标准鼓励实现者采用“小函数优化”的方式,其动机是受引用的关于委托的文章的启发。 (文章本身实际上并没有讨论 .NET 意义上的委托。相反,他们使用术语“委托”来表示绑定的成员函数。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多