[英]What is performance penalty for converting a function into coroutine in C++?
由于对于 C++20 协程,编译器必须通过将所有局部变量放在堆中而不是堆栈中来创建与普通函数不同的代码,相对于进行相同计算的普通函数,协程函数的预期减慢是多少?
我写了一个简单的测试来测量大量值的总和时间:
#include <iostream>
#include <chrono>
#include <coroutine>
constexpr size_t N = 1024ull*1024ull*1024ull;
double compute()
{
double res = 0;
for ( size_t i = 0; i < N; ++i )
res += i;
return res;
}
template<typename F>
auto timer( F f )
{
using namespace std::chrono;
auto s = high_resolution_clock::now();
auto res = f();
nanoseconds dur = high_resolution_clock::now() - s;
std::cout << "duration: " << dur.count() * 1e-9 << " sec\n";
return res;
}
struct Future {
struct promise_type {
double value_;
Future get_return_object() { return { std::coroutine_handle<promise_type>::from_promise(*this) }; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() {}
void return_value(double v) { value_ = v; }
};
bool done() { return h_.done(); }
void resume() { return h_(); }
double value() { return h_.promise().value_; }
~Future() { h_.destroy(); }
std::coroutine_handle<promise_type> h_;
};
double computeInCoroutine()
{
auto future = []() -> Future
{
double res = 0;
for ( size_t i = 0; i < N; ++i )
res += i;
co_return res;
}();
while (!future.done())
future.resume();
return future.value();
}
int main()
{
std::cout << "Ordinary function ";
auto res = timer( compute );
std::cout << "Coroutine ";
res += timer( computeInCoroutine );
return (int)res;
}
在 Visual Studio 2019 16.10.3 中,我得到了结果:
Ordinary function duration: 0.793619 sec
Coroutine duration: 1.05897 sec
请注意,这些时间是在本地计算机上获得的,并且它们的可重复性很高。 我不建议测量在线时间(例如在 Godbold.org 中),因为那里非常不稳定。
那么仅仅通过在计算中途不挂起的协程中转换一个普通函数,在MSVC中我们得到大约30%的性能损失,或者比较不公平?
我拿了你的代码,让它在 godbolt 上的clang/gcc中工作。
铛:
Ordinary function duration: 2.27166 sec 5.76461e+17 Coroutine duration: 2.76769 sec 5.76461e+17
海湾合作委员会:
Ordinary function duration: 2.21894 sec 5.76461e+17 Coroutine duration: 2.18039 sec 5.76461e+17
Clang 慢了 23%,gcc 的速度大致相同(区别在于噪音)。
当我启用-ffast-math
,我得到:
Ordinary function duration: 0.465791 sec 5.76461e+17 Coroutine duration: 1.58706 sec 5.76461e+17
在叮当声中,和
Ordinary function duration: 2.19963 sec 5.76461e+17 Coroutine duration: 2.23827 sec 5.76461e+17
在海湾合作委员会。
然后我重写了future
阅读:
auto future = []() -> Future
{
auto helper = [](){
double res = 0;
for ( size_t i = 0; i < N; ++i )
res += i;
return res;
};
co_return helper();
}();
我在辅助 lambda 中隐藏协程的状态。 这会将叮当时间更改为:
Ordinary function duration: 1.07504 sec 5.76461e+17 Coroutine duration: 0.465179 sec 5.76461e+17
组装没有什么明显的。 所以我交换了他们的订单(没有隐藏 lambda)并得到:
Coroutine duration: 0.467913 sec 5.76461e+17 Ordinary function duration: 0.959462 sec 5.76461e+17
以相反的顺序运行它们会使第一个更快。
因此,我们看到了性能测试工具的工件。
您需要测试这个交叉编译器,因为协程支持是新的并且正在改进。 您需要使用真正的测试工具,多次运行,并在两种情况下完全对称(不是先运行一个,然后另一个;只在任何一个程序执行时运行一个)。 并且您需要设置优化标志,并调整微优化的细节以查看是否存在不稳定性。
然后,您需要查看生成的程序集以了解速度下降的原因是否合理,以及可能在哪里。
微优化很难。
无论如何,当我以多种方式调整基准时,我设法使速度达到或超过非协程版本。 所以不,这种情况下 30% 的命中率是不可预期的。
将此作为适当的基准编写(由于@Yakk 的回答,clang 兼容性发生了变化):
#include <benchmark/benchmark.h>
#include <iostream>
#include <chrono>
#ifdef __clang__
#include <experimental/coroutine>
#else
#include <coroutine>
#endif
namespace cor {
#ifdef __clang__
using namespace std::experimental;
#else
using namespace std;
#endif
}
constexpr size_t N = 1024ull*1024ull*1024ull;
struct Future {
struct promise_type {
double value_;
Future get_return_object() { return { cor::coroutine_handle<promise_type>::from_promise(*this) }; }
cor::suspend_always initial_suspend() { return {}; }
cor::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() {}
void return_value(double v) { value_ = v; }
};
bool done() { return h_.done(); }
void resume() { return h_(); }
double value() { return h_.promise().value_; }
~Future() { h_.destroy(); }
cor::coroutine_handle<promise_type> h_;
};
double compute()
{
double res = 0;
for ( size_t i = 0; i < N; ++i )
benchmark::DoNotOptimize(res += i);
return res;
}
double computeInCoroutine()
{
auto future = []() -> Future
{
double res = 0;
for ( size_t i = 0; i < N; ++i )
benchmark::DoNotOptimize(res += i);
co_return res;
}();
while (!future.done())
future.resume();
return future.value();
}
static void BenchCompute(benchmark::State& state) {
for (auto _ : state) {
benchmark::DoNotOptimize(compute());
}
}
BENCHMARK(BenchCompute);
static void BenchComputeCoroutine(benchmark::State& state) {
for (auto _ : state) {
benchmark::DoNotOptimize(computeInCoroutine());
}
}
BENCHMARK(BenchComputeCoroutine);
BENCHMARK_MAIN();
我使用 MSVC 19 x64 Release 获得以下结果:
2021-07-27T13:22:02-04:00
Running <some_path>\bench.exe
Run on (24 X 3793 MHz CPU s)
CPU Caches:
L1 Data 32 KiB (x12)
L1 Instruction 32 KiB (x12)
L2 Unified 512 KiB (x12)
L3 Unified 16384 KiB (x4)
----------------------------------------------------------------
Benchmark Time CPU Iterations
----------------------------------------------------------------
BenchCompute 2698332000 ns 2687500000 ns 1
BenchComputeCoroutine 2700581500 ns 2703125000 ns 1
GCC 和 clang 结果将有助于完成图片,但在我看来很清楚您的测试是以夸大协程开销的方式完成的,至少在 MSVC 上是这样。
编辑:如果我从计算()函数中删除“繁忙的工作”,并且除了开销之外什么都不做基准测试。
double computeInCoroutine()
{
auto future = []() -> Future
{
double res = 0;
co_return res;
}();
while (!future.done())
future.resume();
return future.value();
}
double compute()
{
double res = 0;
return res;
}
我获得以下信息:
2021-07-27T13:47:29-04:00
Running <some_path>\bench.exe
Run on (24 X 3793 MHz CPU s)
CPU Caches:
L1 Data 32 KiB (x12)
L1 Instruction 32 KiB (x12)
L2 Unified 512 KiB (x12)
L3 Unified 16384 KiB (x4)
----------------------------------------------------------------
Benchmark Time CPU Iterations
----------------------------------------------------------------
BenchCompute 2.61 ns 2.61 ns 263529412
BenchComputeCoroutine 66.5 ns 66.3 ns 8960000
这表示 60 纳秒的开销(编译器和系统特定的)。 仍然比 OP 报告的要轻得多。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.