【问题标题】:Overhead of try/finally in C#?C# 中 try/finally 的开销?
【发布时间】:2011-05-05 15:32:32
【问题描述】:

我们已经看到很多关于何时以及为何使用try/catchtry/catch/finally 的问题。而且我知道try/finally 肯定有一个用例(特别是因为它是实现using 语句的方式)。

我们还看到了有关 the overhead of try/catch and exceptions 的问题。

但是,我链接到的问题并没有谈到只进行 try-finally 的开销。

假设在try 块中发生的任何事情都没有异常,确保在离开try 块时执行finally 语句(有时通过从函数返回)的开销是多少?

再次,我只询问try/finally,没有catch,没有抛出异常。

谢谢!

编辑:好的,我将尝试更好地展示我的用例。

我应该使用哪个,DoWithTryFinallyDoWithoutTryFinally

public bool DoWithTryFinally()
{
  this.IsBusy = true;

  try
  {
    if (DoLongCheckThatWillNotThrowException())
    {
      this.DebugLogSuccess();
      return true;
    }
    else
    {
      this.ErrorLogFailure();
      return false;
    }
  }
  finally
  {
    this.IsBusy = false;
  }
}

public bool DoWithoutTryFinally()
{
  this.IsBusy = true;

  if (DoLongCheckThatWillNotThrowException())
  {
    this.DebugLogSuccess();

    this.IsBusy = false;
    return true;
  }
  else
  {
    this.ErrorLogFailure();

    this.IsBusy = false;
    return false;
  }
}

这种情况过于简单,因为只有两个返回点,但想象一下,如果有四个……或十个……或一百个。

出于以下原因,我有时会想使用try/finally

  • 遵守 DRY 原则(尤其是在退出点数量越来越多的情况下)
  • 如果事实证明我的内部函数没有抛出异常是错误的,那么我想确保将 this.Working 设置为 false

假设考虑到性能问题、可维护性和 DRY 原则, 退出点的数量(特别是如果我可以假设所有内部异常都被捕获)我想承担与try/finally相关的任何性能损失?

编辑 #2: 我将 this.Working 的名称更改为 this.IsBusy。抱歉,忘了说这是多线程的(尽管实际上只有一个线程会调用该方法);其他线程将轮询以查看对象是否正在工作。如果工作按预期进行,返回值只是成功或失败。

【问题讨论】:

  • 我不完全理解这个问题。您仍在编组,只是没有发现异常。仍然必须进行编组,对吗?
  • 我想我们其他人都理解这个问题。这是一个很好的。
  • @DOK (+1) ~ 我也觉得这很好,我想我想明白了:我想这和“如果我没有发生任何坏事,保险的费用是多少”是一样的?” ...我认为如果没有抛出异常,就会产生成本,但无论如何你还是要付出代价。
  • 如果你有一百个返回点我认为你应该重构:-)
  • @drachenstern:是的,你基本上已经明白了。请查看我的更新版本,以更好地了解我的目标。

标签: c# .net performance try-finally


【解决方案1】:

所以让我们假设有一个开销。你打算停止使用finally吗?希望不会。

仅当您可以在不同选项之间进行选择时,IMO 性能指标才有意义。如果不使用finally,我看不出如何获得finally 的语义。

【讨论】:

  • 一个很好的补充点;如果你需要finally,你需要它!
  • 编辑了我的问题。当我并不真正担心异常时,我特别询问是否使用它......所以我可以在不同的选项之间进行选择(根据您在答案中的观点)。请更新您的想法。
  • 您有时可以循环移动它们。例如,Delphi 等效的 try/catch 或 try/finally 非常昂贵,所以我不得不这样做几次。
  • 您更新的问题中的两种方法并没有真正进行比较,因为它们在发生错误时的行为不同。我知道该方法不应该引发异常,但您仍可能遇到异常,例如 OutOfMemoryException、ThreadAbortException 和其他一些异常。在这种情况下,这些方法的行为会有所不同。
  • @BrianRasmussen 你对性能问题有疑问吗? “如果不使用 finally,我看不出你如何获得 finally 的语义”——谁问了这个问题?真正的问题是:“C# 中 try/finally 的开销?”很无辜,除非我们必须假设与性能相关的问题,即使是像这样简单但低级的问题,也必须不被接受。
【解决方案2】:

try/finally 非常轻量级。其实try/catch/finally也一样,只要不抛出异常。

我前段时间做了一个快速的个人资料应用程序来测试它;在一个紧密的循环中,它实际上并没有增加执行时间。

我会再次发布它,但它真的很简单;只需运行一个紧密的循环,使用不会在循环内引发任何异常的try/catch/finally,并针对没有try/catch/finally 的版本对结果进行计时。

【讨论】:

  • 如果没有代码/二进制文件,这是没有意义的。编译器可以将其全部优化为 /mono/dev/null。
【解决方案3】:

安德鲁·巴伯所说的。除非抛出异常,否则实际的 TRY/CATCH 语句不会增加任何/可忽略的开销。最后真的没什么特别的。你的代码总是在 try+catch 语句中的代码完成后跳转到 finally

【讨论】:

    【解决方案4】:

    为什么不看看你实际得到了什么?

    这是一段简单的 C# 代码:

        static void Main(string[] args)
        {
            int i = 0;
            try
            {
                i = 1;
                Console.WriteLine(i);
                return;
            }
            finally
            {
                Console.WriteLine("finally.");
            }
        }
    

    这是调试版本中生成的 IL:

    .method private hidebysig static void Main(string[] args) cil managed
    {
        .entrypoint
        .maxstack 1
        .locals init ([0] int32 i)
        L_0000: nop 
        L_0001: ldc.i4.0 
        L_0002: stloc.0 
        L_0003: nop 
        L_0004: ldc.i4.1 
        L_0005: stloc.0 
        L_0006: ldloc.0 // here's the WriteLine of i 
        L_0007: call void [mscorlib]System.Console::WriteLine(int32)
        L_000c: nop 
        L_000d: leave.s L_001d // this is the flavor of branch that triggers finally
        L_000f: nop 
        L_0010: ldstr "finally."
        L_0015: call void [mscorlib]System.Console::WriteLine(string)
        L_001a: nop 
        L_001b: nop 
        L_001c: endfinally 
        L_001d: nop 
        L_001e: ret 
        .try L_0003 to L_000f finally handler L_000f to L_001d
    }
    

    这是在调试中运行时由 JIT 生成的程序集:

    00000000  push        ebp 
    00000001  mov         ebp,esp 
    00000003  push        edi 
    00000004  push        esi 
    00000005  push        ebx 
    00000006  sub         esp,34h 
    00000009  mov         esi,ecx 
    0000000b  lea         edi,[ebp-38h] 
    0000000e  mov         ecx,0Bh 
    00000013  xor         eax,eax 
    00000015  rep stos    dword ptr es:[edi] 
    00000017  mov         ecx,esi 
    00000019  xor         eax,eax 
    0000001b  mov         dword ptr [ebp-1Ch],eax 
    0000001e  mov         dword ptr [ebp-3Ch],ecx 
    00000021  cmp         dword ptr ds:[00288D34h],0 
    00000028  je          0000002F 
    0000002a  call        59439E21 
    0000002f  xor         edx,edx 
    00000031  mov         dword ptr [ebp-40h],edx 
    00000034  nop 
            int i = 0;
    00000035  xor         edx,edx 
    00000037  mov         dword ptr [ebp-40h],edx 
            try
            {
    0000003a  nop 
                i = 1;
    0000003b  mov         dword ptr [ebp-40h],1 
                Console.WriteLine(i);
    00000042  mov         ecx,dword ptr [ebp-40h] 
    00000045  call        58DB2EA0 
    0000004a  nop 
                return;
    0000004b  nop 
    0000004c  mov         dword ptr [ebp-20h],0 
    00000053  mov         dword ptr [ebp-1Ch],0FCh 
    0000005a  push        4E1584h 
    0000005f  jmp         00000061 
            }
            finally
            {
    00000061  nop 
                Console.WriteLine("finally.");
    00000062  mov         ecx,dword ptr ds:[036E2088h] 
    00000068  call        58DB2DB4 
    0000006d  nop 
            }
    0000006e  nop 
    0000006f  pop         eax 
    00000070  jmp         eax 
    00000072  nop 
        }
    00000073  nop 
    00000074  lea         esp,[ebp-0Ch] 
    00000077  pop         ebx 
    00000078  pop         esi 
    00000079  pop         edi 
    0000007a  pop         ebp 
    0000007b  ret 
    0000007c  mov         dword ptr [ebp-1Ch],0 
    00000083  jmp         00000072 
    

    现在,如果我注释掉 try、finally 和 return,我会从 JIT 得到几乎相同的程序集。您将看到的区别是跳转到 finally 块和一些代码来确定在 finally 执行后去哪里。所以你在谈论微小的差异。在发布中,跳转到 finally 将得到优化——大括号是 nop 指令,所以这将成为跳转到下一条指令,这也是一个 nop——这是一个简单的窥视孔优化。 pop eax 然后 jmp eax 同样便宜。

        {
    00000000  push        ebp 
    00000001  mov         ebp,esp 
    00000003  push        edi 
    00000004  push        esi 
    00000005  push        ebx 
    00000006  sub         esp,34h 
    00000009  mov         esi,ecx 
    0000000b  lea         edi,[ebp-38h] 
    0000000e  mov         ecx,0Bh 
    00000013  xor         eax,eax 
    00000015  rep stos    dword ptr es:[edi] 
    00000017  mov         ecx,esi 
    00000019  xor         eax,eax 
    0000001b  mov         dword ptr [ebp-1Ch],eax 
    0000001e  mov         dword ptr [ebp-3Ch],ecx 
    00000021  cmp         dword ptr ds:[00198D34h],0 
    00000028  je          0000002F 
    0000002a  call        59549E21 
    0000002f  xor         edx,edx 
    00000031  mov         dword ptr [ebp-40h],edx 
    00000034  nop 
            int i = 0;
    00000035  xor         edx,edx 
    00000037  mov         dword ptr [ebp-40h],edx 
            //try
            //{
                i = 1;
    0000003a  mov         dword ptr [ebp-40h],1 
                Console.WriteLine(i);
    00000041  mov         ecx,dword ptr [ebp-40h] 
    00000044  call        58EC2EA0 
    00000049  nop 
            //    return;
            //}
            //finally
            //{
                Console.WriteLine("finally.");
    0000004a  mov         ecx,dword ptr ds:[034C2088h] 
    00000050  call        58EC2DB4 
    00000055  nop 
            //}
        }
    00000056  nop 
    00000057  lea         esp,[ebp-0Ch] 
    0000005a  pop         ebx 
    0000005b  pop         esi 
    0000005c  pop         edi 
    0000005d  pop         ebp 
    0000005e  ret 
    

    所以你说的是 try/finally 的成本非常非常低。很少有问题域对此很重要。如果您正在执行 memcpy 之类的操作并在要复制的每个字节周围进行尝试/最终尝试,然后继续复制数百 MB 的数据,我可以看到这是一个问题,但在大多数情况下?可以忽略不计。

    【讨论】:

    • 我怀疑它会优化前面的微窥视。这只是一个几乎无用的优化,除非指令获取器可以实现优化并跳过两次调用。
    【解决方案5】:

    如果条件不满足,在较低级别finallyelse 一样昂贵。它实际上是汇编程序(IL)中的一个跳转。

    【讨论】:

      【解决方案6】:

      让我们实际为此添加一些基准数据。这个基准测试表明,实际上,尝试/最终的时间与调用空函数的开销一样小(可能更好的说法是:正如 IL 专家所说的那样:“跳转到下一条指令”以上)。

                  static void RunTryFinallyTest()
                  {
                      int cnt = 10000000;
      
                      Console.WriteLine(TryFinallyBenchmarker(cnt, false));
                      Console.WriteLine(TryFinallyBenchmarker(cnt, false));
                      Console.WriteLine(TryFinallyBenchmarker(cnt, false));
                      Console.WriteLine(TryFinallyBenchmarker(cnt, false));
                      Console.WriteLine(TryFinallyBenchmarker(cnt, false));
      
                      Console.WriteLine(TryFinallyBenchmarker(cnt, true));
                      Console.WriteLine(TryFinallyBenchmarker(cnt, true));
                      Console.WriteLine(TryFinallyBenchmarker(cnt, true));
                      Console.WriteLine(TryFinallyBenchmarker(cnt, true));
                      Console.WriteLine(TryFinallyBenchmarker(cnt, true));
      
                      Console.ReadKey();
                  }
      
                  static double TryFinallyBenchmarker(int count, bool useTryFinally)
                  {
                      int over1 = count + 1;
                      int over2 = count + 2;
      
                      if (!useTryFinally)
                      {
                          var sw = Stopwatch.StartNew();
                          for (int i = 0; i < count; i++)
                          {
                              // do something so optimization doesn't ignore whole loop. 
                              if (i == over1) throw new Exception();
                              if (i == over2) throw new Exception();
                          }
                          return sw.Elapsed.TotalMilliseconds;
                      }
                      else
                      {
                          var sw = Stopwatch.StartNew();
                          for (int i = 0; i < count; i++)
                          {
                              // do same things, just second in the finally, make sure finally is 
                              // actually doing something and not optimized out
                              try
                              {
                                  if (i == over1) throw new Exception();
                              } finally
                              {
                                  if (i == over2) throw new Exception();
                              }
                          }
                          return sw.Elapsed.TotalMilliseconds;
                      }
                  }
      

      结果:33,33,32,35,32 63,64,69,66,66 (毫秒,确保你开启了代码优化)

      因此,在 1000 万 个循环中,try/finally 的开销约为 33 毫秒。

      每次尝试/最后,我们正在谈论 0.033/10000000 =

      3.3 纳秒或 33 亿分之一秒的 try/finally 开销。

      【讨论】:

        猜你喜欢
        • 2012-06-17
        • 2011-10-26
        • 1970-01-01
        • 1970-01-01
        • 2018-03-19
        • 2012-01-24
        • 2011-10-31
        • 2016-03-28
        • 1970-01-01
        相关资源
        最近更新 更多