.NET 异常有多慢?

我不想讨论什么时候抛出异常,什么时候不抛出异常。我想解决一个简单的问题。99% 的时候,不抛出异常的理由都是因为它们太慢,而另一方声称(基准测试)速度不是问题。我读过无数的博客,文章,和关于这一方或那一方的帖子。到底是什么?

一些答案的链接: 飞碟Mariani伯明翰

32623 次浏览

在发布模式下,开销是最小的。

除非您打算以递归的方式使用流控制异常(例如,非本地出口) ,否则我怀疑您是否能够注意到其中的差异。

我站在“不慢”的一边——或者更准确地说“不够慢,不值得在正常使用时避免使用它们”。我已经写了两个 太短了 物品关于这一点。对于基准测试方面有一些批评,这些批评主要集中在“在现实生活中会有更多的堆栈需要浏览,所以你会破坏高速缓存等等”——但是使用错误代码在堆栈上运行会破坏高速缓存,所以我不认为这是一个特别好的论点。

只是为了澄清一下——我不支持在不符合逻辑的地方使用异常。例如,int.TryParse完全适合于从用户转换数据。当读取机器生成的文件时,这是不合适的,因为失败意味着“文件没有达到它应该达到的格式,我真的不想尝试处理这个问题,因为我不知道还有什么可能是错误的。”

当在“仅在合理的情况下”使用异常时,我从未见过哪个应用程序的性能受到异常的显著损害。基本上,除非您有重大的正确性问题,否则不应该经常发生异常,如果您有重大的正确性问题,那么性能并不是您面临的最大问题。

我所理解的论点并不是抛出异常是不好的,它们本身就是缓慢的。相反,它是关于使用 throw/catch 结构作为控制普通应用程序逻辑的第一类方法,而不是更传统的条件结构。

通常在正常的应用程序逻辑中执行循环,其中相同的操作重复数千次/数百万次。在这种情况下,通过一些非常简单的分析(参见 Stopwatch 类) ,您可以亲眼看到抛出异常而不是简单的 if 语句可能会大大减慢速度。

事实上我曾经读到过。NET 团队介绍了 TryXXXXX 方法。NET 2.0的许多基础 FCL 类型,特别是因为客户抱怨他们的应用程序的性能是如此之慢。

在许多情况下,这是因为客户试图在循环中进行值的类型转换,但每次尝试都以失败告终。抛出一个转换异常,然后由一个异常处理程序捕获,该处理程序随后吞噬该异常并继续循环。

Microsoft 现在建议特别在这种情况下使用 TryXXX 方法,以避免这种可能的性能问题。

我可能是错的,但听起来你似乎并不确定你所读到的“基准”的准确性。简单的解决办法: 自己试试。

我从来没有遇到过任何异常的性能问题。我经常使用异常——如果可以,我从不使用返回代码。他们是一个坏习惯,在我看来,闻起来像意大利面条代码。

我认为这一切都归结为如何使用异常: 如果你像使用返回代码一样使用它们(堆栈中的每个方法调用捕获和重新抛出) ,那么,是的,它们会很慢,因为每个捕获/抛出都有开销。

但是如果您在堆栈底部抛出并在顶部捕获(您用一个 throw/catch 替换整个返回代码链) ,那么所有代价高昂的操作都会完成一次。

归根结底,它们是一种有效的语言特征。

只是为了证明我的观点

请运行 在这个链接的代码(对于一个答案来说太大了)。

我电脑上的结果是:

Marco@sklivvz: ~/development/test $mono Exceptions.exe | grep PM
2008年2月10日下午2:53:32
2008年2月10日下午2:53:42
2008年2月10日下午2:53:52

时间戳在开头输出,在返回码和异常之间,在结尾输出。两种情况下花的时间是一样的。请注意,您必须使用优化进行编译。

如果你把它们和返回码相比,它们就慢得要命。然而,正如之前的海报所说,你不希望抛出正常的程序操作,所以只有当出现问题时,你才会得到 perf 命中,在绝大多数情况下,性能不再重要(因为例外意味着路障无论如何)。

他们绝对值得使用超过错误代码,其优势是巨大的 IMO。

对于这个问题有一个明确的答案,那就是实施它们的人——克里斯•布鲁姆(Chris Brumme)。他写了一篇关于这个主题的 优秀的博客文章文章(警告——很长)(警告2——写得很好,如果你是一个技术人员,你会把它读到底,然后在下班后补上你的时间:)

执行摘要: 他们行动缓慢。它们被实现为 Win32SEH 异常,所以有些甚至会通过环0 CPU 边界! 显然,在现实世界中,你会做很多其他的工作,所以奇怪的异常根本不会被注意到,但是如果你使用它们的程序流预计你的应用程序被锤。这是微软营销机器对我们造成伤害的另一个例子。我记得有一家微软公司告诉我们,他们的开销绝对为零,这完全是胡说八道。

克里斯引用了一句中肯的话:

事实上,CLR 在内部使用 即使在非托管的 部分引擎。然而, 有一个严重的长期 带有异常的性能问题 这一点必须考虑到你的 决定。

我完全不知道人们在说什么,当他们说他们只有在被扔的时候才会慢。

编辑: 如果没有抛出 Exception,那意味着您正在执行新的 Exception ()或类似的操作。否则,异常将导致线程挂起,并对堆栈进行遍历。这可能在较小的情况下没问题,但在高流量的网站,依赖异常作为工作流或执行路径机制肯定会导致性能问题。异常本身并不坏,而且对于表示异常条件非常有用

中的异常工作流。NET 应用程序使用第一次和第二次机会异常。对于所有异常,即使您正在捕获和处理它们,仍然会创建异常对象,框架仍然需要遍历堆栈来查找处理程序。当然,如果你捕获并重新抛出一个第一次机会的异常,捕获它,重新抛出,导致另一个第一次机会的异常,然后没有找到一个处理程序,然后导致第二次机会的异常。

异常也是堆上的对象——因此,如果抛出大量异常,那么会导致性能和内存问题。

此外,根据我的 ACE 团队撰写的“性能测试 Microsoft. NET Web 应用程序”副本:

”异常处理代价高昂。当 CLR 递归通过调用堆栈寻找正确的异常处理程序时,相关线程的执行被暂停,当它被找到时,异常处理程序和一些 finally 块都必须有机会执行,然后才能执行常规处理。”

我自己在该领域的经验表明,减少异常对性能有很大帮助。当然,在进行性能测试时还需要考虑其他因素——例如,如果磁盘 I/O 中断,或者查询在几秒钟内完成,那么这应该是您的关注点。但是,发现和消除异常应该是该战略的重要组成部分。

我的 XMPP 服务器获得了很大的速度提升(抱歉,没有实际的数字,纯粹是观察) ,因为我一直试图阻止它们发生(比如在尝试读取更多数据之前检查一个套接字是否连接) ,并给自己提供了避免它们的方法(提到的 TryX 方法)。只有大约50个活跃的(聊天)虚拟用户。

这里有一个关于捕捉异常相关性能的简短说明。

当执行路径进入一个“ try”块时,没有什么神奇的事情发生。没有“ try”指令,也没有与进入或退出 try 块相关的开销。有关 try 块的信息存储在方法的元数据中,每当引发异常时,都会在运行时使用该元数据。执行引擎在堆栈中遍历,查找 try 块中包含的第一个调用。只有在引发异常时,才会发生与异常处理相关的开销。

只是为了在这个讨论中加入我自己最近的经验: 与上面所写的大部分内容一致,我发现在重复执行时抛出异常的速度非常慢,即使没有运行调试器也是如此。我只是通过改变大约5行代码,将我正在编写的一个大型程序的性能提高了60% : 切换到返回代码模型,而不是抛出异常。当然,有问题的代码运行了成千上万次,并且在我更改它之前可能抛出了成千上万个异常。因此,我同意上面的陈述: 当某些重要的事情实际上出错时抛出异常,而不是作为在任何“预期”情况下控制应用程序流的方法。

但 mono 抛出异常的速度是.net 独立模式的10倍, 和.net 独立模式抛出异常的速度比.net 调试器模式快60倍。 (测试机具有相同的 CPU 模型)

int c = 1000000;
int s = Environment.TickCount;
for (int i = 0; i < c; i++)
{
try { throw new Exception(); }
catch { }
}
int d = Environment.TickCount - s;


Console.WriteLine(d + "ms / " + c + " exceptions");

在 Windows CLR 中,对于 deep-8调用链,抛出异常比检查和传播返回值慢750倍。(基准见下文)

异常的高成本是因为 windows CLR 集成了 Windows 结构化异常处理。这样就可以正确地捕获异常,并在不同的运行时和语言之间引发异常。然而,这是非常非常缓慢的。

Mono 运行时中的异常(在任何平台上)要快得多,因为它不与 SEH 集成。然而,在跨多个运行时传递异常时会有功能损失,因为它没有使用任何类似 SEH 的东西。

下面是我的 WindowsCLR 异常与返回值基准测试的缩略结果。

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));         }
}
}
}




}

当编写类/函数供其他人使用时,似乎很难说什么时候异常是合适的。BCL 中有一些有用的部分我不得不丢弃并使用 pcall,因为它们抛出异常而不是返回错误。对于某些情况,您可以绕过它,但对于其他类似 System 的情况。管理和性能计数器有一些用法,需要执行 BCL 频繁抛出异常的循环。

如果您正在编写一个库,并且您的函数有可能在循环中使用,并且存在大量迭代的可能性,请使用 Try。.模式或其他方法来暴露异常旁边的错误。即便如此,如果在共享环境中有很多进程在使用函数,也很难说它会被调用多少次。

在我自己的代码中,只有当事情非常异常,需要查看堆栈跟踪并查看哪里出了错,然后修复它时,才会引发异常。因此,我基本上已经重写了 BCL 的一些部分,以使用基于 Try 的错误处理。.模式而不是异常。

我想你已经回答了自己的问题。你和几乎所有了解它们的人都知道它们很慢。这是100% 的事实,但正如许多其他人指出的那样,上下文是100% 决定何时使用它们的因素。编写非服务器应用程序?你不会注意到有什么不同的。编写一个网站公共 API,其中一个错误的客户端请求可以触发一个异常的后端?这对于一个数量级来说是灾难的配方,因为它是请求/秒数的乘积。后端固定的次数比杂货店里的便士小马还多。但问题是,BCL/其他库会抛出您无法控制的异常,因此您必须使用中间人/交叉保护工具,在这些异常到达 BCL 之前触发这些异常。有些案子你根本没有任何辩护理由。比如使用 MongoClient 连接 MongoDB 数据库。所有的蒙古文集。异步函数会抛出异常,如果他们没有成功在某些情况下,但它不抛出很多,我很肯定这些情况是在罕见的一端(这转移到情况的上下文部分)。但我也可能错了。我只能假设他们只在极少数情况下投掷。正如你指出的,你知道它们很慢,所以只有在需要事情不慢的上下文中使用它们才是常识。简单明了。