.NET异常有多慢?

时间:2008-10-02 12:09:10

标签: c# .net performance exception

我不想讨论何时抛出异常以及不抛出异常。我想解决一个简单的问题。 99%的时间不抛出异常的论点围绕着它们缓慢而另一方声称(基准测试)速度不是问题。我读过很多关于一方或另一方的博客,文章和帖子。那是什么?

答案中的一些链接:SkeetMarianiBrumme

14 个答案:

答案 0 :(得分:202)

我处于“不慢”的一面 - 或者更准确地说“不够慢,不值得在正常使用中避免使用它们”。我已经写了两篇short articles。有基准方面的批评,主要是“在现实生活中需要更多的堆栈,所以你要吹掉缓存等” - 但是使用错误代码在堆栈中工作会< em>还打击缓存,所以我不认为这是一个特别好的论点。

只是为了说清楚 - 我不支持在不符合逻辑的情况下使用例外。例如,int.TryParse完全适合转换用户的数据。它在读取机器生成的文件时是合适的,其中失败意味着“文件不是它的格式,我真的不想尝试处理这个,因为我不知道还有什么可能是错的。 “

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

答案 1 :(得分:29)

实施它们的人有一个明确的答案 - 克里斯布鲁姆。他写了关于这个主题的excellent blog article(警告 - 它很长)(警告2 - 写得非常好,如果你是技术人员,你会把它读到最后,然后不得不在下班后弥补你的工作时间:))

执行摘要:它们很慢。它们被实现为Win32 SEH异常,因此有些甚至会传递0 0 CPU边界! 显然在现实世界中,你将做很多其他的工作,所以奇怪的例外根本不会被注意到,但是如果你将它们用于程序流程,除了你的应用程序被锤击。这是MS营销机器给我们带来伤害的另一个例子。我记得有一个微软告诉我们他们如何产生绝对零开销,这是完全的tosh。

克里斯给出了一个相关的引用:

  

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

答案 2 :(得分:8)

我不知道人们说什么,只有当他们被抛出时才说他们很慢。

编辑:如果没有抛出异常,则表示您正在执行新的异常()或类似的操作。否则异常将导致线程被挂起,并且堆栈将被移动。在较小的情况下这可能是好的,但在高流量的网站中,依赖异常作为工作流或执行路径机制肯定会导致性能问题。例外情况本身并不错,对表达特殊情况很有用

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

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

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

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

我自己在该领域的经验表明,减少异常显着有助于提高绩效。当然,在性能测试时还要考虑其他一些因素 - 例如,如果您的磁盘I / O被拍摄,或者您的查询是在几秒钟内,那么这应该是您的重点。但查找和删除异常应该是该策略的重要组成部分。

答案 3 :(得分:6)

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

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

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

事实证明,在许多情况下,这是因为客户在循环中尝试对值进行类型转换,并且每次尝试都失败了。抛出转换异常然后由异常处理程序捕获,然后吞并异常并继续循环。

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

我可能错了,但听起来你不确定你所读到的“基准”的真实性。简单的解决方案:亲自尝试一下。

答案 4 :(得分:4)

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

答案 5 :(得分:3)

只是将我自己最近的经验添加到这个讨论中:与上面写的大部分内容一致,我发现即使没有调试器运行,抛出异常在重复的基础上也会非常慢。我只是通过更改大约五行代码来提高我编写的大型程序的性能60%:切换到返回代码模型而不是抛出异常。当然,有问题的代码运行了数千次,并且在我更改它之前可能会抛出数千个异常。所以我同意上面的陈述:当一些重要的东西出现问题时抛出异常,而不是在任何“预期”情况下控制应用程序流的方式。

答案 6 :(得分:2)

我从未遇到过异常的任何性能问题。我经常使用异常 - 如果可以,我从不使用返回代码。它们是一种不好的做法,在我看来,闻起来像意大利面条代码。

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

但是如果你扔到堆栈的底部并抓住顶部(用一个throw / catch替换整个返回码链),所有昂贵的操作都会完成一次。

在一天结束时,它们是有效的语言功能。

只是为了证明我的观点

请运行code at this link(对于答案来说太大了)。

我的电脑上的结果:

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

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

答案 7 :(得分:2)

如果将它们与返回代码进行比较,它们会很慢。然而,正如之前的海报所说,你不想抛出正常的程序操作,所以你只能在出现问题时获得性能,在绝大多数情况下,性能不再重要(因为异常意味着无论如何都是路障)。

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

答案 8 :(得分:2)

但单声道抛出异常比.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");

答案 9 :(得分:1)

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

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

答案 10 :(得分:1)

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


}

答案 11 :(得分:1)

我想您几乎回答了自己的问题。您以及几乎所有了解它们的人都知道它们很慢。这是100%的事实,但正如许多其他人指出的那样,上下文对于100%何时使用它们至关重要。编写非服务器应用程序?您将永远不会注意到任何区别。编写一个网站公共API,其中格式错误的客户端请求会在后端触发异常?那是灾难的秘诀,数量级乘以请求/秒。在杂货店,后端的挂钉次数比小马驹的钉住次数更多。但是,这样做的问题是BCL /其他库将抛出您无法控制的异常,因此您必须中间人/交叉防护才能触发那些异常,然后再将它们发送到BCL。在某些情况下,您根本没有防御能力。例如使用MongoClient访问MongoDB数据库。如果在某些情况下不成功,所有MongoCollection。* Async函数都将引发异常,但它根本不会引发很多异常,我很确定这些情况处于光谱的稀有端(这将其转移到上下文中)部分情况)。我也可能错了。我只是假设他们只在很少的情况下抛出。正如您所指出的那样,您知道它们很慢,因此在需要不慢的情况下使用它们只是常识。简单明了。

答案 12 :(得分:0)

此处快速说明与捕获异常相关的性能。

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

答案 13 :(得分:-1)

当为其他人编写类/函数时,似乎很难说异常是恰当的。 BCL有一些有用的部分,我不得不放弃并进行pinvoke因为它们抛出异常而不是返回错误。对于某些情况,您可以解决它,但对于其他类似System.Management和Performance Counters,有些用法需要执行循环,其中BCL经常抛出异常。

如果您正在编写库并且可能会在循环中使用您的函数,并且可能会进行大量迭代,请使用Try ..模式或其他方式在异常旁边公开错误。即便如此,如果共享环境中的许多进程正在使用它,那么很难说你的函数会被调用多少。

在我自己的代码中,只有当事情如此特殊以至于需要查看堆栈跟踪并查看出现问题然后修复它时,才会抛出异常。所以我几乎已经重写了BCL的部分内容,以便使用基于Try ..模式而不是异常的错误处理。