std::function 与裸函数指针以及 void* this 的性能比较?

14

库代码:

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

为什么不试一下并进行一些基准测试呢? - Some programmer dude
2
我认为 std::function 如何实现类型擦除是依赖于具体实现的(我认为微软的实现使用了虚函数),因此答案甚至可能取决于您的目标平台。如果我是您,我会尝试一些基准测试。 - Andy Prowl
我同意基准测试会显示出来。我想知道std::function在理论上是否可能专门处理这些情况,并像普通函数指针一样高效。 - balki
2
@balki:就像std::string的“SSO”一样,std::function存在SFO(小型函数对象优化)的可能性。这将避免动态内存分配并加速复制std::function对象。如果您在意调用开销,您不应该使用std::function或函数指针,而应尝试直接使用函数对象。这将启用内联。无论如何,请进行测试。您还可能想检查您的C++供应商是否对std::function进行了SFO。 - sellibitze
3个回答

11

我经常在思考这个问题,所以我开始编写一些非常简单的基准测试来尝试通过循环的原子计数器模拟性能,对于每个函数指针回调版本。

请记住,这些是对只执行一个操作——原子地递增其计数器的函数的调用;

通过检查生成的汇编输出,您可能会发现,裸C函数指针循环被编译成了3个CPU指令;

C++11的std::function调用仅添加了2个CPU指令,因此在我们的示例中有5个。总之:无论使用何种函数指针技术,开销差异都非常小。

((然而令人困惑的是,分配的lambda表达式似乎比其他表达式运行得更快,甚至比C语言的表达式还要快。))

使用以下命令编译示例: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
        );
    }
}

3
如果你正在使用C++11,为什么要使用volatile?天哪! - Tim Seguine
如果您的成员函数是const,结果会有所不同吗? - masaers
4
Tim Seguine,我希望编译器在使用工作线程和主线程(定期访问这些变量以打印统计信息)时不要将变量缓存到寄存器中。如果我使用std::atomic<>,那么就不需要使用volatile关键字了。 - christianparpart
不是必须的,但将const用于不修改本地对象的编程范式确实很好。这样可以使代码更加清晰,并避免未来出现的错误(但这已经超出了本故事的范围 :-) - christianparpart
@trapni:(我知道这是老话题,可能已经没有关系了。)所有计数器之间存在潜在的“错误共享”性能问题。尝试将它们分开放置(每个都在自己的缓存行上),看看是否会获得显著的性能改善。 - yzt
显示剩余4条评论

9

std::function对函数类型进行类型擦除,有多种实现方式,因此您可能需要添加使用的编译器版本的确切答案。

boost::functionstd::function基本相同,并附带一个关于调用开销的FAQ条目和一些性能的一般部分。这些给出了一些关于函数对象性能的提示。如果这适用于您的情况,则取决于您的实现,但数字不应显着不同。


1
顺便提一下,FAQ中说:“在现代> 2GHz平台上,boost :: function的成本可以以大约20ns +/- 10 ns的相对一致性进行测量,与直接内联代码相比。”我认为这不是一个很好的陈述。它没有给出相对估计,并且没有将其与非虚拟函数调用进行比较(仅与内联)。 - Andy Prowl
@AndyProwl 是的,但这样的陈述非常难以做出,并且基准测试很难编写,通常还与编译器版本有关。这比没有声明要好。 - pmr
我相信Boost的开发人员会很高兴收到一些基准测试代码作为补丁提交,这样人们就可以测量它们在特定平台上的实际影响。 - Ulrich Eckhardt
@balki 如果理论上可能的话?编写基准测试?当然可以,但这很棘手。这取决于你关心什么:大小、调用速度、复制/移动速度? - pmr

7

我使用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

看起来最优解是使用lambda表达式(就像用户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();

1
很高兴看到这个,但我建议在循环测试中删除rand,因为它非常慢,会耗费大量运行时间。 - prehistoricpenguin
它已经作为函数的参数被用于每个基准测试函数中。为什么您认为rand()会扰乱基准测试结果? - Kamil Kuczaj
1
@KamilKuczaj 这个函数绝对不能保证恒定时间,并且变化幅度远远超过开销。此外,你正在“DoNotOptimize”很多东西,其中大部分在进行基准测试时真的不应该这样做。而且,这对基准测试来说是无用的。 - Marcus Müller
1
rand 替换为简单的 rng_state = (rng_state << 13) ^ 0xDEADCAFE; bar(rng_state); 可以使整体执行速度比使用 rand 快两到三倍。因此,你的基准测试很可能主要受到在该上下文中调用 rand() 的开销的影响。(这很可能是关于堆栈的有效使用的问题!) - Marcus Müller
1
https://quick-bench.com/q/EgY8KtLhv6MnWYkArIOCtfTFI8U - Marcus Müller

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接