.NET异常有多慢?

151
我不想讨论何时抛出异常和何时不抛出异常的问题,我希望解决一个简单的问题。99%的时间里,不抛出异常的理由是它们很慢,而另一方则声称(通过基准测试)速度不是问题。我已经阅读了许多关于这个问题的博客、文章和帖子,那么到底是哪种情况呢?
答案中提供了一些链接:SkeetMarianiBrumme

14
有谎言,该死的谎言和基准测试。 :) - gbjbaanb
1
不幸的是,这里有几个得票很高的答案忽略了问题所问的“异常有多慢?”并明确要求避免讨论何时使用它们的话题。对于实际提出的问题,一个简单的答案是......在Windows CLR上,异常比返回值慢750倍。 - David Jeske
14个回答

216

我倾向于使用异常,或者更准确地说,除非在正常使用时执行他们会变得太慢而不值得使用,否则我不会避免它们。我写了两篇短文文章来阐述这个观点。对基准测试方面的批评主要是“在现实生活中,会有更多的堆栈需要经过,因此你会清空缓存等”- 但使用错误代码来逐层处理堆栈也会使缓存失效,所以我不认为这是一个特别好的论据。

仅仅为了明确 - 我并不支持在不合适的情况下使用异常。例如,int.TryParse 完全适用于将用户的数据进行转换。当读取机器生成的文件时,如果失败意味着“该文件的格式不正确”,那么使用异常就是不合适的,因为我真的不想尝试处理它,因为我不知道还可能有什么其他问题。

在“仅仅合理的情况”下使用异常,我从未见过应用程序的性能因异常而受到显著影响。基本上,异常不应经常发生,除非你有重大的正确性问题,而如果你有重大的正确性问题,那么性能不是你面临的最大问题。


6
是的,人们一定要意识到在不恰当地使用异常时会有性能成本。我只是认为当它们被适当地使用时,这就不是一个问题 :) - Jon Skeet
10
如果你每秒钟有200多个异常,我会说你滥用了异常。如果它每秒钟发生200次,显然这不是一个“异常”事件。请注意答案的最后一句话:“基本上,除非你有重大的正确性问题,否则异常不应该经常发生,而如果你有重大的正确性问题,那么性能不是你面临的最大问题。” - Jon Skeet
4
每秒超过200次的情况比人们想象的要更常见。我曾在许多公司进行调试,几乎每个高流量站点都会遇到这个问题。我曾看到一个拥有约30个四核服务器的站点一度达到1000/s!(其中只有六个处理直接Web流量的DMZ...)从经验来看,异常很慢,但像反射一样,如果它不经常发生,那就没什么大不了的。 - Paul Lockwood
5
我的观点是,如果你每秒钟有200多个异常,那很可能已经表明你正在滥用异常。这并不让我感到惊讶,但这意味着我首先关注的不是性能问题,而是异常的滥用问题。一旦我删除了所有不适当使用异常的情况,我就不会指望它们在性能方面扮演重要角色。 - Jon Skeet
5
你错过了答案的要点。显然,抛出异常比返回普通值慢得多。没有人争论这一点。问题是它们是否慢了。如果你正处于适合抛出异常的情况那导致了性能问题,那么你可能有更大的问题——因为这表明你的系统存在着很多错误。通常,问题实际上是你最初使用了不适当的异常处理方式。 - Jon Skeet
显示剩余14条评论

37

这个问题有一个权威的答案,来自实现它们的人 - Chris Brumme。他写了一篇优秀的博客文章,讲述了这个主题(警告-非常长)(警告2-写得非常好,如果你是技术人员,你会读到最后,然后不得不在下班后补回工作时间 :))

执行摘要:它们很慢。它们被实现为Win32 SEH异常,因此有些甚至会通过ring 0 CPU边界! 显然,在现实世界中,你会做很多其他工作,所以偶尔的异常根本不会被注意到,但如果你用它们来控制程序流程,那么你的应用程序将受到打击。这是微软营销机器给我们带来的另一个负面影响。我记得有一个微软员工告诉我们,他们完全没有任何开销,这完全是胡说八道。

Chris给出了一个相关的引用:

事实上,CLR甚至在未管理的引擎部分内部使用异常。但是,异常存在严重的长期性能问题,这必须考虑到您的决策中。


我可以在现实世界的测试中提到这个,在那里可空类型导致异常在“这是正常程序流程”的情况下被触发多次,最终导致了显著的性能问题。请记住,异常只用于非常规情况,不要相信任何人说不是这样,否则你会遇到类似那个Github线程的问题! - gbjbaanb

9

当人们说只有在抛出异常的情况下才会变慢时,我不知道他们在说什么。

编辑:如果没有抛出异常,则意味着您正在执行new Exception()或类似操作。否则,异常会导致线程被挂起,并且需要遍历堆栈。在小型情况下可能还可以接受,但在高流量网站中,依赖异常作为工作流或执行路径机制肯定会导致性能问题。异常本身并不是坏的,它们用于表示异常情况。

.NET应用程序中的异常工作流程使用第一次和第二次异常。对于所有异常情况,即使您捕获并处理它们,异常对象仍然会被创建,并且框架仍然必须遍历堆栈以查找处理程序。如果您捕获并重新抛出异常,这当然需要更长时间-您将获得第一次机会异常,捕获它,重新抛出它,导致另一个第一次机会异常,然后找不到处理程序,然后导致第二次机会异常。

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

此外,根据ACE团队编写的“性能测试Microsoft .NET Web应用程序”中的内容:

“异常处理很耗费资源。执行相关线程时,CLR会暂停,遍历调用堆栈以查找正确的异常处理程序,当它被找到时,异常处理程序和若干个finally块都必须有机会执行,然后才能执行常规处理。”

我在现场的经验表明,显著减少异常可以提高性能。当然,在性能测试时还有其他要考虑的因素 - 例如,如果您的磁盘I / O有问题或您的查询需要数秒钟,则应该将其作为重点。但是,查找并消除异常应该成为该策略的重要组成部分。


1
你所写的内容并没有反驳异常只有在抛出时才会变慢这一说法。你只是谈到了它们被抛出的情况。当你通过移除异常来“显著提高性能”时: 1)它们是真正的错误条件,还是仅仅是用户错误? - Jon Skeet
你是在调试器下运行,还是没有? - Jon Skeet
顺便问一下,你的客户抛出了多少个异常?别忘了默认情况下Response.Redirect会抛出一个异常... - Jon Skeet
4
我知道 - 我曾经是微软公司的首席团队成员。 :) 让我们这样说,在某些极端情况下,我们看到了每秒数千个异常。没有什么比连接到实时调试器并能够以你能读取的速度查看异常更让人兴奋的了。异常处理很慢 - 连接到数据库也很慢,所以你只有在确有必要时才会这样做。 - Cory Foy
6
Cory,我认为“只有在抛出异常时才变慢”这一点的意思是,由于存在catch/finally块,你不必担心性能问题。也就是说,它们本身并不会导致性能损失,只有实际发生异常实例才会。 - Ian Horwill
显示剩余3条评论

7
据我了解,争论的焦点并不在于抛出异常本身是缓慢的,而是关于使用throw/catch结构作为控制正常应用程序逻辑的一种主要方式,而不是更传统的条件结构。
通常,在正常应用程序逻辑中,您会执行循环操作,其中相同的操作重复执行数千/数百万次。 在这种情况下,通过一些非常简单的分析(请参见Stopwatch类),您可以自行看到,与简单的if语句相比,抛出异常可能会明显地变慢。
事实上,我曾经读到过微软.NET团队在.NET 2.0中引入TryXXXXX方法到许多基础FCL类型中,特别是因为客户抱怨他们的应用程序性能太慢。
原来在许多情况下,这是因为客户正在尝试在循环中转换值,每次尝试都失败了。 引发了一个转换异常,然后被一个异常处理程序捕获,然后吞噬了该异常并继续了循环。
微软现在建议在这种情况下使用TryXXX方法,以避免可能的性能问题。
我可能错了,但是你似乎对你所读到的“基准测试”的真实性不确定。 简单的解决方案:自己试试。

我认为在内部,那些“try”函数也使用异常处理机制? - greg
1
这些“Try”函数在解析输入值失败时不会在内部抛出异常。但是,它们仍然会在其他错误情况下抛出异常,例如ArgumentException。 - Ash
1
我认为这个答案比其他任何答案都更接近问题的核心。说“只在合理情况下使用异常”并没有真正回答问题——真正的洞见是,使用C#异常控制流比通常的条件结构要慢得多。如果你认为不然也无可厚非。在OCaml中,异常或多或少就是GOTO,也是使用命令式特性时实现_break_的一种可接受方式。在我的特定情况中,将紧密循环中的_int.Parse()_加上_try/catch_替换为_int.TryParse()_,可以显著提高性能。 - Hugh W

4

我尝试了一些方法来防止XMPP服务器出现性能问题(比如在尝试读取更多数据之前检查套接字是否已连接),并给自己提供了避免这些问题的途径(如TryX方法)。经过观察,我的XMPP服务器的速度得到了显著提升(抱歉,没有实际数字)。这是在只有约50个活跃用户(聊天)的情况下实现的。


3
数字会很有用,可惜没有:(。像套接字操作这样的事情应该远远超过异常成本,特别是在不进行调试时。如果你有充分的基准测试结果,我会非常感兴趣看到它们。 - Jon Skeet

3

我想补充一下我的最近经验:与上面大部分所写的一致,即使没有调试器运行,反复抛出异常也会非常慢。通过改变五行左右的代码,我切换到了返回码模型,将一个正在编写的大程序的性能提高了60%。当然,我更改之前的代码运行了数千次,并且在更改之前可能会抛出数千个异常。所以我同意上面的说法:只有在真正出现问题时才抛出异常,而不是作为控制应用程序流程的一种方式来处理“预期”的情况。


3

但是 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");

2
在Windows CLR上,对于深度为8的调用链,抛出异常的速度比检查和传播返回值慢750倍(请参见下面的基准测试)。
异常的高成本是因为Windows CLR与称为Windows Structured Exception Handling的东西集成。这使得不同的运行时和语言可以正确地捕获和抛出异常。然而,它非常非常慢。
在任何平台上的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));         }
        }
    }
}


}

6
除了没有回答问题的重点外,切勿使用DateTime.Now进行基准测试,应该使用Stopwatch,它专门用于测量经过的时间。尽管您正在测量相当长的时间段,但这并不是问题,但养成好习惯尤为重要。 - Jon Skeet
相反,问题是“异常是否慢”,句号。它明确要求避免讨论何时抛出异常,因为这个话题会掩盖事实。异常的性能如何? - David Jeske

2
我从未遇到过异常的性能问题。我经常使用异常——如果可以的话,我从不使用返回码。它们是一种糟糕的实践,在我看来,就像意大利面条代码。
我认为这归结于你如何使用异常:如果你像返回码一样使用它们(堆栈中的每个方法调用都会捕获并重新抛出),那么它们会很慢,因为每个单独的捕获/抛出都有开销。
但是,如果你在堆栈底部抛出异常并在顶部捕获(用一个抛出/捕获替换整个返回码链),所有昂贵的操作都只需执行一次。
最终,它们是一种有效的语言特性。
只是为了证明我的观点,请运行此链接中的代码(太大了无法放在答案中)。
我的电脑上的结果:
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
时间戳在开头、返回码和异常之间、结尾处输出。在两种情况下所需时间相同。请注意,您必须使用优化编译。

2

如果将它们与返回代码进行比较,它们的速度非常慢。然而,正如之前的帖子所述,您不希望在正常程序操作中抛出异常,因此只有在出现问题时才会受到性能影响,在绝大多数情况下,性能已经不再重要(因为异常意味着阻碍)。

相对于错误代码,它们绝对值得使用,优点众多,我个人认为。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接