【问题标题】:How much more expensive is an Exception than a return value?异常比返回值贵多少?
【发布时间】:2010-11-19 21:58:13
【问题描述】:

是否可以更改此代码,带有返回值和异常:

public Foo Bar(Bar b)
{
   if(b.Success)
   {
      return b;
   }
   else
   {
      throw n.Exception;
   }
}

对此,成功和失败分别抛出异常

public Foo Bar(Bar b)
{
   throw b.Success ? new BarException(b) : new FooException();
}

try
{
   Bar(b)
}
catch(BarException bex)
{
   return ex.Bar;
}
catch(FooException fex)
{
   Console.WriteLine(fex.Message);
}

【问题讨论】:

  • 这似乎是stackoverflow.com/questions/99683/…的功能副本
  • 无论答案如何,重要的是要记住成本与整个程序相关。当您 100% 的时间抛出异常时,您可能会发现 3 行代码块的成本要高出 1000 倍。但现实情况是,在执行数据库调用或读取文件等操作的解决方案中,可能只有 1% 的时间会抛出异常。

标签: c# .net exception exception-handling return-value


【解决方案1】:

抛出异常肯定比返回值更昂贵。但就原始成本而言,很难说例外情况要贵多少。

在决定返回值还是异常时,您应该始终考虑以下规则。

仅在特殊情况下使用例外

它们不应该用于一般控制流。

【讨论】:

  • 有时异常对一般控制流有意义 - 语言对控制流的选择较少。例如,考虑一个递归下降解析器,它正在推测性地调查解析路径 - 当它放弃时,它会想要从一个非常深的调用树返回,而无需费力设计每个例程的最简单方法就是抛出一个例外。
  • 我不同意“仅在特殊情况下使用例外”这条规则过于模糊和主观,无法有效。抛出“InvalidParameterException”几乎不是例外情况。
  • @Alan:将无效参数传递给方法应该是一种例外情况。
  • 我会说阻止程序正常运行的一切都是例外情况。
【解决方案2】:

使用下面的代码,测试表明,没有异常的调用+返回每次迭代大约需要 1.6 微秒,而异常(抛出加捕获)每次增加大约 4000 微秒。(!)

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        DateTime start = DateTime.Now;
        bool PreCheck = chkPrecheck.Checked;
        bool NoThrow = chkNoThrow.Checked;
        int divisor = (chkZero.Checked ? 0 : 1);
        int Iterations =  Convert.ToInt32(txtIterations.Text);
        int i = 0;
        ExceptionTest x = new ExceptionTest();
        int r = -2;
        int stat = 0;

        for(i=0; i < Iterations; i++)
        {
            try
            {
                r = x.TryDivide(divisor, PreCheck, NoThrow);
            }
            catch
            {
                stat = -3;
            }

        }

        DateTime stop = DateTime.Now;
        TimeSpan elapsed = stop - start;
        txtTime.Text = elapsed.TotalMilliseconds.ToString();

        txtReturn.Text = r.ToString();
        txtStatus.Text = stat.ToString();

    }
}



class ExceptionTest
{
    public int TryDivide(int quotient, bool precheck, bool nothrow)
    {
        if (precheck)
        {
            if (quotient == 0)
            {
                if (nothrow)
                {
                    return -9;
                }
                else
                {
                    throw new DivideByZeroException();
                }

            }
        }
        else
        {
            try
            {
                int a;
                a = 1 / quotient;
                return a;
            }
            catch
            {
                if (nothrow)
                {
                    return -9;
                }
                else
                {
                    throw;
                }
            }
        }
        return -1;
    }
}

所以是的,例外是非常昂贵。

在有人说之前,是的,我在 Release 模式下进行了测试,而不仅仅是 Debug 模式。自己尝试一下代码,看看是否会得到明显不同的结果。

【讨论】:

  • +1:用于实际测试它,而不是仅仅夸夸其谈。为什么程序员会争论他们可以测量的愚蠢的东西?让我生气,grrrrr ...
  • 感谢您发布这些结果。我知道这会有很大的不同。但这是一个非常大的区别。好东西。
  • 我认为这应该是公认的答案,因为它提供了可衡量的推理
  • 我在这段代码中没有看到“txtIterations”的定义。我猜这是 Form1 上的文本字段?运行时使用了 txtIterations 的什么值?
  • @JonSchneider 是的,它是一个文本框控件。六年前,所以我不记得了,但我猜在 10,000 到 100,000 之间。
【解决方案3】:

异常有两个成本:在异常基础结构中预热到页面 - 如果不在内存中,则进入 CPU 缓存 - 以及收集异常堆栈、搜索异常处理程序、可能调用异常过滤器、展开的每次抛出成本堆栈,调用终结块 - 运行时根据设计未优化的所有操作。

因此,衡量抛出异常的成本可能会产生误导。如果您编写一个迭代地抛出和捕获异常的循环,并且在抛出站点和捕获站点之间没有大量工作,那么成本看起来不会那么大。然而,这是因为它摊销了异常的预热成本,而且这个成本更难衡量。

当然,如果一个人的主要经验是调试器下的程序抛出异常,那么异常不会像看起来那样花费任何成本。但它们确实会产生成本,因此建议特别设计库,以便在需要优化时避免异常。

【讨论】:

  • 捕获堆栈、搜索处理程序等会产生成本,而不是抛出。
  • 收集堆栈可以相对简单地变得相当便宜。通常只需要从几乎不超过 1MB 的连续块中的内存中复制几十个单词。处理程序的搜索同样便宜,但调用过滤例程可能会导致引入大量其他代码,并且如果异常调度和堆栈展开逻辑没有针对频繁抛出异常进行优化,则为每个步骤设置堆栈框架等整个操作可以加起来。
  • @Scott:我认为区分投掷和接球没有任何意义。它们有着千丝万缕的联系——你不能没有另一个,所以你怎么称呼它没有区别。
【解决方案4】:

您可以在此问题的答案中找到很多有用的信息,包括一个获得 45 票赞成的答案How slow are .net exceptions?

【讨论】:

  • 嗯,我回答的哪一部分不正确,angerboy? URL,该问题的答案,Jon 有 45 个赞成票(现在是 49 个)的事实,还是我断言那里有“很多有用的信息”?
  • 我没有说你回答的任何部分都是不正确的 DOK。如果我有意这样做,那么我会说“this 不正确”。相反,我是在回复您的参考资料:“......包括一个有 45 票赞成的答案”,并附有:“这似乎不正确。”,例如,您引用的答案内容似乎不正确.
  • 感谢您的澄清。如果您在回答另一个问题时对此进行了放大,那将非常有帮助。如果乔恩的高分答案不正确,那么您将为每个人提供真正的服务来解释它是如何不正确的(超出您的简短评论)。这是一个如此重要的问题,您显然对此有深入的经验。
【解决方案5】:

使用错误返回将比异常更昂贵 - 只要一段代码忘记检查错误返回,或者无法传播它。

不过,请确保不要对控制流使用异常 - 仅使用它们来指示出现问题。

【讨论】:

    【解决方案6】:

    目前我很难找到任何支持它的文档,但请记住,当您抛出异常时,C# 必须从您调用它的位置生成堆栈跟踪。堆栈跟踪(以及一般的反射)远非免费。

    【讨论】:

    • 技术上收集堆栈跟踪并不是那么昂贵。如果需要将来自堆栈高位(堆栈向下增长)的数据拉入缓存以获取每个推送的返回地址,这肯定会花费一些成本,但这是在有人真正要求堆栈跟踪数据之前需要支付的唯一成本。你可以懒惰地做反思,如果没有人要求,就永远不要付钱。
    【解决方案7】:

    在最近的一些实际工作性能分析中,我们发现低端机器上的大量异常对应用程序性能产生了至关重要的影响,以至于我们花了几周的时间来检查并调整代码以不抛出异常这么多。

    当我说一个关键影响时,它是在应用程序正在使用的单 CPU 内核上将双核 CPU 峰值提高到 95%。

    【讨论】:

      【解决方案8】:

      您应该更喜欢异常而不是错误代码,并且对于错误条件,但不要将异常用于正常程序流程。

      与正常工作流程相比,异常是极其繁重的任务,我发现使用 try-catch-block 会使应用程序性能大幅下降。

      【讨论】:

        【解决方案9】:

        抛出异常是一种相对便宜的操作。捕获它们会产生成本,因为必须进行堆栈遍历才能找到 catch 处理程序、执行 catch 处理程序中的代码、找到 finally 块、执行 finally 块中的代码,然后返回给原始调用者。

        强烈建议您不要对控制流使用异常。从“时间和材料”的角度来看,使用返回码来指示错误会变得很昂贵,因为它最终会产生维护成本。

        除此之外,您的两个示例不匹配,甚至无效。既然你返回b,它的类型是Bar,你的第一个方法应该是:

        public Bar Foo(Bar b)
        {
           if(b.Success)
           {
              return b;
           }
           else
           {
              throw n.Exception;
           }
        }
        

        可以改写为:

        public Bar Foo(Bar b)
        {
           if (!b.Success)
              throw n.Exception;
        
           return b;
        }
        

        【讨论】:

          【解决方案10】:

          通常我会避免这种情况,因为捕获异常的成本很高。如果不是经常发生的事情,可能还不错。

          但是,为什么不直接返回 null 呢?然后就变成了这样:

          public Foo Bar(Bar b)
          {
             if(b.Success)
             {
                return b;
             }
             else
             {
                return null;
             }
          }
          

          然后,每当您调用 Bar() 时,您只需在使用该值之前检查以确保返回的值不为空。这是一个便宜得多的操作。我认为这是一个很好的做法,因为这是 Microsoft 在许多 .NET 的内置函数中到处使用的技术。

          【讨论】:

          • 这是一个非常糟糕的做法,因为有人可能会忘记检查 null。它还会检查代码中很少发生的事情,从而掩盖代码的实际用途。
          • "这是一个非常糟糕的做法,因为有人可能会忘记检查 null。" - 抛出异常不能说同样的话吗?如果您抛出异常,您需要记住捕获它。
          • 我想我的意思是,无论哪种情况,如果您没有正确编写围绕函数调用的代码,这都会反过来咬您一口。不同之处在于我的方法比抛出和捕获异常要快得多。
          【解决方案11】:

          我看到许多不同的系统,其中异常被认为是软件问题的指示,也意味着提供有关导致它的事件的信息。这对系统来说总是很沉重。然而,有些系统的异常并不例外,因为语言本身使用相同或相似的方法从例程等中返回。所以它归结为异常是如何嵌入到语言和系统中的。 我在 Java 和我现在工作的专有系统中进行了测量,结果符合预期 - 例外,例外情况要贵 (20-25)%。

          这不一定是规则。例如,我想知道 python 是如何站在这一点上的。我不再做任何python开发,所以我不打算调查。

          作为一个可能有趣的轶事证据:几年前,在我工作的专有系统中,对协议违规异常的非智能使用导致了严重的问题和我们的一个生产系统的中断。异常用于指示从外部接收的消息中缺少或损坏的必填字段。一切都很好,直到一个阳光明媚的一天,一些技术不太好的公司生产了一个节点,该节点在投入运行时引起了很多麻烦。必须删除异常,因为我们无法应对来自故障节点的低级别信号。错误确实也在我们这边——而是遵循协议规范,该规范描述了如何处理格式错误的消息(忽略并步进信号故障计数器),我们试图检测和调试外部节点中的软件故障。 如果例外情况不那么昂贵,这将不是什么大问题。

          事实上,在这些天来测试安全性时,我总是有一个或几个 TC 可以做到这一点 - 产生大量的协议违规情况。如果开发人员使用异常来处理这些异常,那么系统可以很容易地陷入瘫痪。当这一次失败时,我将再次开始测量差异......

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2014-07-15
            • 1970-01-01
            • 2016-02-27
            • 1970-01-01
            • 1970-01-01
            • 2016-10-27
            相关资源
            最近更新 更多