在C#中,异常的代价有多高?

93

C#中的异常有多昂贵?只要堆栈不太深,它们似乎并不是非常昂贵,但我读到了一些相互矛盾的报告。

是否有未被反驳的明确报告?


1
没有被处理的异常并不会很昂贵,因此您可以使用try/block。 - Kishore Kumar
@KishoreJangid - 仅仅抛出异常也会有开销,即使它们没有被处理。 - BornToCode
5
可能是 How slow are .NET exceptions? 的重复问题。 - TheLethalCoder
10个回答

37
阅读到异常在性能方面的成本很高后,我编写了一个简单的测量程序,非常类似于Jon Skeet多年前发布的。我在这里提到这一点主要是为了提供更新的数字。
下面的程序处理100万个异常只需要29914毫秒,相当于每毫秒33个异常。这足够快,可以将异常作为大多数情况下替代返回代码的可行选择。
请注意,使用返回代码而不是异常,同样的程序运行时间少于1毫秒,这意味着异常比返回代码慢至少30,000倍。正如Rico Mariani所强调的,这些数字也是最小值。实际上,抛出和捕获异常会花费更多时间。
在配备Intel Core2 Duo T8100 @ 2.1 GHz的笔记本电脑上测试,使用.NET 4.0进行发布构建,未在调试器下运行(这会使其变得更慢)。
这是我的测试代码:
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;
}

7
正如我所发现的那样,在一个紧凑的游戏循环中它们是不可行的。我试图在我的索引越界检查中偷懒 :) - mpen
4
作为REST调用返回值的返回机制,使用抛出带有HTTP状态码的异常似乎是一种非常可靠、统一且相对廉价的机制。考虑到网络连接本身的开销,这种机制不仅可以返回更有用的请求错误信息,而且可以大大简化业务逻辑实现,因为它们要么成功返回,要么抛出信息详尽且可记录的异常。 - Triynko
在游戏开发中,像Ubisoft、Naughty Dog等公司的讲座中,当涉及到C++时,你不应该在代码中使用异常。相反,你应该在发生非常规情况时使用assert,而错误处理则应通过错误代码完成。@mpen - Konrad
我不知道这对于 C# 是否同样适用,例如在 Web 开发中。 - Konrad
3
这句话的意思是,在大多数情况下,异常处理比返回代码更可行。这足够快速了。对于一个调用深度只有2-3的调用堆栈,我不会说它真正代表了“大多数情况”。我们可能还应该考虑更深的调用堆栈。 - Vector Sigma
1
由于你的第二个循环没有任何作用,编译器可能会将其完全优化掉。 - g t

30

我认为如果异常的表现影响到您的应用程序,那么您抛出的异常太多了。异常应该是用于特殊情况而不是常规错误处理。

话虽如此,我的记忆中异常处理的本质是从堆栈底部开始查找与抛出的异常类型匹配的catch语句。因此性能将受到您距离catch语句的深度以及您拥有的catch语句数量的影响。


6
@Colin,虽然我同意你的观点,但你有些跑题了,并且语气有点说教。 - Robert Paulson
然后你有Java阵营,那里鼓励使用异常。 - Unknown
27
如果说陈述我的观点就是“布道”,那也没关系。我的观点仍然是,如果异常情况影响了你的表现,那么你需要减少异常情况。虽然这与Chance的问题不完全相关,但它绝对是相关主题。我怎么知道Chance是否已经考虑过我的观点呢? - Colin Burnett
30
对我来说,这听起来并不像是说教。实际上听起来就像是Jon Skeet所说的:“如果你到了异常显著影响性能的程度,那么你在使用异常方面除了性能问题之外还存在其他问题。” - D'Arcy Rittich
异常情况即使在特殊情况下也可能成为性能问题。例如,在高峰期,您依赖的第三方API不可用。异常情况是适当的(甚至是不可避免的),但即使有适当的响应,性能也可能成为一个问题,以便让您正确地响应(例如,自定义错误页面),而不是被DOS攻击(未响应等)。 - penguat
1
你是否也喜欢当别人在没有身体语言、面部表情、音色和音量的情况下,强加他们对你“语气”的看法呢? - Ronnie Overby

6

在我的情况下,异常非常昂贵。我重写了这个代码:

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秒。这个函数在启动时至少被调用32,000次。虽然代码的意图不是很明确,但节省的成本非常巨大。


4
未被处理的异常并不会增加成本。 - Kishore Kumar
关于“速度提高了约30秒”:相对的速度提高是多少?1.1倍?20倍?10,000倍? - Peter Mortensen
@PeterMortensen 这是一个非常好的问题,Peter。 - mpen
.NET Core 3.0 应用程序,速度慢了大约30倍。我的代码片段比较了1000个 try { int.Parse } catch { } 和 1000个 int.TryParse。 - GettnDer

5

我自己进行了测量,以查明异常的影响有多严重。我并没有试图测量抛出/捕获异常的绝对时间,而是更关心在每次循环中抛出异常会使循环变得多慢。测量代码如下:

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倍。

4

C#中的基本异常对象相当轻量级;通常是封装InnerException时,当对象树变得太深时才会变得沉重。

至于确定性报告,我不知道有没有,虽然进行内存消耗和速度的初步dotTrace分析(或任何其他分析器)将非常容易。


4

以下是我的个人经验:

我正在开发一个程序,用Newtonsoft (Json.NET)解析JSON文件并从中提取数据。

我对此进行了改写:

选项1,使用异常处理

try
{
    name = rawPropWithChildren.Value["title"].ToString();
}
catch(System.NullReferenceException)
{
    name = rawPropWithChildren.Name;
}

改为:

改为如下:

选项二,不带异常

if(rawPropWithChildren.Value["title"] == null)
{
    name = rawPropWithChildren.Name;
}
else
{
    name = rawPropWithChildren.Value["title"].ToString();
}

当然,您并没有上下文来进行判断,但这是我的结果(在调试模式下):
  • 选项1,包含异常。 38.50秒

  • 选项2,不包含异常。 06.48秒

为了提供一点背景,我正在处理数千个可能为空的JSON属性。异常抛出的次数太多了,可能在执行时间的15%左右。嗯,并不是很精确,但它们被抛出得太多了。
我想修复这个问题,所以我改变了我的代码,我不明白为什么执行时间会快这么多。那是因为我的异常处理太糟糕了。
所以,从这里我学到了什么:我需要只在特定情况下使用异常,并且只能用于无法用简单条件语句测试的事物。它们也必须尽可能少地抛出。
对于您来说,这可能只是一个随意的故事,但我想我以后在编写代码时肯定会三思而后行,不再轻易使用异常!

1
抱歉晚来一步,但这不是一个不公平的测试吗?您使用了一个“if”语句来短路选项2中的逻辑。一个try catch块应该是 if() null { try } else { try },对吧?我认为,用异常代替if语句并不等同于将异常与返回代码进行比较,或者触发try的if检查。 - Sean Brookins
@SeanBrookins你的意思是他正在测试“捕获异常”和“根本没有异常”,而不是更公平的测试,“捕获异常”和“抛出/不捕获异常”?这很公平,但差异可能非常小。 - jspinella
这可能是真的,@jspinella,但如果您使用一个if块,然后也使用异常(我认为这是最好的方法),您可能会发现异常并不是非常昂贵的。使用异常而不是逻辑使性能变差,但异常应该用于意外数据、无法解析的条目等,而不是替换逻辑检查。假设差异很小也意味着最初的观察不应该被测试,对吧? - Sean Brookins

4

简而言之;


答案应始终从回答“与什么相比而言昂贵?”开始

例外情况往往比任何连接的服务或数据调用快上几个数量级,因此避免使用它们很可能无法提供实际的好处,而这些例外情况提供了改进的信息和控制流。

抛出异常可以以微秒为单位进行测量(但这取决于堆栈深度):

enter image description here 来自这篇文章的图片和他发布的测试代码:.Net exceptions performance

你通常会得到你所付出的代价吗? 大多数时候是的。

更详细的解释:


我对这个问题的起源非常感兴趣。据我所知,这可能是对稍微有用的C++异常的残留厌恶。这也可能是对微软公共API开发指南的误解。.Net异常中包含了丰富的信息,并且允许编写整洁而简洁的代码,无需过多检查成功与否并进行日志记录。我在另一个答案中详细解释了异常的好处以及这种API误解。 在20年的编程经验中,我从未删除过throw或catch语句来提高性能(并不是说我不能,只是说还有更低 hanging fruit,而且之后没有人抱怨)。
有一个单独的问题有竞争性的答案,一个捕获异常(内置方法没有提供"Try"方法),另一个避免使用异常。
我决定对这两种方法进行一次性能比较,对于较少的列数,非异常版本更快,但异常版本的性能随着规模的增加逐渐超过了避免异常的版本

enter image description here

以下是该测试的LinqPad代码(包括图形渲染)。
这里需要强调的是,关于“异常很慢”的观点有一个问题:“比什么慢?”如果深度堆栈异常花费了500微秒,那么当它发生在创建数据库所需的唯一约束上,并且该过程花费了数据库3000微秒时,这是否重要?无论如何,这表明基于性能原因普遍避免异常并不一定会产生更高性能的代码。
性能测试的代码:
void Main()
{
    var loopResults = new List<Results>();
    var exceptionResults = new List<Results>();
    var totalRuns = 10000;
    for (var colCount = 1; colCount < 20; colCount++)
    {
        using (var conn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDb;Initial Catalog=master;Integrated Security=True;"))
        {
            conn.Open();

            //create a dummy table where we can control the total columns
            var columns = String.Join(",",
                (new int[colCount]).Select((item, i) => $"'{i}' as col{i}")
            );
            var sql = $"select {columns} into #dummyTable";
            var cmd = new SqlCommand(sql,conn);
            cmd.ExecuteNonQuery();

            var cmd2 = new SqlCommand("select * from #dummyTable", conn);

            var reader = cmd2.ExecuteReader();
            reader.Read();

            Func<Func<IDataRecord, String, Boolean>, List<Results>> test = funcToTest =>
            {
                var results = new List<Results>();
                Random r = new Random();
                for (var faultRate = 0.1; faultRate <= 0.5; faultRate += 0.1)
                {
                    Stopwatch stopwatch = new Stopwatch();
                    stopwatch.Start();
                    var faultCount=0;
                    for (var testRun = 0; testRun < totalRuns; testRun++)
                    {
                        if (r.NextDouble() <= faultRate)
                        {
                            faultCount++;
                            if(funcToTest(reader, "colDNE"))
                                throw new ApplicationException("Should have thrown false");
                        }
                        else
                        {
                            for (var col = 0; col < colCount; col++)
                            {
                                if(!funcToTest(reader, $"col{col}"))
                                    throw new ApplicationException("Should have thrown true");
                            }
                        }
                    }
                    stopwatch.Stop();
                    results.Add(new UserQuery.Results{
                        ColumnCount = colCount,
                        TargetNotFoundRate = faultRate,
                        NotFoundRate = faultCount * 1.0f / totalRuns,
                        TotalTime=stopwatch.Elapsed
                    });
                }
                return results;
            };
            loopResults.AddRange(test(HasColumnLoop));

            exceptionResults.AddRange(test(HasColumnException));

        }

    }
    "Loop".Dump();
    loopResults.Dump();

    "Exception".Dump();
    exceptionResults.Dump();

    var combinedResults = loopResults.Join(exceptionResults,l => l.ResultKey, e=> e.ResultKey, (l, e) => new{ResultKey = l.ResultKey, LoopResult=l.TotalTime, ExceptionResult=e.TotalTime});
    combinedResults.Dump();
    combinedResults
        .Chart(r => r.ResultKey, r => r.LoopResult.Milliseconds * 1.0 / totalRuns, LINQPad.Util.SeriesType.Line)
        .AddYSeries(r => r.ExceptionResult.Milliseconds * 1.0 / totalRuns, LINQPad.Util.SeriesType.Line)
        .Dump();
}
public static bool HasColumnLoop(IDataRecord dr, string columnName)
{
    for (int i = 0; i < dr.FieldCount; i++)
    {
        if (dr.GetName(i).Equals(columnName, StringComparison.InvariantCultureIgnoreCase))
            return true;
    }
    return false;
}

public static bool HasColumnException(IDataRecord r, string columnName)
{
    try
    {
        return r.GetOrdinal(columnName) >= 0;
    }
    catch (IndexOutOfRangeException)
    {
        return false;
    }
}

public class Results
{
    public double NotFoundRate { get; set; }
    public double TargetNotFoundRate { get; set; }
    public int ColumnCount { get; set; }
    public double ResultKey {get => ColumnCount + TargetNotFoundRate;}
    public TimeSpan TotalTime { get; set; }


}

3
异常会导致性能下降的原因似乎在于生成异常对象的时候(虽然这种下降的情况只占90%左右,不足为虑)。因此建议进行代码分析-如果异常确实影响了性能,可以编写一种新的高性能方法来替代使用异常的方法。 (我想到的一个例子是使用TryParse来解决使用异常的Parse方法的性能问题)。
尽管如此,大多数情况下,异常并不会对性能造成显著影响 - 因此,根据CodeProject上的文章MS设计准则的建议,通过抛出异常来报告失败是正确的做法。

1
然而,它确实说异常通常不应该被抛出:“不要使用异常来控制正常的流程。除了系统故障外,通常应该有一种方法编写代码,避免抛出异常。例如,您可以提供一种在调用成员之前检查先决条件的方法,以允许用户编写不会抛出异常的代码。” - Andrew Aylett
1
@Andrew:没错。也许我对另一个问题的回答可以帮助澄清我的立场https://dev59.com/rXRA5IYBdhLWcg3wsgLq。仅当方法无法执行其存在的原因时才抛出异常。 - Gishu
同意:)我只是不确定“报告失败”是否足够清晰。我喜欢这些指南。 - Andrew Aylett

1

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


-1

异常是昂贵的,但在选择异常和返回代码之间时,还有更多需要考虑的因素。

从历史上看,争论的焦点是:异常确保代码被强制处理情况,而返回代码可以被忽略。我从未支持这些论点,因为没有程序员会想要故意忽略并破坏他们的代码 - 尤其是一个好的测试团队/或者一个编写良好的测试用例肯定不会忽略返回代码。

从现代编程实践的角度来看,管理异常需要考虑到它们的成本和可行性。

第一

由于大多数前端将与抛出异常的API断开连接。例如,使用REST API的移动应用程序。同样的API也可以用于基于Angular的Web前端。

任何一种情况都更喜欢使用返回代码而不是异常。

第二

现今,黑客们会随意尝试破解所有网络工具。在这种情况下,如果他们一直攻击您的应用程序登录API,并且该应用程序不断抛出异常,那么您每天将处理成千上万个异常。当然,许多人会说防火墙会处理此类攻击。但是,并非所有人都愿意花钱管理专用防火墙或昂贵的反垃圾邮件服务。更好的做法是使您的代码准备好应对这些情况。


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