Reputation: 390
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
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
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