C#中try/finally的开销是多少?

79
我们看到很多关于何时以及为什么要使用try/catchtry/catch/finally的问题。我知道使用try/finally肯定有用例(特别是因为这是using语句实现的方式)。
我们也看到了一些关于try/catch和异常开销的问题。
然而,我链接的问题并没有讨论仅有try-finally的开销。
假设在try块中没有任何异常,确保在离开try块时(有时通过从函数返回)执行finally语句的开销是多少?
再次强调,我只询问try/finally,没有catch,不会抛出异常。
谢谢!
编辑:好吧,我将尝试更好地展示我的用例。
我应该使用DoWithTryFinally还是DoWithoutTryFinally
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。抱歉,忘记提到这是多线程的(尽管只有一个线程会实际调用该方法);其他线程将轮询以查看对象是否正在执行其工作。返回值仅是工作是否按预期进行的成功或失败。


6
我认为我们其他人已经理解了这个问题。这是一个不错的问题。 - DOK
1
@DOK (+1) ~ 我也认为这是一个好问题,我想我理解了它的意思:我猜这就像“如果我从未遇到过任何不幸,保险费用是多少?”...我想如果没有异常被抛出,那么成本就会发生,但你最好还是付款。 - jcolebrand
7
如果你有一百个返回点,我认为你应该进行重构。 - Philipp
@Philipp:显然。 :-) 在我的实际用例中,我实际上正在将我的代码与其他人的代码进行比较,我们都有大约五个返回点--两个接近结尾的全局成功/失败检查,但是在中间有几个简单的if检查应该会提前结束一切。我认为这是try/finally的好地方,但我想知道是否应该担心性能开销。 - Platinum Azure
@白金:如果你已经有对比的代码,为什么不直接测量差异呢?就它的价值而言,差异可能微不足道。 - Joren
显示剩余4条评论
6个回答

107

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

这是一段简单的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 - 这是一个简单的peephole优化。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的操作,并且在每个要复制的字节周围放置一个try/finally,然后继续复制数百MB的数据,我可以看出那可能是一个问题,但在大多数情况下,代价可以忽略不计。


我怀疑它能够优化微小的预读。那只是一个几乎无用的优化,除非指令获取器能够意识到这种优化并跳过两个调用。 - jcolebrand

56

那么我们假设有一些开销。你是否打算停止使用finally呢?希望不是这样。

在我看来,性能指标只有在可以选择不同选项时才有意义。我无法想象如何在不使用finally的情况下获得finally的语义。


9
一个很好的补充点:如果你需要“最终”,那么你就需要它! - Andrew Barber
2
修改了我的问题。我特别询问在不太担心异常的情况下如何使用它... 因此,根据您在答案中提到的观点,我可以在不同选项之间进行选择。请您分享您的想法。 - Platinum Azure
2
有时候你可以在循环中将它们移得更远一些。例如,try/catch或try/finally的Delphi等效代码非常昂贵,所以我不得不这样做几次。 - CodesInChaos
4
在你更新的问题中,这两种方法并不能真正进行比较,因为它们在出现错误的情况下表现不同。我知道该方法不应抛出异常,但您仍可能遇到诸如OutOfMemoryException、ThreadAbortException和其他一些异常。在这种情况下,这两种方法将表现不同。 - Brian Rasmussen
3
@BrianRasmussen,您是否对问及性能问题有意见?“我看不出你如何在没有使用finally的情况下获得finally的语义”——是谁提出了这个问题?真正的问题是:“C#中try/finally的开销?”除非我们必须认为必须反对性能相关的问题,即使是像这样简单但低级别的问题,也很无辜。 - Nicholas Petersen
显示剩余5条评论

28

try/finally非常轻量级。实际上,只要没有抛出异常,try/catch/finally也是如此。

我之前写了一个简单的性能测试应用程序来测试它。在一个紧密循环中,它实际上对执行时间几乎没有造成任何影响。

我本可以再次将其发布,但它真的非常简单;只需运行一个紧密的循环,在循环内部使用try/catch/finally,并与不使用try/catch/finally的版本进行比较测试执行时间。


没有代码或二进制文件,这就没有意义。编译器可以将其全部优化为/mono/dev/null。 - stefan

14

让我们实际上把一些基准数字应用到这个问题上。这个基准测试所显示的是,确实,在有try/finally的情况下所需时间与调用空函数的开销几乎相同(可能更好的说法是:跳转到下一条指令,就像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毫秒的开销。

每个try/finally的开销为0.033/10000000 =

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


6

Andrew Barber说过,实际上TRY / CATCH语句在未抛出异常时不会添加任何或很少的开销。 finally也没有什么特别之处。你的代码只是在try + catch语句中的代码完成后始终跳转到finally。


6

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


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