Производительность std :: function по сравнению с необработанным указателем на функцию и void * this?

Код библиотеки:

class Resource
{
public:
typedef void (*func_sig)(int, char, double, void*);
//Registration
registerCallback(void* app_obj, func_sig func)
{
_app_obj = app_obj;
_func = func;
}

//Calling when the time comes
void call_app_code()
{
_func(231,'a',432.4234,app_obj);
}
//Other useful methods
private:
void* app_obj;
func_sig _func;
//Other members
};

Код приложения:

class App
{
public:
void callme(int, char, double);
//other functions, members;
};

void callHelper(int i, char c, double d, void* app_obj)
{
static_cast<App*>(app_obj)->callme(i,c,d);
}

int main()
{
App a;
Resource r;
r.registercallback(&a, callHelper);
//Do something
}

Выше приведена минимальная реализация механизма обратного вызова. Он более подробный, не поддерживает привязку, заполнители и т. Д., Например std :: function.
Если я использую std::function или же boost::function для приведенного выше варианта использования будут ли какие-либо недостатки в производительности? Этот обратный вызов будет находиться на очень критическом пути приложения реального времени. Я слышал, что boost :: function использует виртуальные функции для фактической отправки. Будет ли это оптимизировано, если не задействованы обязательные / заполнители?

Обновить

Для тех, кто заинтересован в проверке сборок в последних компиляторах: https://gcc.godbolt.org/z/-6mQvt

11

Решение

Я уже довольно часто задавался вопросом, поэтому я начал писать какой-то очень минимальный тест, который пытается имитировать производительность с помощью циклических атомных счетчиков для каждой версии обратного вызова с указателем на функцию.

Имейте в виду, это голый вызовы функций, которые делают только одно, атомарно увеличивая свой счетчик;

Проверив сгенерированный вывод ассемблера, вы можете обнаружить, что пустой цикл указателя на C-функцию скомпилирован в 3 инструкции процессора;

C ++ 11 std::function call просто добавляет еще 2 инструкции процессора, таким образом, 5 в нашем примере. В заключение: совершенно неважно, какой метод указателя на функцию вы используете, различия в накладных расходах в любом случае очень малы.

((Однако сбивает с толку то, что назначенное лямбда-выражение, кажется, работает быстрее, чем другие, даже чем C-one.))

Скомпилируйте пример с: clang++ -o tests/perftest-fncb tests/perftest-fncb.cpp -std=c++11 -pthread -lpthread -lrt -O3 -march=native -mtune=native

#include <functional>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

typedef unsigned long long counter_t;

struct Counter {
volatile counter_t bare;
volatile counter_t cxx;
volatile counter_t cxo1;
volatile counter_t virt;
volatile counter_t lambda;

Counter() : bare(0), cxx(0), cxo1(0), virt(0), lambda(0) {}
} counter;

void bare(Counter* counter) { __sync_fetch_and_add(&counter->bare, 1); }
void cxx(Counter* counter) { __sync_fetch_and_add(&counter->cxx, 1); }

struct CXO1 {
void cxo1(Counter* counter) { __sync_fetch_and_add(&counter->cxo1, 1); }
virtual void virt(Counter* counter) { __sync_fetch_and_add(&counter->virt, 1); }
} cxo1;

void (*bare_cb)(Counter*) = nullptr;
std::function<void(Counter*)> cxx_cb;
std::function<void(Counter*)> cxo1_cb;
std::function<void(Counter*)> virt_cb;
std::function<void(Counter*)> lambda_cb;

void* bare_main(void* p) { while (true) { bare_cb(&counter); } }
void* cxx_main(void* p) { while (true) { cxx_cb(&counter); } }
void* cxo1_main(void* p) { while (true) { cxo1_cb(&counter); } }
void* virt_main(void* p) { while (true) { virt_cb(&counter); } }
void* lambda_main(void* p) { while (true) { lambda_cb(&counter); } }

int main()
{
pthread_t bare_thread;
pthread_t cxx_thread;
pthread_t cxo1_thread;
pthread_t virt_thread;
pthread_t lambda_thread;

bare_cb = &bare;
cxx_cb = std::bind(&cxx, std::placeholders::_1);
cxo1_cb = std::bind(&CXO1::cxo1, &cxo1, std::placeholders::_1);
virt_cb = std::bind(&CXO1::virt, &cxo1, std::placeholders::_1);
lambda_cb = [](Counter* counter) { __sync_fetch_and_add(&counter->lambda, 1); };

pthread_create(&bare_thread, nullptr, &bare_main, nullptr);
pthread_create(&cxx_thread, nullptr, &cxx_main, nullptr);
pthread_create(&cxo1_thread, nullptr, &cxo1_main, nullptr);
pthread_create(&virt_thread, nullptr, &virt_main, nullptr);
pthread_create(&lambda_thread, nullptr, &lambda_main, nullptr);

for (unsigned long long n = 1; true; ++n) {
sleep(1);
Counter c = counter;

printf(
"%15llu bare function pointer\n""%15llu C++11 function object to bare function\n""%15llu C++11 function object to object method\n""%15llu C++11 function object to object method (virtual)\n""%15llu C++11 function object to lambda expression %30llu-th second.\n\n",
c.bare, c.cxx, c.cxo1, c.virt, c.lambda, n
);
}
}
8

Другие решения

std::function выполняет стирание типа для типа функции, и существует несколько способов его реализации, поэтому вам, возможно, следует добавить, какую версию какого компилятора вы используете, чтобы получить точный ответ.

boost::function в значительной степени идентична std::function и поставляется с Часто задаваемые вопросы вход по вызову и некоторые общий раздел по производительности. Они дают некоторые подсказки о том, как функционирует объект функции. Если это применимо в вашем случае, зависит от вашей реализации, но цифры не должны быть существенно разными.

7