Как избежать издержек std :: function - PullRequest
2 голосов
/ 26 апреля 2019

Я хочу выполнить набор операций над элементами в (настраиваемом) односвязном списке. Код для обхода связанного списка и выполнения операций прост, но повторяется и может быть ошибочным, если его везде копировать / вставлять. Производительность и аккуратное распределение памяти важны для моей программы, поэтому я хочу избежать ненужных накладных расходов.

Я хочу написать оболочку, включающую повторяющийся код и инкапсулирующую операции, которые должны выполняться над каждым элементом связанного списка. Поскольку функции, которые выполняются внутри операции, меняются, мне нужно захватить несколько переменных (в реальном коде), которые должны быть предоставлены для операции, поэтому я рассмотрел использование 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

Эти временные интервалы достаточно постоянны для нескольких прогонов и показывают 4-кратный множитель при использовании std::function. Есть ли лучший способ сделать то, что я хочу?

Ответы [ 2 ]

6 голосов
/ 26 апреля 2019

Функциональные объекты имеют довольно большой вес, но могут использоваться там, где полезная нагрузка достаточно велика (> 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

В качестве бонуса, если вы не использовали связанный список, цикл может полностью исчезнуть.

6 голосов
/ 26 апреля 2019

Использовать шаблон:

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
...