C#中异常的代价是多少?


Answers:


72

Jon Skeet于2006年1月在.NET中撰写了“异常和性能”

更新了异常和性能Redux(感谢@Gulzar)

Rico Mariani在“ .NET异常的真正代价”中提出的建议–解决方案


另请参见:Krzysztof Cwalina-设计指南更新:异常抛出


乔恩·斯凯特(Jon Skeet)基本上说“它们根本不贵”,但是正如我们从这里的其他答案以及Internet上的其他地方看到的那样,许多人发现捕获大量异常时C#代码的运行速度明显慢。造成这种情况的原因有两点:1)比较执行时间的简单示例是SIMPLE,现实世界中的代码通常不是,2)调试时捕获异常的开销要大得多(因为VS必须停止执行并向您显示异常)。在一个循环数以十万计的应用程序中,对性能的微小影响加在一起。
jspinella

我与那些说“我的测试代码显示捕获异常非常昂贵”的人所看到的趋势是,它们正在循环数千/十万次。对于100,000个对象的迭代,如果每个对象导致捕获一个异常,并且每个捕获“花费” 1/33毫秒(根据Helge Klein的回答),那么我期望异常捕获将使执行时间增加大约3秒,但是da的回答表明惩罚要比1/33 ms高得多(尽管我们不知道他的示例中有多少次迭代,但他说,甚至仅在15%的时间内抛出异常)。
jspinella

这是一个古老的问题,也是一个古老的答案,但我们只是在回答“异常情况如何昂贵”。这个问题不可避免地源于我们已经建立的观念,即我们希望避免异常编程。例外情况应在特殊情况下进行。该论点的主旨是,避免例外通常会提高性能,但是例外有其地位,并且在正确使用时很有用。
罗伯特·保尔森

这是一个很好的答案,但是由于大多数人都会阅读您的答案,然后关闭选项卡,并忽略下面所有有用和有趣的答案,因此我认为有必要强调这些答案的内容。
jspinella

21

读完例外对于性能造成的损失,我提出了一个简单的测量程序,该程序与几年前发布的Jon Skeet非常相似。我在这里提到这个主要是为了提供更新的数字。

该程序花了29914毫秒以下的时间来处理100万个异常,相当于每毫秒33个异常。这样的速度足以使异常成为大多数情况下返回代码的可行替代方案。

但是请注意,使用返回码而不是异常,同一程序的运行时间不到一毫秒,这意味着异常比返回码慢至少30,000倍。正如Rico Mariani强调的那样这些数字也是最小数字。实际上,引发和捕获异常将花费更多时间。

在具有.NET 4.0发行版的Intel Core2 Duo T8100 @ 2,1 GHz的笔记本电脑上进行测量,该测试未在调试器下运行(这会使运行速度变慢)。

这是我的测试代码:

static void Main(string[] args)
{
    int iterations = 1000000;
    Console.WriteLine("Starting " + iterations.ToString() + " iterations...\n");

    var stopwatch = new Stopwatch();

    // Test exceptions
    stopwatch.Reset();
    stopwatch.Start();
    for (int i = 1; i <= iterations; i++)
    {
        try
        {
            TestExceptions();
        }
        catch (Exception)
        {
            // Do nothing
        }
    }
    stopwatch.Stop();
    Console.WriteLine("Exceptions: " + stopwatch.ElapsedMilliseconds.ToString() + " ms");

    // Test return codes
    stopwatch.Reset();
    stopwatch.Start();
    int retcode;
    for (int i = 1; i <= iterations; i++)
    {
        retcode = TestReturnCodes();
        if (retcode == 1)
        {
            // Do nothing
        }
    }
    stopwatch.Stop();
    Console.WriteLine("Return codes: " + stopwatch.ElapsedMilliseconds.ToString() + " ms");

    Console.WriteLine("\nFinished.");
    Console.ReadKey();
}

static void TestExceptions()
{
    throw new Exception("Failed");
}

static int TestReturnCodes()
{
    return 1;
}

2
所以他们活在紧张的游戏循环,因为我发现。我正试图对我的越界索引检查保持懒惰:)
mpen 2012年

作为REST调用返回值的返回机制如何。伴随网络连接本身的开销,似乎抛出了带有HTTP状态代码的异常,这将是一种非常可靠,统一且相对便宜的返回值的机制,更不用说关于请求出错的更多有用信息了。 。它还可以极大地简化业务逻辑实现,因为您要么成功返回,要么抛出信息丰富的可记录异常。
Triynko

在gamedev中@mpen(有来自ubisoft,顽皮狗等的谈话),对于C ++,您根本不需要在代码中使用异常。您要做的是assert在发生异常情况时使用错误代码进行错误处理。
康拉德(Konrad)'18

我不知道这对C#是否起作用,例如在Web开发中。
康拉德(Konrad)'18

That is fast enough to make exceptions a viable alternative to return codes for most situations.我不会说深度为2-3的调用堆栈确实代表了“大多数情况”。更深层次的调用堆栈可能也应该考虑。
矢量Sigma

17

我想我在营地,如果你的应用程序,那么你会抛出异常影响性能WAY太多了。例外应针对特殊情况,而不是作为常规错误处理。

就是说,我对异常处理方式的回忆实质上是在栈上查找与抛出的异常类型相匹配的catch语句。因此,性能将受到您对捕获的了解程度以及您拥有多少个捕获语句的影响最大。


4
我同意您的说法时,@ Colin却使您无话可说,语气有些讲道。
罗伯特·鲍尔森

然后,您进入了Java阵营,在这里鼓励异常。
未知

15
如果说我的观点是“突破”,那就这样吧。我的观点仍然是,如果异常影响您的性能,那么您需要抛出更少的异常。这不完全是Chance的问题,但最肯定的是话题。我应该怎么知道Chance已经考虑过我的观点了?
科林·伯内特

18
对我来说听起来并不讲道。实际上听起来就像乔恩·斯凯特(Jon Skeet)所说的那样:“如果到达某个异常会严重损害性能的地步,那么在使用异常方面,除了性能之外,您还会遇到其他问题。”
D'Arcy Rittich,2009年

即使在特殊情况下,异常也可能成为性能问题。例如,您依赖的第三方API在高峰负载期间变得不可用。异常是适当的(即使无法避免,也可以避免吗?),但是即使响应适当,也可能要考虑性能,以使您能够正确地进行响应(例如,自定义错误页面),而不是被拒绝(没有响应等)。
penguat 2015年

4

C#中的准系统异常对象相当轻巧;通常,封装InnerException对象的能力会使对象树变得太深时变得沉重。

关于确定性的报告,我不知道任何报告,尽管针对内存消耗和速度的粗略dotTrace配置文件(或任何其他配置文件)将非常容易做到。


4

就我而言,例外非常昂贵。我改写了这个:

public BlockTemplate this[int x,int y, int z]
{
    get
    {
        try
        {
            return Data.BlockTemplate[World[Center.X + x, Center.Y + y, Center.Z + z]];
        }
        catch(IndexOutOfRangeException e)
        {
            return Data.BlockTemplate[BlockType.Air];
        }
    }
}

变成这个:

public BlockTemplate this[int x,int y, int z]
{
    get
    {
        int ix = Center.X + x;
        int iy = Center.Y + y;
        int iz = Center.Z + z;
        if (ix < 0 || ix >= World.GetLength(0)
            || iy < 0 || iy >= World.GetLength(1)
            || iz < 0 || iz >= World.GetLength(2)) 
            return Data.BlockTemplate[BlockType.Air];
        return Data.BlockTemplate[World[ix, iy, iz]];
    }
}

并注意到大约30秒的良好速度增加。启动此函数至少调用32K次。代码并未明确意图是什么,但是节省了大量成本。


4
未处理的异常并不昂贵。
Kishore Kumar

3

我进行了自己的测量,以了解异常含义的严重性。我没有尝试测量引发/捕获异常的绝对时间。我最感兴趣的是,如果在每次通过中都抛出异常,循环将变慢多少。测量代码如下所示

     for(; ; ) {
        iValue = Level1(iValue);
        lCounter += 1;
        if(DateTime.Now >= sFinish) break;
     }

     for(; ; ) {
        try {
           iValue = Level3Throw(iValue);
        }
        catch(InvalidOperationException) {
           iValue += 3;
        }
        lCounter += 1;
        if(DateTime.Now >= sFinish) break;
     }

相差20倍。第二个片段要慢20倍。


2

出现异常的性能问题似乎是在生成异常对象的时候(尽管它太小而无法在90%的时间内引起任何关注)。因此,该建议是分析代码-如果异常造成性能损失,你写的不使用例外,一个新的高PERF方法。(想到的一个例子是(引入TryParse来克服使用异常的Parse的性能问题)

他说,在大多数情况下,异常在大多数情况下不会对性能造成重大影响-因此,MS设计指南旨在通过引发异常来报告故障


但是,它确实说通常不应引发异常:“请勿将异常用于正常的控制流程。除了系统故障外,通常应该有一种编写代码的方法,可以避免引发异常。例如,您可以提供在调用成员以允许用户编写不引发异常的代码之前检查前提条件的方法。”
Andrew Aylett

1
@安德鲁:对。也许我对另一个问题的回答将有助于阐明我的立场stackoverflow.com/questions/859494。仅当该方法无法执行其存在原因时才引发异常。
Gishu 2010年

同意:)。我只是不确定“报告失败”是否足够清楚。我喜欢那些准则。
Andrew Aylett '02

2

只是为了提供个人经验:我正在使用NewtonSoft开发一个程序来解析JSON文件并从中提取数据。

我改写了这个:

  • 选项1,有例外。
try
{
    name = rawPropWithChildren.Value["title"].ToString();
}
catch(System.NullReferenceException)
{
    name = rawPropWithChildren.Name;
}

对此:

  • 选项2,无例外。
if(rawPropWithChildren.Value["title"] == null)
{
    name = rawPropWithChildren.Name;
}
else
{
    name = rawPropWithChildren.Value["title"].ToString();
}

OFC你真的没有上下文它法官,但这里是我的结果:
在调试模式

  • 选项1,有例外。 38.50秒

  • 选项2,有例外。 06.48秒

为了提供一些背景信息,我正在使用数千个可以为null的json属性。异常抛出过多,例如在执行时间的15%之内。好吧,不是很精确,但是它们被扔了太多次了。我想解决此问题,所以我更改了代码,但我不明白为什么执行时间如此之快。那是因为我糟糕的异常处理。

因此,我从中学到的内容是:仅在特殊情况下以及无法用简单的条件语句进行测试的情况下,才需要使用异常。还必须尽可能少地抛出它们。

对于您来说,这是一个随机的故事,但是我想从现在开始在代码中使用异常之前,我肯定会三思而后行!


嗨,很抱歉在这里增加了您的时间,但这不是不公平的考验吗?您使用了'if'语句来缩短选项2中的逻辑。try catch块应说明if()null {try} else {try},对吗?我不认为使用异常代替if语句与将异常与返回码或触发检查的if检查相同。
肖恩·布鲁克金斯

@SeanBrookins您的意思是说他正在测试“捕获异常”和“根本没有异常”,而不是更公平的测试,即“捕获异常”和“抛出/不捕获异常”?这是公平的,但差异可能很小。
jspinella

@jspinella,这可能是正确的,但是如果您使用同时使用了异常的if块(我认为这是最好的方法),您可能会发现异常的代价不是很高。使用异常而不是逻辑会使性能变差,但是应该将异常用于意外数据,无法解析的条目等,而不是替换逻辑检查。假设差异很小,这也意味着不应对初始观察结果进行测试,是吗?
肖恩·布鲁金斯

1

我最近在一个求和循环中测量了C#异常(引发和捕获),该循环在每次加法时都会引发算术溢出。在四核笔记本电脑上,算术溢出的抛出和捕获约为8.5微秒= 117 KiloExceptions /秒。


0

异常的代价很高,但是要在异常代码和返回代码之间进行选择时,异常代码的功能更多。

从历史上讲,例外是确保代码被强制处理的情况,而返回代码可以忽略,我从来不赞成使用这些参数,因为没有程序员会故意忽略并破坏他们的代码-尤其是一支优秀的测试团队/或编写良好的测试用例绝对不会忽略返回代码。

从现代编程实践的角度来看,不仅要考虑异常例外的代价,而且还要考虑其可行性。

1号 由于大多数前端都将与抛出异常的API断开连接。例如使用Rest API的移动应用。相同的api也可以用于基于Angular js的Web前端。

无论哪种情况,都将首选返回码而不是例外。

2号 如今,黑客随机尝试破坏所有Web实用程序。在这种情况下,如果他们不断攻击您的应用程序登录api,并且应用程序不断引发异常,那么您每天将要处理成千上万的异常。当然,许多人会说防火墙会处理此类攻击,但是并非所有人都在花钱来管理专用防火墙或昂贵的反垃圾邮件服务,因此最好为这些情况准备代码。

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.