Производительность 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


person balki    schedule 13.01.2013    source источник
comment
Почему бы не попробовать и не провести сравнительный анализ?   -  person Some programmer dude    schedule 13.01.2013
comment
Я считаю, что то, как std::function реализует стирание типа, зависит от реализации (и я думаю, что Microsoft использует виртуальные функции), поэтому ответ может даже зависеть от того, на какую платформу вы ориентируетесь. на вашем месте я бы попробовал несколько тестов   -  person Andy Prowl    schedule 13.01.2013
comment
Я согласен, что бенчмаркинг покажет. Мне интересно, теоретически возможно ли, чтобы std::function специализировал такие случаи и был бы таким же эффективным, как обычная функция ptr.   -  person balki    schedule 14.01.2013
comment
@balki: Как и SSO для std::string, есть возможность SFO (небольшая оптимизация функтора) для std::function. Это позволит избежать динамического выделения памяти и ускорит копирование объектов std::function. Если вы заботитесь о накладных расходах на вызовы, вам не следует использовать std::function или указатели на функции, а пытаться использовать функторы напрямую. Это позволит встроить. В любом случае, протестируйте. Вы также можете проверить, использует ли ваш поставщик C++ SFO для std::function.   -  person sellibitze    schedule 14.01.2013


Ответы (3)


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

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

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

вызов std::function в C++11 просто добавляет еще 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
        );
    }
}
person christianparpart    schedule 15.10.2013
comment
гм, если вы используете С++ 11, почему, во имя бога, вы используете volatile? - person Tim Seguine; 17.10.2013
comment
Были бы результаты другими, если бы ваши функции-члены были константными? - person masaers; 11.12.2013
comment
Тим Сегин, я хочу, чтобы компилятор не кэшировал переменные в регистрах при использовании, поскольку они используются из рабочих потоков И основного потока (который периодически обращается к этим переменным для печати статистики). если бы я использовал std::atomic‹›, тогда использование ключевого слова volatile было бы ненужным. - person christianparpart; 03.02.2014
comment
masaers, Нет, но это действительно хорошая парадигма кодирования для создания константы, когда она не изменяет ваш локальный объект. Делает код чистым и позволяет избежать ошибок в будущем (но это не в тему :-) - person christianparpart; 03.02.2014
comment
((Однако смущает то, что назначенное лямбда-выражение работает быстрее, чем другие, даже чем C-one.)): Проблема, вероятно, заключается в параллелизме. Я точно не знаю, что происходит, но когда вы запускаете потоки по одному, я получаю такой результат: 1954073390 голый указатель на функцию 1952530828 объект функции C++11 для голой функции 1953096356 объект функции C++11 к методу объекта 1953336344 объект функции C++11 к методу объекта (виртуальный) 1951464452 объект функции C++11 к лямбда-выражению 10-я секунда. Все очень близко друг к другу. - person Timo Türschmann; 08.11.2014
comment
@trapni: (я знаю, что это устарело и может быть неактуальным.) Существует потенциально огромная проблема с производительностью в ложном совместном использовании всех счетчиков. Попробуйте разместить их отдельно друг от друга (каждый на своей строке кэша) и посмотрите, получите ли вы значительное улучшение производительности или нет. - person yzt; 24.09.2015
comment
@yzt извините, я только что нашел ваш ответ в этот момент (с Рождеством!), вы действительно попробовали то, что предлагаете? Но да, я считаю, что дело в кешах процессора, которые делают все так же быстро, как и другие. Но в настоящих приложениях у вас больше объемного кода вокруг реальных вызовов функций, что может привести к другим результатам. Я был бы очень заинтересован в более глубоком анализе по этому вопросу. - person christianparpart; 26.12.2015
comment
@trapni: я проверял это несколько месяцев назад, когда комментировал здесь. Что я помню, так это то, что производительность была улучшена после того, как я исправил несколько других вещей в тесте (я не помню что; я мог бы раскопать код). - person yzt; 26.12.2015
comment
@trapni: Чтобы решить проблему ложного разделения между потоками (что, я должен подчеркнуть, не имеет ничего общего с тем, что эта программа пытается сравнить, поскольку это не должно быть многопоточной программой ,) вам просто нужно вставить отступ между счетчиками. Это не имеет ничего общего с кодом. Просто убедитесь, что счетчики находятся в памяти на расстоянии не менее 64 байт (стоимость одной кэш-линии). - person yzt; 26.12.2015

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

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

person pmr    schedule 13.01.2013
comment
Кстати, в FAQ говорится: стоимость boost::function может быть разумно последовательно измерена на уровне около 20 нс +/- 10 нс на современной платформе с частотой 2 ГГц по сравнению с прямым встраиванием кода. Это не очень хорошее утверждение, IMO. Он не дает относительных оценок и не сравнивает его с невиртуальными вызовами функций (только с встраиванием) - person Andy Prowl; 13.01.2013
comment
@AndyProwl Да, но такие утверждения невероятно сложно сделать, а тесты очень сложно написать, и обычно они также зависят от версии компилятора. Это лучше, чем вообще никакого заявления. - person pmr; 13.01.2013
comment
Я полагаю, что люди из Boost были бы рады получить какой-нибудь тестовый код, отправленный в виде патча, чтобы люди могли измерить фактическое влияние на их конкретную платформу. - person Ulrich Eckhardt; 13.01.2013
comment
@balki Если что теоретически возможно? Написать бенчмарк? Конечно, но это сложно. Я зависит от того, что вас волнует: размер, скорость вызова, скорость копирования/перемещения? - person pmr; 14.01.2013

Я провел быстрый тест, используя Google Benchmark. Вот результаты:

Run on (4 X 2712 MHz CPU s)
----------------------------------------------------------
Benchmark                   Time           CPU Iterations
----------------------------------------------------------
RawFunctionPointer         11 ns         11 ns   56000000
StdBind                    12 ns         12 ns   64000000
StdFunction                11 ns         11 ns   56000000
Lambda                      9 ns          9 ns   64000000

Кажется, что наиболее оптимальным решением является использование лямбда-выражений (точно так же, как пользователь christianparpart, упомянутый в этой теме). Код, который я использовал для теста, можно найти ниже.

#include <benchmark/benchmark.h>

#include <cstdlib>
#include <cstdio>
#include <functional>

static volatile int global_var = 0;

void my_int_func(int x)
{
    global_var = x + x + 3;
    benchmark::DoNotOptimize(global_var);
    benchmark::DoNotOptimize(x);
}

static void RawFunctionPointer(benchmark::State &state)
{
    void (*bar)(int) = &my_int_func;
    srand (time(nullptr));
    for (auto _ : state)
    {
        bar(rand());
        benchmark::DoNotOptimize(my_int_func);
        benchmark::DoNotOptimize(bar);
    }
}

static void StdFunction(benchmark::State &state)
{
    std::function<void(int)> bar = my_int_func;
    srand (time(nullptr));
    for (auto _ : state)
    {
        bar(rand());
        benchmark::DoNotOptimize(my_int_func);
        benchmark::DoNotOptimize(bar);
    }
}

static void StdBind(benchmark::State &state)
{
    auto bar = std::bind(my_int_func, std::placeholders::_1);
    srand (time(nullptr));
    for (auto _ : state)
    {
        bar(rand());
        benchmark::DoNotOptimize(my_int_func);
        benchmark::DoNotOptimize(bar);
    }
}

static void Lambda(benchmark::State &state)
{
    auto bar = [](int x) {
        global_var = x + x + 3;
        benchmark::DoNotOptimize(global_var);
        benchmark::DoNotOptimize(x);
    };
    srand (time(nullptr));
    for (auto _ : state)
    {
        bar(rand());
        benchmark::DoNotOptimize(my_int_func);
        benchmark::DoNotOptimize(bar);
    }
}


BENCHMARK(RawFunctionPointer);
BENCHMARK(StdBind);
BENCHMARK(StdFunction);
BENCHMARK(Lambda);

BENCHMARK_MAIN();
person Kamil Kuczaj    schedule 20.02.2019
comment
Рад видеть это, но я предлагаю удалить rand в тесте для цикла, так как он очень медленный и будет стоить много времени выполнения. - person prehistoricpenguin; 08.09.2020
comment
Он использовался в каждой тестовой функции в качестве аргумента функции. Почему вы считаете, что rand() мешает результатам тестов? - person Kamil Kuczaj; 09.09.2020