避免std :: function的开销

时间:2019-04-26 09:53:33

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

我想对(自定义)单链接列表中的元素运行一组操作。遍历链表并运行操作的代码很简单,但重复性很强,如果到处复制/粘贴,可能会出错。性能和仔细的内存分配在我的程序中很重要,因此我想避免不必要的开销。

我想编写一个包装程序以包含重复代码并封装将在链表的每个元素上进行的操作。由于操作中发生的功能各不相同,因此我需要捕获必须提供给操作的多个变量(在实际代码中),因此我研究了使用std::function。在此示例代码中完成的实际计算在这里毫无意义。

#include <iostream>
#include <memory>

struct Foo
{
  explicit Foo(int num) : variable(num) {}
  int variable;
  std::unique_ptr<Foo> next;
};

void doStuff(Foo& foo, std::function<void(Foo&)> operation)
{
  Foo* fooPtr = &foo;
  do
  {
    operation(*fooPtr);
  } while (fooPtr->next && (fooPtr = fooPtr->next.get()));
}

int main(int argc, char** argv)
{
  int val = 7;
  Foo first(4);
  first.next = std::make_unique<Foo>(5);
  first.next->next = std::make_unique<Foo>(6);
#ifdef USE_FUNC
  for (long i = 0; i < 100000000; ++i)
  {
    doStuff(first, [&](Foo& foo){ foo.variable += val + i; /*Other, more complex functionality here */ });
  }
  doStuff(first, [&](Foo& foo){ std::cout << foo.variable << std::endl; /*Other, more complex and different functionality here */ });
#else
  for (long i = 0; i < 100000000; ++i)
  {
    Foo* fooPtr = &first;
    do
    {
      fooPtr->variable += val + i;
    } while (fooPtr->next && (fooPtr = fooPtr->next.get()));
  }
  Foo* fooPtr = &first;
  do
  {
    std::cout << fooPtr->variable << std::endl;
  } while (fooPtr->next && (fooPtr = fooPtr->next.get()));
#endif
}

如果以以下身份运行:

g++ test.cpp -O3 -Wall -o mytest && time ./mytest
1587459716
1587459717
1587459718

real    0m0.252s
user    0m0.250s
sys 0m0.001s

运行方式为:

g++ test.cpp -O3 -Wall -DUSE_FUNC -o mytest && time ./mytest 
1587459716
1587459717
1587459718

real    0m0.834s
user    0m0.831s
sys 0m0.001s

这些时序在多次运行中相当一致,并且在使用std::function时显示4倍的乘数。有什么更好的方法可以做我想做的事?

2 个答案:

答案 0 :(得分:6)

使用模板:

template<typename T>
void doStuff(Foo& foo, T const& operation)

对我来说,这给出了:

mvine@xxx:~/mikeytemp$ g++ test.cpp -O3 -DUSE_FUNC -std=c++14 -Wall -o mytest && time ./mytest
1587459716
1587459717
1587459718

real    0m0.534s
user    0m0.529s
sys     0m0.005s
mvine@xxx:~/mikeytemp$ g++ test.cpp -O3 -std=c++14 -Wall -o mytest && time ./mytest
1587459716
1587459717
1587459718

real    0m0.583s
user    0m0.583s
sys     0m0.000s

答案 1 :(得分:6)

功能对象的重量很重,但可以用于有效载荷很大(> 10000个周期)或需要多态的情况,例如在通用作业调度程序中。

它们需要包含您的可调用对象的副本,并处理它可能引发的任何异常。

使用模板会使您更接近金属,因为结果代码经常内联。

template <typename Func>
void doStuff(Foo& foo, Func operation)
{
  Foo* fooPtr = &foo;
  do
  {
    operation(*fooPtr);
  } while (fooPtr->next && (fooPtr = fooPtr->next.get()));
}

编译器将能够查看您的函数并消除冗余。

在Golbolt上,您的内循环变为

.LBB0_6:                                # =>This Loop Header: Depth=1
        lea     edx, [rax + 7]
        mov     rsi, rcx
.LBB0_7:                                #   Parent Loop BB0_6 Depth=1
        add     dword ptr [rsi], edx
        mov     rsi, qword ptr [rsi + 8]
        test    rsi, rsi
        jne     .LBB0_7
        mov     esi, eax
        or      esi, 1
        add     esi, 7
        mov     rdx, rcx
.LBB0_9:                                #   Parent Loop BB0_6 Depth=1
        add     dword ptr [rdx], esi
        mov     rdx, qword ptr [rdx + 8]
        test    rdx, rdx
        jne     .LBB0_9
        add     rax, 2
        cmp     rax, 100000000
        jne     .LBB0_6

作为奖励,如果您不使用链接列表,则循环可能会完全消失。