Sobrecarga de try / finally en C #?
-
29-09-2019 - |
Pregunta
Hemos visto un montón de preguntas acerca de cuándo y por qué utilizar try
/ catch
y try
/ catch
/ finally
. Y sé que hay definitivamente un caso de uso para try
/ finally
(especialmente ya que es la forma en que se implementa la declaración using
).
También hemos visto preguntas sobre la sobrecarga de try / catch y excepciones .
La pregunta he vinculado, sin embargo, no habla de la sobrecarga de tener sólo tratar -por fin-.
Si se asume que no hay excepciones a todo lo que sucede en el bloque try
, ¿cuál es la sobrecarga de asegurarse de que los estados finally
se ejecutan a la salida del bloque de try
(a veces mediante la devolución de la función)?
Una vez más, estoy pidiendo sólo alrededor try
/ finally
, sin catch
, sin lanzamiento de excepciones.
Gracias!
EDIT:. De acuerdo, voy a tratar de mostrar mi caso uso un poco mejor
¿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 sólo hay dos puntos de retorno, pero se imaginan si había cuatro o diez ... ... o cien.
En algún momento me gustaría utilizar try
/ finally
por las siguientes razones:
- Mantenga a principios seco (sobre todo porque el número de puntos de salida se hace mayor)
- Si resulta que estoy equivocado acerca de mi función interna no lanzar una excepción, entonces yo quiero para asegurarse de
this.Working
se establece enfalse
.
Así que, hipotéticamente, dada problemas de rendimiento, facilidad de mantenimiento, y los principios en seco, por lo que el número de puntos de salida (especialmente si puede asumen que todas las excepciones internas son capturados) hago Quiero que incurrir en cualquier pérdida de rendimiento está asociado con try
/ finally
?
editar # 2: He cambiado el nombre de this.Working
a this.IsBusy
. Lo sentimos, se olvidó de mencionar esto es multiproceso (aunque sólo un hilo será siempre realmente llamar al método); otros temas serán de votación para ver si el objeto está haciendo su trabajo. El valor de retorno es más que el éxito o fracaso de si el trabajo fue como se esperaba.
Solución
¿Por qué no mira a lo que ofrece?
Aquí es un simple trozo 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á la IL dando como resultado la generació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 ensamblador generado por el compilador JIT de depuración cuando se ejecuta en:
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 me comente la oportunidad y, finalmente, y el regreso, me sale el montaje casi idénticos desde el JIT. Las diferencias son que verá un salto en el bloque finally y algo de código para averiguar dónde ir después de la que se ejecuta finalmente. Así que estamos hablando de diferencias pequeñas. En el comunicado, el salto en los finalmente tendrá optimizados a cabo - los apoyos son instrucciones nop, por lo que esto se convertiría en un salto a la siguiente instrucción, que es también un nop - que es una optimización mirilla fácil. El POP EAX y EAX continuación JMP es igualmente barato.
{
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 estamos hablando costes muy, muy pequeñas para try / finally. Hay muy pocos dominios problema en el que esto es importante. Si estás haciendo algo así como establecimiento de memoria y poner un try / finally alrededor de cada byte se copia y luego proceder a copiar cientos de MB de datos, pude ver que ser un problema, pero en la mayoría de uso? Insignificante.
Otros consejos
Así que vamos a asumir que hay una sobrecarga. ¿Vas a dejar de usar finally
entonces? Ojalá no.
OMI métricas de rendimiento sólo son relevantes si se puede elegir entre diferentes opciones. No puedo ver cómo se puede obtener la semántica de finally
sin utilizar finally
.
try/finally
es muy ligero. En realidad, por lo que es try/catch/finally
siempre y cuando no excepción.
Yo tenía un perfil rápido de aplicaciones que hice hace un tiempo para probarlo; en un bucle estrecho, que se sumó nada en absoluto que el tiempo de ejecución.
que había puesto de nuevo, pero era muy simple; basta con ejecutar un bucle estrecho hacer algo, con un try/catch/finally
que no lanza ninguna excepción dentro del bucle, y el tiempo los resultados en contra de una versión sin la try/catch/finally
.
Vamos a realmente poner algunos números de referencia a esto. Lo que este punto de referencia muestra es que, efectivamente, el tiempo de tener un try / finally es casi tan pequeño como la sobrecarga de una llamada a una función vacía (probablemente mejor poner: "un salto a la siguiente instrucción" como el experto IL declaró que más arriba).
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 que tiene optimización de código de)
Así que alrededor del 33 milisegundos generales para el try / finally en 10 millones bucles.
Por try / finally entonces, estamos hablando 0,033 / 10000000 =
3.3 nanosegundos o 3.3 mil millonésima parte de un segundo por encima de un try / finally.
Lo que dijo Andrew Barber. Las declaraciones reales try / catch no agregan / sobrecarga insignificante a menos que se produce una excepción. No hay nada realmente especial acerca finalmente. Su código sólo salta siempre a fin después de que el código en las declaraciones try + captura se realiza
En niveles más bajos finally
es tan caro como un else
si no cumple la condición. En realidad, es un salto en ensamblador (IL).