在开发软件时遇到的许多情况下,处理运行时错误非常重要-从错误的用户输入到损坏的网络数据包。如果用户突然下载PNG而不是PDF,或者在更新软件时断开了网络电缆的连接,则应用程序不会崩溃。用户期望该程序无论发生什么都会工作,并且可以在后台处理紧急情况,或者通过友好界面发送的消息让他选择解决问题的选项。异常处理可能是一个令人困惑,复杂的任务,并且对于许多C ++开发人员而言,这从根本上来说很重要,它会大大降低应用程序的速度。但是,与许多其他情况一样,有几种方法可以解决此问题。接下来,我们将深入研究C ++中的异常处理过程,处理其陷阱,并观察其如何影响应用程序的速度。另外,我们将研究可用于减少开销的替代方法。在本文中,我不会敦促您完全停止使用这些异常。应该应用它们,但是要在无法避免的情况下精确地应用它们:例如,如何报告在构造函数内部发生的错误?我们将主要考虑使用异常来处理运行时错误。使用我们将要讨论的替代方法,可以使您开发更可靠和易于维护的应用程序。快速性能测试
与控制程序进度的常规机制相比,C ++中的异常要慢多少?异常显然比简单的break或return操作慢。但是,让我们找出速度慢了多少!在下面的示例中,我们编写了一个简单的函数,该函数随机生成数字,并在检查一个生成的数字的基础上给出/不给出错误消息。我们测试了几种用于错误处理的实现选项:- 引发带有整数参数的异常。尽管这在实践中并未特别适用,但这是在C ++中使用异常的最简单方法。因此,我们消除了测试实施中的过度复杂性。
- 丢弃std :: runtime_error,它可以发送文本消息。与前一个选项不同,此选项在实际项目中使用更多。让我们来看看第二种选择是否会比第一种明显增加间接费用成本。
- 空收益。
- 返回C样式int错误代码
为了运行测试,我们使用了简单的Google基准测试库。她在一个周期内反复运行了每个测试。接下来,我将描述一切如何发生。不耐烦的读者可以立即跳转到结果。测试代码我们的超复杂随机数生成器:const int randomRange = 2;
const int errorInt = 0;
int getRandom() {
return random() % randomRange;
}
测试功能:
void exitWithBasicException() {
if (getRandom() == errorInt) {
throw -2;
}
}
void exitWithMessageException() {
if (getRandom() == errorInt) {
throw std::runtime_error("Halt! Who goes there?");
}
}
void exitWithReturn() {
if (getRandom() == errorInt) {
return;
}
}
int exitWithErrorCode() {
if (getRandom() == errorInt) {
return -1;
}
return 0;
}
就是这样,现在我们可以使用Google基准测试库了:
void BM_exitWithBasicException(benchmark::State& state) {
for (auto _ : state) {
try {
exitWithBasicException();
} catch (int ex) {
}
}
}
void BM_exitWithMessageException(benchmark::State& state) {
for (auto _ : state) {
try {
exitWithMessageException();
} catch (const std::runtime_error &ex) {
}
}
}
void BM_exitWithReturn(benchmark::State& state) {
for (auto _ : state) {
exitWithReturn();
}
}
void BM_exitWithErrorCode(benchmark::State& state) {
for (auto _ : state) {
auto err = exitWithErrorCode();
if (err < 0) {
}
}
}
BENCHMARK(BM_exitWithBasicException);
BENCHMARK(BM_exitWithMessageException);
BENCHMARK(BM_exitWithReturn);
BENCHMARK(BM_exitWithErrorCode);
BENCHMARK_MAIN();
对于那些想要触摸美丽的人,我们在这里发布了完整的代码。结果
在控制台中,根据编译选项,我们看到不同的测试结果:Debug -O0:------------------------------------------------------------------
Benchmark Time CPU Iterations
------------------------------------------------------------------
BM_exitWithBasicException 1407 ns 1407 ns 491232
BM_exitWithMessageException 1605 ns 1605 ns 431393
BM_exitWithReturn 142 ns 142 ns 5172121
BM_exitWithErrorCode 144 ns 143 ns 5069378
发行-O2:------------------------------------------------------------------
Benchmark Time CPU Iterations
------------------------------------------------------------------
BM_exitWithBasicException 1092 ns 1092 ns 630165
BM_exitWithMessageException 1261 ns 1261 ns 547761
BM_exitWithReturn 10.7 ns 10.7 ns 64519697
BM_exitWithErrorCode 11.5 ns 11.5 ns 62180216
(于2015年MacBook Pro 2.5GHz i7推出)结果令人惊叹!看看有无异常(空返回和errorCode)之间的代码速度之间的巨大差距。借助编译器优化,这个差距更大!无疑,这是一个伟大的考验。编译器可能会对测试3和4进行了大量的代码优化,但无论如何差距还是很大的。因此,这使我们能够估计使用异常时的开销。多亏了零成本模型,在大多数C ++中的try块实现中,都没有额外的开销。但是捕获块的工作要慢得多。通过简单的示例,我们可以看到如何缓慢地抛出和捕获异常。即使在这么小的调用堆栈上!随着堆栈深度的增加,开销将线性增加。这就是为什么设法捕获尽可能接近引发异常的代码如此重要的原因。好吧,我们发现异常工作缓慢。然后也许停止它?但是,并非一切都那么简单。为什么每个人都继续使用异常?
C ++性能技术报告(第5.4章)
中充分记录了异常的好处:, , errorCode- [ ], . , . , .
再一次:主要思想是无法忽略和忘记异常。这使异常成为非常强大的内置C ++工具,能够通过我们从C继承的错误代码来代替困难的处理。此外,异常在与程序不直接相关的情况下非常有用,例如,“硬盘已满”。 “或”网络电缆已损坏。在这种情况下,例外是理想的。但是,处理与程序直接相关的错误处理的最佳方法是什么?无论如何,我们都需要一种机制来明确地指示开发人员,他应该检查错误,为他提供有关该错误的足够信息(如果出现了此错误),以消息的形式或其他某种格式进行传输。似乎我们再次返回到内置异常,但是刚才我们将讨论替代解决方案。预期
除了开销之外,异常还有一个缺点:它们按顺序工作:异常一旦抛出就需要捕获并处理,不可能将其推迟到以后。我们对于它可以做些什么呢?事实证明有办法。 Andrei Alexandrescu教授为我们开设了一个特别课程,称为Expected。它允许您创建T类的对象(如果一切正常)或Exception类的对象(如果发生错误)。就是这个或那个。或或。本质上,这是对数据结构的包装,在C ++中将其称为联合。因此,我们举例说明他的想法:template <class T>
class Expected {
private:
union {
T value;
Exception exception;
};
public:
Expected(const T& value) ...
Expected(const Exception& ex) ...
bool hasError() ...
T value() ...
Exception error() ...
};
全面实施肯定会更加困难。但是Expected的主要思想是,在同一个对象(Expected&e)中,我们可以接收程序正常运行的数据(它将具有我们已知的类型和格式:T&值)和错误数据(例外和例外)。因此,很容易再次检查一下我们遇到的情况(例如,使用hasError方法)。但是,现在没有人会强迫我们在这一秒内处理异常。我们将没有throw()调用或catch块。相反,我们可以在方便时引用Exception对象。预期性能测试
我们将为新类编写类似的性能测试:
Expected<int> exitWithExpected() {
if (getRandom() == errorInt) {
return std::runtime_error("Halt! If you want...");
}
return 0;
}
void BM_exitWithExpected(benchmark::State& state) {
for (auto _ : state) {
auto expected = exitWithExpected();
if (expected.hasError()){
}
}
}
BENCHMARK(BM_exitWithExpected);
BENCHMARK_MAIN();
击鼓!!!调试-O0:------------------------------------------------------------------
Benchmark Time CPU Iterations
------------------------------------------------------------------
BM_exitWithExpected 147 ns 147 ns 4685942
发行-O2:------------------------------------------------------------------
Benchmark Time CPU Iterations
------------------------------------------------------------------
BM_exitWithExpected 57.5 ns 57.5 ns 11873261
不错!对于未经优化的std :: runtime_error,我们可以将操作时间从1605减少到147纳秒。通过优化,一切看起来都更好:从1261纳秒降至57.5纳秒。这比使用-O0快10倍以上,比使用-O2快20倍以上。因此,与内置异常相比,Expected的工作速度快了很多倍,并且还为我们提供了更灵活的错误处理机制。此外,它具有语义纯洁性,并且无需牺牲错误消息和剥夺用户的质量反馈。结论
例外并非绝对邪恶。有时这一点都是好事,因为它们在达到预期目的时会非常有效地工作:在特殊情况下。我们只有在使用更有效的解决方案的情况下,才开始遇到问题。尽管我们的测试非常简单,但我们的测试显示:如果在仅发送数据(使用return)就足够的情况下不捕获异常(缓慢的catch块),则可以大大减少开销。在本文中,我们还简要介绍了Expected类以及如何使用它来加快错误处理过程。Expected使跟踪程序的进度变得容易,也使我们更加灵活,可以将消息发送给用户和开发人员。