C++関数呼び出しのオーバーヘッド時間 / 関数呼び出し時のパフォーマンスに関する問題

設計行情 SDK、針對不同的回呼函數實現方式,進行了一次耗時的測試。近期在看 C++ 函數編程,當函數變成了一等公民,在程式內部流轉,耗時有什么不同?

前文連結:编译器、回调函数、性能测试 leimao 大佬刚好也做了类似的測試,借代码一用。

本文

実行プラットフォームは引き続き、当社の旧友である https://wandbox.org/ です。

#include <cassert>
#include <chrono>
#include <functional>
#include <iostream>
#include <vector>

int add_one(int input) { return input + 1; }

bool validate_vector_add_one(std::vector<int> const& input_vector,
                             std::vector<int> const& output_vector)
{
    bool is_valid{true};
    for (size_t i{0}; i < input_vector.size(); ++i)
    {
        if (output_vector.at(i) != input_vector.at(i) + 1)
        {
            is_valid = false;
            break;
        }
    }
    return is_valid;
}

void reset_vector(std::vector<int>& input_vector)
{
    for (size_t i{0}; i < input_vector.size(); ++i)
    {
        input_vector.at(i) = 0;
    }
}

template <typename T, typename Func>
void unitary_function_pass_by_lambda_function(T& output, T const& input,
                                              Func const func)
{
    output = func(input);
}

template <typename T>
void unitary_function_pass_by_std_function_value(T& output, T const& input,
                                                 std::function<T(T)> const func)
{
    output = func(input);
}

template <typename T>
void unitary_function_pass_by_std_function_reference(
    T& output, T const& input, std::function<T(T)> const& func)
{
    output = func(input);
}

template <typename T>
void unitary_function_pass_by_function_pointer(T& output, T const& input,
                                               T (*func)(T))
{
    output = func(input);
}

int main()
{
    // Set floating point format std::cout with 3 decimal places.
    std::cout.precision(3);

    size_t const num_elements{10000000};
    std::vector<int> input_vector(num_elements, 0);
    std::vector<int> output_vector(num_elements, 0);

    auto const lambda_function_add_one{[](int const& input) -> int
                                       { return input + 1; }};
    std::function<int(int)> const std_function_add_one{lambda_function_add_one};

    std::cout << "The size of a function pointer: " << sizeof(&add_one)
              << std::endl;
    std::cout << "The size of a std::function pointer: "
              << sizeof(&std_function_add_one) << std::endl;
    std::cout << "The size of a std::function: " << sizeof(std_function_add_one)
              << std::endl;

    // Call function frequently in a vanilla way.
    // The compiler knows what function to call at compile time and can optimize
    // the code.
    // This is the best performance we could get.
    std::chrono::steady_clock::time_point const time_start_vanilla{
        std::chrono::steady_clock::now()};
    for (size_t i{0}; i < num_elements; ++i)
    {
        output_vector.at(i) = add_one(input_vector.at(i));
    }
    std::chrono::steady_clock::time_point const time_end_vanilla{
        std::chrono::steady_clock::now()};
    auto const time_elapsed_vanilla{
        std::chrono::duration_cast<std::chrono::nanoseconds>(time_end_vanilla -
                                                             time_start_vanilla)
            .count()};
    float const latency_vanilla{time_elapsed_vanilla /
                                static_cast<float>(num_elements)};
    std::cout << "Latency Pass Vanilla: " << latency_vanilla << " ns"
              << std::endl;
    assert(validate_vector_add_one(input_vector, output_vector));
    reset_vector(output_vector

## 正文
// 時々、コンパイル時に呼び出す関数を知らない場合があります。
// `std::function` を使用して、関数を引数として渡すことができます。
// この場合は、`std::function` を値で渡します。
// `std::function` のサイズが 32 バイトであるため、値を渡すと多くのコピーが発生し、パフォーマンスが悪くなります。
std::chrono::steady_clock::time_point const
    time_start_pass_by_std_function_value{std::chrono::steady_clock::now()};
for (size_t i{0}; i < num_elements; ++i)
{
    unitary_function_pass_by_std_function_value(
        output_vector.at(i), input_vector.at(i), std_function_add_one);
}
std::chrono::steady_clock::time_point const
    time_end_pass_by_std_function_value{std::chrono::steady_clock::now()};
auto const time_elapsed_pass_by_std_function_value{
    std::chrono::duration_cast<std::chrono::nanoseconds>(
        time_end_pass_by_std_function_value -
        time_start_pass_by_std_function_value)
        .count()};
float const latency_pass_by_std_function_value{
    time_elapsed_pass_by_std_function_value /
    static_cast<float>(num_elements)};
std::cout << "Latency Pass By Std Function Value: "
          << latency_pass_by_std_function_value << " ns" << std::endl;
assert(validate_vector_add_one(input_vector, output_vector));
reset_vector(output_vector);

// `std::function` を値で渡す代わりに、参照(ポインタ)で渡すこともできます。
// この場合、オブジェクトのコピーは排除されます。パフォーマンスは、`std::function` を値で渡した場合よりも優れています。
// ただし、ワイルドな方法ほどではありません。
std::chrono::steady_clock::time_point const
    time_start_pass_by_std_function_reference{
        std::chrono::steady_clock::now()};
for (size_t i{0}; i < num_elements; ++i)
{
    unitary_function_pass_by_std_function_reference(
        output_vector.at(i), input_vector.at(i), std_function_add_one);
}
std::chrono::steady_clock::time_point const
    time_end_pass_by_std_function_reference{
        std::chrono::steady_clock::now()};
auto const time_elapsed_pass_by_std_function_reference{
    std::chrono::duration_cast<std::chrono::nanoseconds>(
        time_end_pass_by_std_function_reference -
        time_start_pass_by_std_function_reference)
        .count()};
float const latency_pass_by_std_function_reference{
    time_elapsed_pass_by_std_function_reference /
    static_cast<float>(num_elements)};
std::cout << "Latency Pass By Std Function Reference: "
          << latency_pass_by_std_function_reference << " ns" << std::endl;
assert(validate_vector_add_one(input_vector, output_vector));
reset_vector(output_vector);

## 本文
// `std::function` は、関数ポインタ、呼び出し可能オブジェクト、ラムダ関数をラップする汎用的なものです。
// 汎用性があるため、関数ポインタほど効率的ではありません。この場合は、関数ポインタを関数に渡します。
// `std::function` を参照で渡すよりもパフォーマンスが優れています。
std::chrono::steady_clock::time_point const time_start_pass_by_function_pointer{std::chrono::steady_clock::now()};
for (size_t i{0}; i < num_elements; ++i)
{
    unitary_function_pass_by_function_pointer(output_vector.at(i),
                                                  input_vector.at(i), &add_one);
}
std::chrono::steady_clock::time_point const time_end_pass_by_function_pointer{std::chrono::steady_clock::now()};
auto const time_elapsed_pass_by_function_pointer{
        std::chrono::duration_cast<std::chrono::nanoseconds>(
            time_end_pass_by_function_pointer -
            time_start_pass_by_function_pointer)
            .count()};
float const latency_pass_by_function_pointer{
        time_elapsed_pass_by_function_pointer /
        static_cast<float>(num_elements)};
std::cout << "Latency Pass By Function Pointer: "
              << latency_pass_by_function_pointer << " ns" << std::endl;
assert(validate_vector_add_one(input_vector, output_vector));
reset_vector(output_vector);

// ラムダ関数を関数に渡すこともできます。
// コンパイラは、コンパイル時に呼び出す関数を知っており、コードを最適化できます。
// `std::function` を参照で渡すよりもパフォーマンスも優れています。
std::chrono::steady_clock::time_point const time_start_pass_by_lambda_function{std::chrono::steady_clock::now()};
for (size_t i{0}; i < num_elements; ++i)
{
    unitary_function_pass_by_lambda_function(
        output_vector.at(i), input_vector.at(i), lambda_function_add_one);
}
std::chrono::steady_clock::time_point const time_end_pass_by_lambda_function{std::chrono::steady_clock::now()};
auto const time_elapsed_pass_by_lambda_function{
        std::chrono::duration_cast<std::chrono::nanoseconds>(
            time_end_pass_by_lambda_function -
            time_start_pass_by_lambda_function)
            .count()};
float const latency_pass_by_lambda_function{
        time_elapsed_pass_by_lambda_function /
        static_cast<float>(num_elements)};
std::cout << "Latency Pass By Lambda Function: "
              << latency_pass_by_lambda_function << " ns" << std::endl;
assert(validate_vector_add_one(input_vector, output_vector));
reset_vector(output_vector);

## 本文

```shell
# チーム全体の最適化 (O2) を有効にし、コンパイルには gcc13 を選択しました。gcc のバージョンが異なる場合、性能と時間の違いはわずかに異なりますが、バージョンが高いほど lambda の効果が良いです。
関数のポインタのサイズ: 8 バイト
std::function ポインタのサイズ: 8 バイト
std::function オブジェクトのサイズ: 32 バイト
Vanilla パスのレイテンシ: 0.418 ns
std::function 値でパスするレイテンシ: 3.47 ns
std::function リファレンスでパスするレイテンシ: 1.36 ns
ポインタで関数をパスするレイテンシ: 0.396 ns
ラムダ関数でパスするレイテンシ: 0.44 ns

参考文献

https://leimao.github.io/blog/CPP-Function-Call-Performance/

Licensed under CC BY-NC-SA 4.0
最終更新 2025年06月02日 20:54
金融ITプログラマーのいじくり回しと日常のつぶやき
Hugo で構築されています。
テーマ StackJimmy によって設計されています。