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