.NET异常有多慢?


143

我不想讨论何时以及不引发异常。我希望解决一个简单的问题。99%的情况下,不引发异常的论点围绕它们的缓慢进行,而另一方声称(通过基准测试)速度不是问题。我已经阅读了许多有关某一方面的博客,文章和帖子。那是什么呢?

答案中的一些链接:SkeetMarianiBrumme


13
有谎言,该死的谎言和基准。:)
gbjbaanb

不幸的是,这里有几个获得高度投票的答案都没有回答这个问题,即“例外情况有多慢?”,特别是被要求避免使用频率多久的话题。一个实际回答的问题的简单答案是.....在Windows CLR上,异常比返回值慢750倍。
David Jeske '16

Answers:


207

我站在“不慢”的一边,或者更确切地说是“不够慢,不值得在正常使用中避免使用它们”。我已经写了两篇有关此的简短 文章。对基准方面存在批评,这主要归结为“在现实生活中,有更多的堆栈要通过,因此您会浪费高速缓存等”-但是使用错误代码沿堆栈向上移动将破坏缓存,所以我认为这不是一个特别好的论点。

为了清楚起见,我不支持使用不合逻辑的异常。例如,int.TryParse完全适合转换用户的数据。当读取机器生成的文件时,这是适当的,失败的意思是“该文件的格式不正确,我真的不想尝试处理该文件,因为我不知道还有什么问题。 ”

在“仅在合理的情况下”使用例外时,我从未见过一个性能受到例外严重影响的应用程序。基本上,除非您遇到重大的正确性问题,否则异常不应该经常发生;如果您遇到重大的正确性问题,那么性能并不是您面临的最大问题。


2
不幸的是,人们被告知,异常是免费的,可以将它们用于琐碎的“正确”功能,当您出错时,应按照您所说的使用它们-在“特殊”情况下
gbjbaanb

4
是的,人们当然应该意识到,不当使用异常会导致性能损失。我只是认为适当使用它们不成问题的:)
Jon Skeet

7
@PaulLockwood:我会说,如果你有200+每例外第二,你是滥用例外。如果每秒发生200次,则显然这不是“异常”事件。请注意答案的最后一句话:“基本上,除非您遇到重大的正确性问题,否则异常不应该经常发生;如果您遇到重大的正确性问题,那么性能并不是您面临的最大问题。”
乔恩·斯基特

4
@PaulLockwood:我的意思是,如果您每秒有200多个异常,那可能已经表明您正在滥用异常。经常出现这种情况并不令我感到惊讶,但是这意味着性能方面并不是我的首要考虑-异常的滥用将是。一旦删除了所有对异常的不当使用,就不会期望它们成为性能的重要组成部分。
乔恩·斯基特

4
@DavidJeske:您错过了答案的重点。显然,抛出异常比返回正常值要慢得多。没有人在争论。现在的问题是,他们是否是缓慢。如果您处于抛出异常的适当情况下,并且导致了性能问题,那么您可能会遇到更大的问题-因为这表明您的系统存在很多错误。通常,问题实际上是您在异常开始时使用异常。
乔恩·斯基特

31

实施它们的人Chris Brumme对此有一个明确的答案。他写了一篇很棒的博客文章有关该主题(警告-很长)(警告2-很好写,如果您是技术人员,则需要阅读到末尾,然后必须花点时间工作:) )

摘要:它们很慢。它们被实现为Win32 SEH异常,因此有些甚至会超过Ring 0 CPU边界!显然,在现实世界中,您将做很多其他工作,因此根本不会注意到奇怪的异常,但是,如果将它们用于程序流,则期望您的应用程序受到重创。这是MS营销机器给我们造成损害的另一个示例。我记得一位microsoftie告诉我们他们是如何产生绝对零开销的,这完全是tosh。

克里斯给出了一个相关的报价:

实际上,即使在引擎的非托管部分,CLR在内部也使用异常。但是,存在一个严重的长期性能问题,但有例外情况,必须将此因素纳入您的决策。


我可以在实际测试中提到这一点,在这种情况下,可为空的类型会在“这是正常的程序流”中多次引发异常,最终导致严重的性能问题。Alwyas记住,例外是在特殊情况下发生的,不要相信任何人说不这样的话,否则您将得到像这样的github线程!
gbjbaanb

8

我不知道人们说什么才慢,被抛出就在说什么。

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

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

异常也是堆上的对象-因此,如果您抛出大量异常,则将导致性能和内存问题。

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

“异常处理非常昂贵。在搜索正确的异常处理程序时,CLR通过调用堆栈递归时,所涉及线程的执行将被挂起,并且当找到异常处理程序和一些finally块时,它们都必须有执行的机会。才能进行常规处理。”

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


1
您所写的任何内容都与宣称异常只有在引发异常时才缓慢的说法相矛盾。你只谈到了他们的情况下抛出。当您通过消除异常来“显着提高性能”时:1)它们是真正的错误情况,还是仅仅是用户错误?
乔恩·斯基特

2)您是否在调试器下运行?
乔恩·斯基特

如果不抛出异常,您唯一可以做的就是将其创建为对象,这是没有意义的。是否在调试器下无关紧要-它仍然会变慢。是的,连接调试器时会发生钩子,但是仍然很慢
Cory Foy

4
我知道-我是MSFT总理团队的一员。:)可以说很多-在我们看到的某些极端情况下为每秒数千。就像连接实时调试器一样,只是以最快的速度看到异常。Ex的速度很慢-连接数据库的速度也很慢,因此您可以在需要时执行此操作。
科里·福伊

5
Cory,我认为“只有在抛出它们时才缓慢”的要点是,由于仅存在catch / finally块,因此您不必担心性能。也就是说,它们本身不会导致性能下降,只会导致实际异常实例的发生。
伊恩·霍威

6

据我了解,并不是说抛出异常是不好的,它们本身很慢。相反,它是将throw / catch构造用作控制常规应用程序逻辑的第一类方法,而不是更传统的条件构造。

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

实际上,我曾经读过Microsoft的.NET团队将.NET 2.0中的TryXXXXX方法引入了许多基本FCL类型,特别是因为客户抱怨他们的应用程序性能太慢。

事实证明,在许多情况下,这是因为客户试图在循环中进行值的类型转换,而每次尝试均失败。引发了转换异常,然后由异常处理程序捕获了该异常,然后吞没了该异常并继续执行循环。

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

我可能是错的,但听起来您不确定所阅读的“基准”的准确性。简单的解决方案:自己尝试一下。


我以为这些“ try”函数也使用异常?
greg

1
这些“ Try”函数不会在内部因解析输入值失败而引发异常。但是,它们仍然会为其他错误情况(例如ArgumentException)抛出异常。
灰烬

我认为这个答案比其他任何答案都更贴近问题的核心。说“仅在合理的情况下使用例外”并不能真正回答问题-真正的见解是,使用C#例外进行控制流比通常的条件构造要慢得多。您可能会原谅其他想法。在OCaml中,异常或多或少是GOTO,是使用命令式功能时实现中断的公认方式。在我的特殊情况下,在紧密循环中替换int.Parse()加上try / catchint.TryParse()相比,性能得到了显着提高。
休W

4

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


3
数字将是有益的,不幸的是:(像套接字操作的事情应该大大超过例外成本,肯定不会在调试的时候如果你的基准它充分,我会看到的结果很感兴趣。
乔恩斯基特

3

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


2

如果将它们与返回代码进行比较,它们将变得很慢。但是,正如先前的发帖人所述,您不想使程序正常运行,因此只有在出现问题时才能获得成功,并且在大多数情况下,性能不再重要(因为无论如何,这都意味着障碍)。

它们绝对值得使用,而不是错误代码,其优点是巨大的IMO。


2

我从来没有任何性能问题,例外。我经常使用异常-如果可能的话,我从不使用返回码。这是一种不好的做法,在我看来,它们的味道像意大利面条代码。

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

但是,如果您在堆栈的底部抛出并在顶部捕获(将整个返回代码链替换为一个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

时间戳在开始时输出,在返回码和异常之间,最后输出。两种情况都需要花费相同的时间。请注意,您必须进行优化编译。


2

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

1

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

除非您将以递归方式将异常用于流控制(例如,非本地出口),否则我怀疑您是否会注意到差异。


1

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


}

5
除了遗漏问题的重点之外,请不要使用DateTime.Now作为基准-使用秒表,该表旨在测量经过时间。由于您正在测量相当长的时间,因此这不应该成为问题,但是值得养成习惯。
乔恩·斯基特

相反,问题是“例外情况是否缓慢”。它特别要求避免何时抛出异常这一话题,因为该话题掩盖了事实。异常表现如何?
David Jeske '16

0

在此,快速捕获与捕获异常相关的性能。

当执行路径进入“ try”块时,没有任何神奇的事情发生。没有“ try”指令,也没有与进入或退出try块相关的成本。有关try块的信息存储在方法的元数据中,并且在运行时会在引发异常时使用此元数据。执行引擎在堆栈中向下移动,以查找try块中包含的第一个调用。仅当引发异常时,才会发生与异常处理相关的所有开销。


1
但是,异常的存在影响优化-具有显式异常处理程序的方法更难以内联,并且指令的重新排序受到它们的限制。
Eamon Nerbonne

-1

在编写供他人使用的类/函数时,似乎很难说出适当的例外情况。BCL有一些有用的部分,我不得不放弃并进行细化,因为它们会引发异常而不是返回错误。在某些情况下,您可以解决该问题,但在其他情况下,例如System.Management和Performance Counters,则需要在某些情况下执行循环,在该循环中BCL经常抛出异常。

如果您正在编写库,则极有可能在循环中使用您的函数,并且有可能发生大量迭代,请使用Try ..模式或其他方式将错误暴露在异常之外。即使这样,也很难说如果共享环境中的许多进程正在使用您的函数会调用多少。

在我自己的代码中,仅在异常情况异常时抛出异常,以至于有必要查看堆栈跟踪并查看出了什么问题然后进行修复。因此,我几乎已经重写了BCL的各个部分,以使用基于Try ..模式而不是异常的错误处理。


2
这似乎与张贴者的“ 我不想讨论何时以及不抛出异常 ”的说法不符
hrbrmstr 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.