【问题标题】:Performance of std::function compared to raw function pointer and void* this?std::function 与原始函数指针和 void* this 相比的性能?
【发布时间】:2012-12-27 16:59:03
【问题描述】:

库代码:

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::functionboost::function,会不会有任何性能缺陷?此回调将处于实时应用程序的非常关键的路径中。我听说 boost::function 使用虚函数来进行实际的调度。如果不涉及绑定/占位符,那会被优化吗?

更新

对于那些有兴趣在最新编译器中检查程序集的人:https://gcc.godbolt.org/z/-6mQvt

【问题讨论】:

  • 为什么不试试做一些基准测试呢?
  • 我相信 std::function 如何实现类型擦除取决于实现(我认为微软使用虚函数),因此答案甚至可能取决于您的目标平台。如果我是你,我会尝试一些基准测试
  • 我同意基准测试会显示。我想知道 std::function 在理论上是否有可能专门化这种情况并像普通函数 ptr 一样高效。
  • @balki:就像 std::string 的“SSO”一样,std::function 也有可能进行 SFO(小型函子优化)。这将避免动态内存分配并加速复制 std::function 对象。如果您关心调用开销,则不应使用 std::function 或函数指针,而应尝试直接使用仿函数。这将启用内联。无论如何,测试一下。您可能还想检查您的 C++ 供应商是否为 std::function 执行 SFO。

标签: c++ c++11 boost-function std-function


【解决方案1】:

我已经经常怀疑自己,所以我开始编写一些非常小的基准测试,尝试通过循环原子计数器模拟每个函数指针回调版本的性能。

请记住,这些是对只做一件事的函数的简单调用,它以原子方式递增其计数器;

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

C++11 的 std::function 调用只是增加了 2 条 CPU 指令,因此在我们的示例中为 5 条。作为结论:无论您使用哪种函数指针技术方式,开销差异都非常小。

((但令人困惑的是,分配的 lambda 表达式似乎比其他表达式运行得更快,甚至比 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
        );
    }
}

【讨论】:

  • 嗯,如果你使用的是 c++11,你为什么要使用 volatile?
  • 如果你的成员函数是 const,结果会不会不同?
  • Tim Seguine,我希望编译器在使用时不要将变量缓存在寄存器中,因为它们是从工作线程和主线程(定期访问这些变量以打印统计信息)中使用的。如果我使用 std::atomic 那么就没有必要使用 volatile 关键字了。
  • masaers,不——但它确实是一个很好的编码范例,当它不修改你的本地对象时。使代码干净并避免将来出现错误(但这不是这个故事:-)
  • ((令人困惑的是,分配的 lambda 表达式似乎比其他表达式运行得更快,甚至比 C-one 还要快。)):问题可能是并发性。我不完全知道会发生什么,但是当您一次运行一个线程时,我会得到如下结果: 1954073390 裸函数指针 1952530828 C++11 函数对象到裸函数 1953096356 C++11 函数对象到对象方法 1953336344 C++11 函数对象到对象方法(虚拟)1951464452 C++11 函数对象到 lambda 表达式第 10 秒。都非常紧密地结合在一起。
【解决方案2】:

std::function 对函数类型执行类型擦除,并且实现它的方法不止一种,因此您可能应该添加您正在使用的编译器的哪个版本以获得准确的答案。

boost::functionstd::function 基本相同,并带有一个FAQ 呼叫开销条目和一些general section on performance。这些给出了函数对象如何执行的一些提示。如果这适用于您的情况,则取决于您的实施,但数字不应有显着差异。

【讨论】:

  • 顺便说一句,FAQ 说:“在现代 >2GHz 平台上,与直接内联代码相比, boost::function 的成本可以合理地一致地测量为大约 20ns +/- 10ns。”。这不是一个很好的声明IMO。它不提供相对估计,也不将其与非虚拟函数调用进行比较(仅用于内联)
  • @AndyProwl 是的,但是这样的陈述非常难以做出,并且基准真的很难编写,而且通常还取决于编译器版本。总比没有声明好。
  • 我相信 Boost 人会对一些作为补丁提交的基准代码感到高兴,这样人们就可以衡量对他们特定平台的实际影响。
  • @balki 如果理论上有什么可能?写一个基准?当然可以,但这很棘手。我取决于你关心什么:大小、调用速度、复制/移动速度?
【解决方案3】:

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

似乎最优化的解决方案是使用 lambdas(就像这个线程中提到的用户 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();

【讨论】:

  • 很高兴看到这一点,但我建议在循环测试中删除 rand,因为它非常慢并且会占用大量运行时间。
  • 它已在每个基准函数中用作函数的参数。为什么你认为 rand() 会干扰基准测试结果?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-16
  • 2022-01-23
  • 2021-09-29
相关资源
最近更新 更多