在C#中尝试/最终的开销?


72

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

我们还看到了有关try / catch和exception开销的问题。

但是,我链接到的问题并没有涉及仅进行最终尝试的开销。

假设try块中没有发生任何异常,那么确保finally语句在离开try块时得到执行(有时通过从函数返回)有什么开销?

同样,我只询问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原则,对于/关联的任何性能损失,我想招致多少出口点(特别是如果我可以假定所有内部异常均被捕获)?tryfinally

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


我不太明白这个问题。您仍在编组中,只是没有捕捉到异常。仍然必须进行编组,对吗?
jcolebrand

6
我认为我们其余的人都明白这个问题。这是一个很好的。
DOK 2010年

1
@DOK(+1)〜我也认为它也是一个好东西,我想我已经明白了:我想这与“如果我没有发生任何不好的情况下的保险费用是多少?”相同。...我认为如果不抛出异常,成本将会发生,但是您还是还是要付出代价。
jcolebrand

7
如果您有一百个返回点,我认为您应该重构:-)
Philipp

@drachenstern:是的,您基本上已经明白了。请参阅我的更新版本,以更好地了解我要做什么。
白金Azure

Answers:


97

为什么不看看您实际上得到了什么?

这是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之后要去哪里。所以您在谈论TINY的差异。在发行版中,进入finally的跳转将得到优化-大括号是nop指令,因此这将成为到下一条指令(也是nop)的跳转-这是一个简单的窥孔优化。流行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 

因此,您说的是非常小的最终尝试成本。在这个问题上很少有问题域。如果您正在执行类似memcpy的操作,并在要复制的每个字节上进行尝试/最后操作,然后继续复制数百MB的数据,我可以认为这是一个问题,但是在大多数情况下?微不足道。


我怀疑这会优化前面的微观窥视。除非指令获取程序可以实现优化并跳过两个调用,否则这几乎是没有用的优化。
jcolebrand

54

因此,我们假设存在开销。你要停止使用吗finally吗?希望不会。

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


8
很大的一点;如果需要 finally,就需要!
Andrew Barber 2010年

2
编辑了我的问题。我特别是在不真正担心异常的情况下询问使用它的方法。因此,我可以在不同的选项之间进行选择(根据您在回答中的观点)。请更新您的想法。
白金Azure

1
有时您可以将它们进一步循环移动。例如,等效于try / catch或try / finally的Delphi相当昂贵,因此我不得不这样做几次。
CodesInChaos 2010年

4
您更新后的问题中的这两种方法并没有真正进行比较,因为在发生错误的情况下它们的行为会有所不同。我知道该方法不应该引发异常,但是您仍然可能会遇到异常,例如OutOfMemoryException,ThreadAbortException和其他一些异常。在这种情况下,方法的行为会有所不同。
Brian Rasmussen

3
@BrianRasmussen您是否对性能问题有疑问?“我看不到不使用finally怎么能获得final的语义”-谁问了这个问题?真正的问题是:“在C#中尝试/最终尝试的开销?” 除非我们必须假设与性能相关的问题,否则即使在诸如此类简单但低级的问题上,也必须皱眉,这是非常纯真的。
Nicholas Petersen 2014年

28

try/finally非常轻巧。其实也是try/catch/finally只要不引发异常,也是如此。

我有一个前不久做过的快速配置文件应用程序来测试它;在一个紧密的循环中,它实际上根本没有增加执行时间。

我会再次发布它,但这确实很简单;只需运行一个紧密的循环来执行某项操作,并try/catch/finally在循环内不引发任何异常,然后将结果与不带的版本进行计时try/catch/finally


没有代码/二进制文件,这是没有意义的。编译器可以将其全部优化到/ mono / dev / null中。
stefan 2012年

13

实际上,让我们为此设置一些基准数字。该基准测试表明,确实,进行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 / final大约需要33毫秒的开销。

每次尝试/最终,我们正在说的是0.033 / 10000000 =

尝试/最终需要3.3纳秒或33亿分之一秒的开销。


6

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


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.