type see remarks operador example cref c# .net performance try-finally

see - Sobrecarga de prueba/finalmente en C#?



see cref c# (6)

Hemos visto muchas preguntas sobre cuándo y por qué usar try / catch y try / catch / finally . Y sé que definitivamente hay un caso de uso para try / finally (especialmente porque es la forma en que se implementa la declaración de using ).

También hemos visto preguntas acerca de los gastos generales de prueba / captura y excepciones .

Sin embargo, la pregunta a la que me vinculé no habla sobre la sobrecarga de tener SOLO intento por fin.

Suponiendo que no haya excepciones a lo que ocurra en el bloque try , ¿cuál es la sobrecarga de asegurarse de que las sentencias finally se ejecuten al salir del bloque try (a veces al regresar de la función)?

Nuevamente, solo pregunto acerca de try / finally , no hay problema, no hay excepciones.

¡Gracias!

EDIT: De acuerdo, voy a tratar de mostrar un poco mejor mi estuche de uso.

¿Qué debo usar, DoWithTryFinally o 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; } }

Este caso es demasiado simplista porque solo hay dos puntos de retorno, pero imagínese si hubiera cuatro ... o diez ... o cien.

En algún momento, me gustaría usar try / finally por los siguientes motivos:

  • Mantener los principios DRY (especialmente a medida que aumenta el número de puntos de salida)
  • Si resulta que estoy equivocado acerca de que mi función interna no es lanzar una excepción, entonces quiero asegurarme de que this.Working está configurado en false .

Así que hipotéticamente, dadas las preocupaciones de rendimiento, capacidad de mantenimiento y los principios DRY, ¿para qué número de puntos de salida (especialmente si puedo suponer que se detectan todas las excepciones internas)?

EDITAR # 2: Cambié el nombre de este. this.Working para this.IsBusy . Lo sentimos, olvidé mencionar que esto es multiproceso (aunque solo un hilo llamará al método); otros subprocesos se sondearán para ver si el objeto está haciendo su trabajo. El valor de retorno es simplemente el éxito o el fracaso si el trabajo fue como se esperaba.


¿Por qué no mirar lo que realmente obtienes?

Aquí hay un simple fragmento de código en C #:

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

Y aquí está el IL resultante en la compilación de depuración:

.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 }

y aquí está el ensamblado generado por el JIT cuando se ejecuta en depuración:

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

Ahora, si comento el intento, y finalmente la devolución, obtengo un ensamblaje casi idéntico del JIT. Las diferencias que verás son un salto hacia el bloque final y algún código para averiguar dónde ir después de que se ejecute finalmente. Así que estás hablando de pequeñas diferencias. En el lanzamiento, el salto hacia el final finalmente se optimizará. Las llaves son instrucciones nop, por lo que esto se convertiría en un salto a la siguiente instrucción, que también es nop. Esa es una fácil optimización de mirilla. El pop eax y luego el jmp eax son igualmente baratos.

{ 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

Así que estás hablando muy, muy pequeños costos para intentar / finalmente. Hay muy pocos dominios de problemas donde esto importa. Si está haciendo algo como memcpy y pruebe / finalmente alrededor de cada byte que se está copiando y luego copie cientos de MB de datos, podría ver que eso es un problema, ¿pero en la mayoría de los casos? Despreciable.


Así que supongamos que hay una sobrecarga. ¿Vas a dejar de usar finally entonces? Ojalá no.

Las métricas de rendimiento de IMO solo son relevantes si puede elegir entre diferentes opciones. No puedo ver cómo puedes obtener la semántica de finally sin usar finally .


En los niveles más bajos, finally es tan caro como otra else si la condición no se cumple. En realidad es un salto en ensamblador (IL).


Lo que dijo Andrew Barber. Las declaraciones reales de TRY / CATCH agregan una sobrecarga nula / insignificante a menos que se lance una excepción. Por fin no hay nada realmente especial. Su código siempre salta para finalmente después de que el código en las declaraciones try + catch se haya completado.


Vamos a poner algunos números de referencia a esto. Lo que muestra este punto de referencia es que, de hecho, el momento de tener una prueba / finalmente es tan pequeño como la sobrecarga de una llamada a una función vacía (probablemente mejor dicho: "un salto a la siguiente instrucción", como lo dijo el experto de IL encima).

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; } }

Resultado: 33,33,32,35,32 63,64,69,66,66 (milisegundos, asegúrese de tener la optimización del código activada)

Entonces, aproximadamente 33 milisegundos de sobrecarga para la prueba / finalmente en 10 millones de bucles.

Por prueba / finalmente, estamos hablando 0.033 / 10000000 =

3.3 nanosegundos o 3.3 mil millonésimas de una segunda sobrecarga de un intento / finalmente.


try/finally es muy ligero. En realidad, también lo es try/catch/finally siempre que no se lance una excepción.

Tuve una aplicación de perfil rápido que hice hace un tiempo para probarlo; En un circuito cerrado, realmente no agregó nada en absoluto al tiempo de ejecución.

Lo publicaría de nuevo, pero era realmente simple; simplemente ejecute un bucle cerrado haciendo algo, con un try/catch/finally que no genere excepciones dentro del bucle, y compare los resultados con una versión sin el try/catch/finally .