我不想讨论何时以及不引发异常。我希望解决一个简单的问题。99%的情况下,不引发异常的论点围绕它们的缓慢进行,而另一方声称(通过基准测试)速度不是问题。我已经阅读了许多有关某一方面的博客,文章和帖子。那是什么呢?
我不想讨论何时以及不引发异常。我希望解决一个简单的问题。99%的情况下,不引发异常的论点围绕它们的缓慢进行,而另一方声称(通过基准测试)速度不是问题。我已经阅读了许多有关某一方面的博客,文章和帖子。那是什么呢?
Answers:
我站在“不慢”的一边,或者更确切地说是“不够慢,不值得在正常使用中避免使用它们”。我已经写了两篇有关此的简短 文章。对基准方面存在批评,这主要归结为“在现实生活中,有更多的堆栈要通过,因此您会浪费高速缓存等”-但是使用错误代码沿堆栈向上移动也将破坏缓存,所以我认为这不是一个特别好的论点。
为了清楚起见,我不支持使用不合逻辑的异常。例如,int.TryParse
完全适合转换用户的数据。当读取机器生成的文件时,这是适当的,失败的意思是“该文件的格式不正确,我真的不想尝试处理该文件,因为我不知道还有什么问题。 ”
在“仅在合理的情况下”使用例外时,我从未见过一个性能受到例外严重影响的应用程序。基本上,除非您遇到重大的正确性问题,否则异常不应该经常发生;如果您遇到重大的正确性问题,那么性能并不是您面临的最大问题。
实施它们的人Chris Brumme对此有一个明确的答案。他写了一篇很棒的博客文章有关该主题(警告-很长)(警告2-很好写,如果您是技术人员,则需要阅读到末尾,然后必须花点时间工作:) )
摘要:它们很慢。它们被实现为Win32 SEH异常,因此有些甚至会超过Ring 0 CPU边界!显然,在现实世界中,您将做很多其他工作,因此根本不会注意到奇怪的异常,但是,如果将它们用于程序流,则期望您的应用程序受到重创。这是MS营销机器给我们造成损害的另一个示例。我记得一位microsoftie告诉我们他们是如何产生绝对零开销的,这完全是tosh。
克里斯给出了一个相关的报价:
实际上,即使在引擎的非托管部分,CLR在内部也使用异常。但是,存在一个严重的长期性能问题,但有例外情况,必须将此因素纳入您的决策。
我不知道人们说什么才慢,被抛出就在说什么。
编辑:如果未引发异常,则意味着您正在执行新的Exception()或类似的操作。否则,异常将导致线程被挂起,并且堆栈被遍历。在较小的情况下,这可能是可以的,但是在高流量的网站中,依靠异常作为工作流或执行路径机制肯定会导致性能问题。异常本身并不坏,并且对于表达异常条件很有用
.NET应用程序中的异常工作流程使用第一次和第二次机会异常。对于所有异常,即使您正在捕获并处理它们,仍会创建异常对象,并且框架仍必须遍历堆栈以查找处理程序。如果捕获并重新抛出当然会花费更长的时间-您将获得一个优先机会异常,将其捕获并重新抛出,从而导致另一个优先机会异常,然后该异常找不到处理程序,然后导致第二次机会异常。
异常也是堆上的对象-因此,如果您抛出大量异常,则将导致性能和内存问题。
此外,根据我由ACE团队撰写的“性能测试Microsoft .NET Web应用程序”的副本:
“异常处理非常昂贵。在搜索正确的异常处理程序时,CLR通过调用堆栈递归时,所涉及线程的执行将被挂起,并且当找到异常处理程序和一些finally块时,它们都必须有执行的机会。才能进行常规处理。”
我在该领域的经验表明,减少异常现象对性能有很大帮助。当然,在进行性能测试时,还需要考虑其他因素-例如,如果磁盘I / O被拍摄或查询在几秒钟之内,那么这应该成为您的重点。但是,发现和消除异常应该是该策略的重要组成部分。
据我了解,并不是说抛出异常是不好的,它们本身很慢。相反,它是将throw / catch构造用作控制常规应用程序逻辑的第一类方法,而不是更传统的条件构造。
通常,在正常的应用程序逻辑中,您会执行循环,在此循环中,相同的动作会重复数千/百万次。在这种情况下,通过一些非常简单的分析(请参见Stopwatch类),您可以亲自看到抛出异常而不是简单的if语句会变得很慢。
实际上,我曾经读过Microsoft的.NET团队将.NET 2.0中的TryXXXXX方法引入了许多基本FCL类型,特别是因为客户抱怨他们的应用程序性能太慢。
事实证明,在许多情况下,这是因为客户试图在循环中进行值的类型转换,而每次尝试均失败。引发了转换异常,然后由异常处理程序捕获了该异常,然后吞没了该异常并继续执行循环。
Microsoft现在建议特别在这种情况下使用TryXXX方法,以避免这种可能的性能问题。
我可能是错的,但听起来您不确定所阅读的“基准”的准确性。简单的解决方案:自己尝试一下。
我从来没有任何性能问题,例外。我经常使用异常-如果可能的话,我从不使用返回码。这是一种不好的做法,在我看来,它们的味道像意大利面条代码。
我认为一切都归结为您如何使用异常:如果您将异常用作返回码(堆栈中的每个方法调用都捕获并重新抛出),那么它们会很慢,因为每次捕获/抛出都有开销。
但是,如果您在堆栈的底部抛出并在顶部捕获(将整个返回代码链替换为一个throw / catch),则所有昂贵的操作都将执行一次。
归根结底,它们是有效的语言功能。
只是为了证明我的观点
我计算机上的结果:
marco@sklivvz:~/develop/test$ mono Exceptions.exe | grep PM
10/2/2008 2:53:32 PM
10/2/2008 2:53:42 PM
10/2/2008 2:53:52 PM
时间戳在开始时输出,在返回码和异常之间,最后输出。两种情况都需要花费相同的时间。请注意,您必须进行优化编译。
在Windows CLR上,对于深度为8的调用链,抛出异常比检查和传播返回值要慢750倍。(请参阅下面的基准)
这种异常的高昂代价是因为Windows CLR与称为Windows结构化异常处理的东西集成在一起。这样可以在不同的运行时和语言之间正确捕获和引发异常。但是,它非常非常慢。
Mono运行时(在任何平台上)的异常要快得多,因为它不与SEH集成。但是,跨多个运行时传递异常时会丢失功能,因为它不使用SEH之类的东西。
这是我的Windows CLR异常与返回值基准测试的简短结果。
baseline: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 13.0007 ms
baseline: recurse_depth 8, error_freqeuncy 0.25 (0), time elapsed 13.0007 ms
baseline: recurse_depth 8, error_freqeuncy 0.5 (0), time elapsed 13.0008 ms
baseline: recurse_depth 8, error_freqeuncy 0.75 (0), time elapsed 13.0008 ms
baseline: recurse_depth 8, error_freqeuncy 1 (0), time elapsed 14.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0 (0), time elapsed 13.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0.25 (249999), time elapsed 14.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0.5 (499999), time elapsed 16.0009 ms
retval_error: recurse_depth 5, error_freqeuncy 0.75 (999999), time elapsed 16.001 ms
retval_error: recurse_depth 5, error_freqeuncy 1 (999999), time elapsed 16.0009 ms
retval_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 20.0011 ms
retval_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 21.0012 ms
retval_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 24.0014 ms
retval_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 24.0014 ms
retval_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 24.0013 ms
exception_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 31.0017 ms
exception_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 5607.3208 ms
exception_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 11172.639 ms
exception_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 22297.2753 ms
exception_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 22102.2641 ms
这是代码。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
public class TestIt {
int value;
public class TestException : Exception { }
public int getValue() {
return value;
}
public void reset() {
value = 0;
}
public bool baseline_null(bool shouldfail, int recurse_depth) {
if (recurse_depth <= 0) {
return shouldfail;
} else {
return baseline_null(shouldfail,recurse_depth-1);
}
}
public bool retval_error(bool shouldfail, int recurse_depth) {
if (recurse_depth <= 0) {
if (shouldfail) {
return false;
} else {
return true;
}
} else {
bool nested_error = retval_error(shouldfail,recurse_depth-1);
if (nested_error) {
return true;
} else {
return false;
}
}
}
public void exception_error(bool shouldfail, int recurse_depth) {
if (recurse_depth <= 0) {
if (shouldfail) {
throw new TestException();
}
} else {
exception_error(shouldfail,recurse_depth-1);
}
}
public static void Main(String[] args) {
int i;
long l;
TestIt t = new TestIt();
int failures;
int ITERATION_COUNT = 1000000;
// (0) baseline null workload
for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {
int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);
failures = 0;
DateTime start_time = DateTime.Now;
t.reset();
for (i = 1; i < ITERATION_COUNT; i++) {
bool shoulderror = (i % EXCEPTION_MOD) == 0;
t.baseline_null(shoulderror,recurse_depth);
}
double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
Console.WriteLine(
String.Format(
"baseline: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
recurse_depth, exception_freq, failures,elapsed_time));
}
}
// (1) retval_error
for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {
int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);
failures = 0;
DateTime start_time = DateTime.Now;
t.reset();
for (i = 1; i < ITERATION_COUNT; i++) {
bool shoulderror = (i % EXCEPTION_MOD) == 0;
if (!t.retval_error(shoulderror,recurse_depth)) {
failures++;
}
}
double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
Console.WriteLine(
String.Format(
"retval_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
recurse_depth, exception_freq, failures,elapsed_time));
}
}
// (2) exception_error
for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {
int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);
failures = 0;
DateTime start_time = DateTime.Now;
t.reset();
for (i = 1; i < ITERATION_COUNT; i++) {
bool shoulderror = (i % EXCEPTION_MOD) == 0;
try {
t.exception_error(shoulderror,recurse_depth);
} catch (TestException e) {
failures++;
}
}
double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
Console.WriteLine(
String.Format(
"exception_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
recurse_depth, exception_freq, failures,elapsed_time)); }
}
}
}
}
在此,快速捕获与捕获异常相关的性能。
当执行路径进入“ try”块时,没有任何神奇的事情发生。没有“ try”指令,也没有与进入或退出try块相关的成本。有关try块的信息存储在方法的元数据中,并且在运行时会在引发异常时使用此元数据。执行引擎在堆栈中向下移动,以查找try块中包含的第一个调用。仅当引发异常时,才会发生与异常处理相关的所有开销。
在编写供他人使用的类/函数时,似乎很难说出适当的例外情况。BCL有一些有用的部分,我不得不放弃并进行细化,因为它们会引发异常而不是返回错误。在某些情况下,您可以解决该问题,但在其他情况下,例如System.Management和Performance Counters,则需要在某些情况下执行循环,在该循环中BCL经常抛出异常。
如果您正在编写库,则极有可能在循环中使用您的函数,并且有可能发生大量迭代,请使用Try ..模式或其他方式将错误暴露在异常之外。即使这样,也很难说如果共享环境中的许多进程正在使用您的函数会调用多少。
在我自己的代码中,仅在异常情况异常时抛出异常,以至于有必要查看堆栈跟踪并查看出了什么问题然后进行修复。因此,我几乎已经重写了BCL的各个部分,以使用基于Try ..模式而不是异常的错误处理。