Obiphil
Obiphil

Reputation: 390

Avoiding the overheads of std::function

I want to run a set of operations over elements in a (custom) singly-linked list. The code to traverse the linked list and run the operations is simple, but repetitive and could be done wrong if copy/pasted everywhere. Performance & careful memory allocation is important in my program, so I want to avoid unnecessary overheads.

I want to write a wrapper to include the repetitive code and encapsulate the operations which are to take place on each element of the linked list. As the functions which take place within the operation vary I need to capture multiple variables (in the real code) which must be provided to the operation, so I looked at using std::function. The actual calculations done in this example code are meaningless here.

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

If run as:

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

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

Whereas if run as:

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

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

These timings are fairly consistent across multiple runs, and show a 4x multiplier when using std::function. Is there a better way I can do what I want to do?

Upvotes: 2

Views: 215

Answers (2)

Andy Thomason
Andy Thomason

Reputation: 328

Function objects are quite heavy weight, but have a use where the payload is quite large (>10000 cycles) or needs to be polymorphic such as in a generalised job scheduler.

They need to contain a copy of your callable object and handle any exceptions it might throw.

Using a template gets you much closer to the metal as the resulting code frequently gets inlined.

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

The compiler will be able to look inside your function and eliminate redundancy.

On Golbolt, your inner loop becomes

.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

As a bonus, if you didn't use a linked list, the loop may disappear entirely.

Upvotes: 6

Mike Vine
Mike Vine

Reputation: 9837

Use a template:

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

For me this gives:

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

Upvotes: 6

Related Questions