C++ 最快的 `finally`
Fastest `finally` for C++
C++ 到目前为止(不幸的是)不支持 try
语句的 finally
子句。这引发了对如何释放资源的猜测。在网上研究了这个问题后,虽然我找到了一些解决方案,但我并不清楚它们的性能(如果性能不是那么重要,我会使用 Java )。所以我不得不进行基准测试。
选项是:
基于函子的 finally
class 在 CodeProject 上提出。它很强大,但速度很慢。反汇编表明,外部函数局部变量的捕获效率非常低:一个一个地推入堆栈,而不是仅将帧指针传递给内部 (lambda) 函数。
RAII:堆栈上的手动清洁器对象:缺点是手动输入并针对每个使用的地方进行定制。还有一个缺点就是需要把资源释放需要的所有变量都拷贝给它。
MSVC++ 特定 __try
/ __finally
statement。缺点就是明显不便携
我创建了这个小型基准来比较这些方法的运行时性能:
#include <chrono>
#include <functional>
#include <cstdio>
class Finally1 {
std::function<void(void)> _functor;
public:
Finally1(const std::function<void(void)> &functor) : _functor(functor) {}
~Finally1() {
_functor();
}
};
void BenchmarkFunctor() {
volatile int64_t var = 0;
const int64_t nIterations = 234567890;
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 0; i < nIterations; i++) {
Finally1 doFinally([&] {
var++;
});
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("Functor: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
}
void BenchmarkObject() {
volatile int64_t var = 0;
const int64_t nIterations = 234567890;
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 0; i < nIterations; i++) {
class Cleaner {
volatile int64_t* _pVar;
public:
Cleaner(volatile int64_t& var) : _pVar(&var) { }
~Cleaner() { (*_pVar)++; }
} c(var);
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("Object: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
}
void BenchmarkMSVCpp() {
volatile int64_t var = 0;
const int64_t nIterations = 234567890;
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 0; i < nIterations; i++) {
__try {
}
__finally {
var++;
}
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("__finally: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
}
template <typename Func> class Finally4 {
Func f;
public:
Finally4(Func&& func) : f(std::forward<Func>(func)) {}
~Finally4() { f(); }
};
template <typename F> Finally4<F> MakeFinally4(F&& f) {
return Finally4<F>(std::forward<F>(f));
}
void BenchmarkTemplate() {
volatile int64_t var = 0;
const int64_t nIterations = 234567890;
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 0; i < nIterations; i++) {
auto doFinally = MakeFinally4([&] { var++; });
//Finally4 doFinally{ [&] { var++; } };
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("Template: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
}
void BenchmarkEmpty() {
volatile int64_t var = 0;
const int64_t nIterations = 234567890;
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 0; i < nIterations; i++) {
var++;
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("Empty: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
}
int __cdecl main() {
BenchmarkFunctor();
BenchmarkObject();
BenchmarkMSVCpp();
BenchmarkTemplate();
BenchmarkEmpty();
return 0;
}
我的 Ryzen 1800X @3.9Ghz 和 DDR4 @2.6Ghz CL13 的结果是:
Functor: 175148825.946 Ops/sec, var=234567890
Object: 553446751.181 Ops/sec, var=234567890
__finally: 553832236.221 Ops/sec, var=234567890
Template: 554964345.876 Ops/sec, var=234567890
Empty: 554468478.903 Ops/sec, var=234567890
显然,除了 functor-base (#1) 之外的所有选项都和空循环一样快。
那么是否有一种快速而强大的 C++ 替代方案 finally
,它是可移植的并且需要从外部函数的堆栈中进行最少的复制?
更新:我已经对@Jarod42 解决方案进行了基准测试,所以这里的问题是更新的代码和输出。尽管如@Sopel 所述,如果不执行复制省略,它可能会中断。
更新 2:澄清我的要求是在 C++ 中执行代码块的一种方便快捷的方法,即使抛出异常也是如此。由于问题中提到的原因,有些方法很慢或不方便。
您可以实现 Finally
而无需类型擦除和 std::function
的开销:
template <typename F>
class Finally {
F f;
public:
template <typename Func>
Finally(Func&& func) : f(std::forward<Func>(func)) {}
~Finally() { f(); }
Finally(const Finally&) = delete;
Finally(Finally&&) = delete;
Finally& operator =(const Finally&) = delete;
Finally& operator =(Finally&&) = delete;
};
template <typename F>
Finally<F> make_finally(F&& f)
{
return { std::forward<F>(f) };
}
并像这样使用它:
auto&& doFinally = make_finally([&] { var++; });
好吧,是你的基准测试失败了:它实际上并没有抛出,所以你只看到了非异常路径。这是非常糟糕的,因为优化器可以证明您没有抛出异常,因此它可以丢弃所有实际处理执行清理的代码,但在运行中出现异常。
我认为,您应该重复您的基准测试,将对 exceptionThrower()
或 nonthrowingThrower()
的调用放入您的 try{}
块中。这两个函数应作为单独的翻译单元编译,并且仅 link 与基准代码一起编辑。这将强制编译器实际生成异常处理代码,而不管您是调用 exceptionThrower()
还是 nonthrowingThrower()
。 (确保您没有打开 link 时间优化,这可能会破坏效果。)
这还可以让您轻松比较异常和非抛出执行路径之间的性能影响。
除了基准问题外,C++ 中的异常很慢。你永远不会在一秒钟内抛出数亿个异常。它充其量大约是个位数百万,可能更少。我希望不同 finally
实现之间的任何性能差异在投掷情况下完全不相关。您可以优化的是非抛出路径,其中您的成本只是 finally
实现对象的 construction/destruction,无论它是什么。
C++ 到目前为止(不幸的是)不支持 try
语句的 finally
子句。这引发了对如何释放资源的猜测。在网上研究了这个问题后,虽然我找到了一些解决方案,但我并不清楚它们的性能(如果性能不是那么重要,我会使用 Java )。所以我不得不进行基准测试。
选项是:
基于函子的
finally
class 在 CodeProject 上提出。它很强大,但速度很慢。反汇编表明,外部函数局部变量的捕获效率非常低:一个一个地推入堆栈,而不是仅将帧指针传递给内部 (lambda) 函数。RAII:堆栈上的手动清洁器对象:缺点是手动输入并针对每个使用的地方进行定制。还有一个缺点就是需要把资源释放需要的所有变量都拷贝给它。
MSVC++ 特定
__try
/__finally
statement。缺点就是明显不便携
我创建了这个小型基准来比较这些方法的运行时性能:
#include <chrono>
#include <functional>
#include <cstdio>
class Finally1 {
std::function<void(void)> _functor;
public:
Finally1(const std::function<void(void)> &functor) : _functor(functor) {}
~Finally1() {
_functor();
}
};
void BenchmarkFunctor() {
volatile int64_t var = 0;
const int64_t nIterations = 234567890;
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 0; i < nIterations; i++) {
Finally1 doFinally([&] {
var++;
});
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("Functor: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
}
void BenchmarkObject() {
volatile int64_t var = 0;
const int64_t nIterations = 234567890;
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 0; i < nIterations; i++) {
class Cleaner {
volatile int64_t* _pVar;
public:
Cleaner(volatile int64_t& var) : _pVar(&var) { }
~Cleaner() { (*_pVar)++; }
} c(var);
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("Object: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
}
void BenchmarkMSVCpp() {
volatile int64_t var = 0;
const int64_t nIterations = 234567890;
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 0; i < nIterations; i++) {
__try {
}
__finally {
var++;
}
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("__finally: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
}
template <typename Func> class Finally4 {
Func f;
public:
Finally4(Func&& func) : f(std::forward<Func>(func)) {}
~Finally4() { f(); }
};
template <typename F> Finally4<F> MakeFinally4(F&& f) {
return Finally4<F>(std::forward<F>(f));
}
void BenchmarkTemplate() {
volatile int64_t var = 0;
const int64_t nIterations = 234567890;
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 0; i < nIterations; i++) {
auto doFinally = MakeFinally4([&] { var++; });
//Finally4 doFinally{ [&] { var++; } };
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("Template: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
}
void BenchmarkEmpty() {
volatile int64_t var = 0;
const int64_t nIterations = 234567890;
auto start = std::chrono::high_resolution_clock::now();
for (int64_t i = 0; i < nIterations; i++) {
var++;
}
auto elapsed = std::chrono::high_resolution_clock::now() - start;
double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
printf("Empty: %.3lf Ops/sec, var=%lld\n", nIterations / nSec, (long long)var);
}
int __cdecl main() {
BenchmarkFunctor();
BenchmarkObject();
BenchmarkMSVCpp();
BenchmarkTemplate();
BenchmarkEmpty();
return 0;
}
我的 Ryzen 1800X @3.9Ghz 和 DDR4 @2.6Ghz CL13 的结果是:
Functor: 175148825.946 Ops/sec, var=234567890
Object: 553446751.181 Ops/sec, var=234567890
__finally: 553832236.221 Ops/sec, var=234567890
Template: 554964345.876 Ops/sec, var=234567890
Empty: 554468478.903 Ops/sec, var=234567890
显然,除了 functor-base (#1) 之外的所有选项都和空循环一样快。
那么是否有一种快速而强大的 C++ 替代方案 finally
,它是可移植的并且需要从外部函数的堆栈中进行最少的复制?
更新:我已经对@Jarod42 解决方案进行了基准测试,所以这里的问题是更新的代码和输出。尽管如@Sopel 所述,如果不执行复制省略,它可能会中断。
更新 2:澄清我的要求是在 C++ 中执行代码块的一种方便快捷的方法,即使抛出异常也是如此。由于问题中提到的原因,有些方法很慢或不方便。
您可以实现 Finally
而无需类型擦除和 std::function
的开销:
template <typename F>
class Finally {
F f;
public:
template <typename Func>
Finally(Func&& func) : f(std::forward<Func>(func)) {}
~Finally() { f(); }
Finally(const Finally&) = delete;
Finally(Finally&&) = delete;
Finally& operator =(const Finally&) = delete;
Finally& operator =(Finally&&) = delete;
};
template <typename F>
Finally<F> make_finally(F&& f)
{
return { std::forward<F>(f) };
}
并像这样使用它:
auto&& doFinally = make_finally([&] { var++; });
好吧,是你的基准测试失败了:它实际上并没有抛出,所以你只看到了非异常路径。这是非常糟糕的,因为优化器可以证明您没有抛出异常,因此它可以丢弃所有实际处理执行清理的代码,但在运行中出现异常。
我认为,您应该重复您的基准测试,将对 exceptionThrower()
或 nonthrowingThrower()
的调用放入您的 try{}
块中。这两个函数应作为单独的翻译单元编译,并且仅 link 与基准代码一起编辑。这将强制编译器实际生成异常处理代码,而不管您是调用 exceptionThrower()
还是 nonthrowingThrower()
。 (确保您没有打开 link 时间优化,这可能会破坏效果。)
这还可以让您轻松比较异常和非抛出执行路径之间的性能影响。
除了基准问题外,C++ 中的异常很慢。你永远不会在一秒钟内抛出数亿个异常。它充其量大约是个位数百万,可能更少。我希望不同 finally
实现之间的任何性能差异在投掷情况下完全不相关。您可以优化的是非抛出路径,其中您的成本只是 finally
实现对象的 construction/destruction,无论它是什么。