設計行情 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/