c# try - ¿Trata-captura acelerando mi código?




catch asp.net (5)

Uno de los ingenieros de Roslyn que se especializa en comprender la optimización del uso de la pila echó un vistazo a esto y me informa que parece haber un problema en la interacción entre la forma en que el compilador C # genera almacenes de variables locales y la forma en que se registra el compilador JIT Programación en el correspondiente código x86. El resultado es una generación de código subóptima en las cargas y almacenes de los locales.

Por alguna razón poco clara para todos nosotros, la ruta de generación de código problemática se evita cuando el JITter sabe que el bloque está en una región protegida contra intentos.

Esto es bastante raro. Seguiremos con el equipo de JITter y veremos si podemos obtener un error para que puedan solucionarlo.

Además, estamos trabajando en mejoras para los algoritmos de los compiladores de Roslyn a C # y VB para determinar cuándo los locales se pueden convertir en "efímeros", es decir, solo deben empujarse y colocarse en la pila, en lugar de asignar una ubicación específica en la pila para La duración de la activación. Creemos que el JITter podrá hacer un mejor trabajo de asignación de registros y todo eso si le damos mejores pistas sobre cuándo los locales pueden ser "muertos" antes.

Gracias por traer esto a nuestra atención y disculpas por el extraño comportamiento.

Escribí un código para probar el impacto de try-catch, pero viendo algunos resultados sorprendentes.

static void Main(string[] args)
{
    Thread.CurrentThread.Priority = ThreadPriority.Highest;
    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.RealTime;

    long start = 0, stop = 0, elapsed = 0;
    double avg = 0.0;

    long temp = Fibo(1);

    for (int i = 1; i < 100000000; i++)
    {
        start = Stopwatch.GetTimestamp();
        temp = Fibo(100);
        stop = Stopwatch.GetTimestamp();

        elapsed = stop - start;
        avg = avg + ((double)elapsed - avg) / i;
    }

    Console.WriteLine("Elapsed: " + avg);
    Console.ReadKey();
}

static long Fibo(int n)
{
    long n1 = 0, n2 = 1, fibo = 0;
    n++;

    for (int i = 1; i < n; i++)
    {
        n1 = n2;
        n2 = fibo;
        fibo = n1 + n2;
    }

    return fibo;
}

En mi computadora, esto imprime constantemente un valor de alrededor de 0.96 ...

Cuando envuelvo el bucle for dentro de Fibo () con un bloque try-catch como este:

static long Fibo(int n)
{
    long n1 = 0, n2 = 1, fibo = 0;
    n++;

    try
    {
        for (int i = 1; i < n; i++)
        {
            n1 = n2;
            n2 = fibo;
            fibo = n1 + n2;
        }
    }
    catch {}

    return fibo;
}

Ahora imprime de manera consistente 0.69 ... - ¡en realidad corre más rápido! ¿Pero por qué?

Nota: Compilé esto usando la configuración de la versión y ejecuté directamente el archivo EXE (fuera de Visual Studio).

EDITAR: el excelente análisis de Jon Skeet muestra que try-catch está causando de alguna manera que el CLR x86 use los registros de la CPU de una manera más favorable en este caso específico (y creo que todavía tenemos que entender por qué). Confirmé el hallazgo de Jon de que x64 CLR no tiene esta diferencia y que fue más rápido que el CLR x86. También probé el uso de tipos int dentro del método Fibo en lugar de long tipos long , y luego el CLR x86 fue tan rápido como el CLR x64.

ACTUALIZACIÓN: Parece que este problema ha sido solucionado por Roslyn. La misma máquina, la misma versión CLR: el problema sigue siendo el anterior cuando se compila con VS 2013, pero el problema desaparece cuando se compila con VS 2015.


Los desensamblajes de Jon muestran que la diferencia entre las dos versiones es que la versión rápida usa un par de registros ( esi,edi ) para almacenar una de las variables locales donde la versión lenta no lo hace.

El compilador JIT hace diferentes suposiciones con respecto al uso del registro para el código que contiene un bloque try-catch versus un código que no lo contiene. Esto hace que haga diferentes selecciones de asignación de registros. En este caso, esto favorece el código con el bloque try-catch. Un código diferente puede llevar al efecto opuesto, por lo que no consideraría esto como una técnica de aceleración de propósito general.

Al final, es muy difícil saber qué código terminará ejecutándose más rápido. Algo así como la asignación de registros y los factores que influyen en ella son detalles de implementación de tan bajo nivel que no veo cómo una técnica específica podría producir de manera confiable un código más rápido.

Por ejemplo, considere los siguientes dos métodos. Fueron adaptados de un ejemplo de la vida real:

interface IIndexed { int this[int index] { get; set; } }
struct StructArray : IIndexed { 
    public int[] Array;
    public int this[int index] {
        get { return Array[index]; }
        set { Array[index] = value; }
    }
}

static int Generic<T>(int length, T a, T b) where T : IIndexed {
    int sum = 0;
    for (int i = 0; i < length; i++)
        sum += a[i] * b[i];
    return sum;
}
static int Specialized(int length, StructArray a, StructArray b) {
    int sum = 0;
    for (int i = 0; i < length; i++)
        sum += a[i] * b[i];
    return sum;
}

Una es una versión genérica de la otra. Reemplazar el tipo genérico con StructArray haría que los métodos sean idénticos. Debido a que StructArray es un tipo de valor, obtiene su propia versión compilada del método genérico. Sin embargo, el tiempo de ejecución real es significativamente más largo que el de los métodos especializados, pero solo para x86. Para x64, los tiempos son prácticamente idénticos. En otros casos, también he observado diferencias para x64.


Bueno, la forma en que estás sincronizando las cosas me parece bastante desagradable. Sería mucho más sensato simplemente cronometrar todo el bucle:

var stopwatch = Stopwatch.StartNew();
for (int i = 1; i < 100000000; i++)
{
    Fibo(100);
}
stopwatch.Stop();
Console.WriteLine("Elapsed time: {0}", stopwatch.Elapsed);

De esa manera, no estará a la merced de los tiempos pequeños, la aritmética de punto flotante y el error acumulado.

Después de haber realizado ese cambio, vea si la versión "non-catch" es aún más lenta que la versión "catch".

EDIT: Está bien, lo he intentado yo mismo, y estoy viendo el mismo resultado. Muy raro. Me pregunté si el try / catch estaba deshabilitando algunos inline mal, pero usar [MethodImpl(MethodImplOptions.NoInlining)] lugar no ayudó ...

Básicamente, tendrá que ver el código JITted optimizado en cordbg, sospecho ...

EDITAR: Un poco más de información:

  • Poniendo el try / catch alrededor del n++; La línea aún mejora el rendimiento, pero no tanto como ponerlo alrededor de todo el bloque.
  • Si detecta una excepción específica ( ArgumentException en mis pruebas) todavía es rápido
  • Si imprime la excepción en el bloque catch, sigue siendo rápido.
  • Si vuelves a lanzar la excepción en el bloque catch, vuelve a ser lento.
  • Si usas un bloque finally en lugar de un bloque catch, vuelve a ser lento
  • Si usas un bloque finally y un bloque catch, es rápido

Extraño...

EDIT: Está bien, tenemos desmontaje ...

Esto es usar el compilador C # 2 y .NET 2 (32 bits) CLR, desensamblando con mdbg (ya que no tengo cordbg en mi máquina). Sigo viendo los mismos efectos de rendimiento, incluso debajo del depurador. La versión rápida usa un bloque try alrededor de todo entre las declaraciones de variables y la declaración de retorno, con solo un controlador catch{} . Obviamente, la versión lenta es la misma, excepto sin el try / catch. El código de llamada (es decir, Principal) es el mismo en ambos casos, y tiene la misma representación de ensamblaje (por lo que no es un problema en línea).

Código desensamblado para versión rápida:

 [0000] push        ebp
 [0001] mov         ebp,esp
 [0003] push        edi
 [0004] push        esi
 [0005] push        ebx
 [0006] sub         esp,1Ch
 [0009] xor         eax,eax
 [000b] mov         dword ptr [ebp-20h],eax
 [000e] mov         dword ptr [ebp-1Ch],eax
 [0011] mov         dword ptr [ebp-18h],eax
 [0014] mov         dword ptr [ebp-14h],eax
 [0017] xor         eax,eax
 [0019] mov         dword ptr [ebp-18h],eax
*[001c] mov         esi,1
 [0021] xor         edi,edi
 [0023] mov         dword ptr [ebp-28h],1
 [002a] mov         dword ptr [ebp-24h],0
 [0031] inc         ecx
 [0032] mov         ebx,2
 [0037] cmp         ecx,2
 [003a] jle         00000024
 [003c] mov         eax,esi
 [003e] mov         edx,edi
 [0040] mov         esi,dword ptr [ebp-28h]
 [0043] mov         edi,dword ptr [ebp-24h]
 [0046] add         eax,dword ptr [ebp-28h]
 [0049] adc         edx,dword ptr [ebp-24h]
 [004c] mov         dword ptr [ebp-28h],eax
 [004f] mov         dword ptr [ebp-24h],edx
 [0052] inc         ebx
 [0053] cmp         ebx,ecx
 [0055] jl          FFFFFFE7
 [0057] jmp         00000007
 [0059] call        64571ACB
 [005e] mov         eax,dword ptr [ebp-28h]
 [0061] mov         edx,dword ptr [ebp-24h]
 [0064] lea         esp,[ebp-0Ch]
 [0067] pop         ebx
 [0068] pop         esi
 [0069] pop         edi
 [006a] pop         ebp
 [006b] ret

Código desensamblado para versión lenta:

 [0000] push        ebp
 [0001] mov         ebp,esp
 [0003] push        esi
 [0004] sub         esp,18h
*[0007] mov         dword ptr [ebp-14h],1
 [000e] mov         dword ptr [ebp-10h],0
 [0015] mov         dword ptr [ebp-1Ch],1
 [001c] mov         dword ptr [ebp-18h],0
 [0023] inc         ecx
 [0024] mov         esi,2
 [0029] cmp         ecx,2
 [002c] jle         00000031
 [002e] mov         eax,dword ptr [ebp-14h]
 [0031] mov         edx,dword ptr [ebp-10h]
 [0034] mov         dword ptr [ebp-0Ch],eax
 [0037] mov         dword ptr [ebp-8],edx
 [003a] mov         eax,dword ptr [ebp-1Ch]
 [003d] mov         edx,dword ptr [ebp-18h]
 [0040] mov         dword ptr [ebp-14h],eax
 [0043] mov         dword ptr [ebp-10h],edx
 [0046] mov         eax,dword ptr [ebp-0Ch]
 [0049] mov         edx,dword ptr [ebp-8]
 [004c] add         eax,dword ptr [ebp-1Ch]
 [004f] adc         edx,dword ptr [ebp-18h]
 [0052] mov         dword ptr [ebp-1Ch],eax
 [0055] mov         dword ptr [ebp-18h],edx
 [0058] inc         esi
 [0059] cmp         esi,ecx
 [005b] jl          FFFFFFD3
 [005d] mov         eax,dword ptr [ebp-1Ch]
 [0060] mov         edx,dword ptr [ebp-18h]
 [0063] lea         esp,[ebp-4]
 [0066] pop         esi
 [0067] pop         ebp
 [0068] ret

En cada caso, el * muestra dónde ingresó el depurador en un simple "paso a paso".

EDIT: Bueno, ahora he revisado el código y creo que puedo ver cómo funciona cada versión ... y creo que la versión más lenta es más lenta porque usa menos registros y más espacio en la pila. Para valores pequeños de n es posiblemente más rápido, pero cuando el bucle ocupa la mayor parte del tiempo, es más lento.

Posiblemente el bloque try / catch obliga a guardar y restaurar más registros, por lo que el JIT también los utiliza para el bucle ... lo que mejora el rendimiento en general. No está claro si es una decisión razonable que el JIT no utilice tantos registros en el código "normal".

EDIT: Acabo de probar esto en mi máquina x64. El x64 CLR es mucho más rápido (alrededor de 3-4 veces más rápido) que el x86 CLR en este código, y bajo x64 el bloque try / catch no hace una diferencia notable.


Habría puesto esto como un comentario, ya que realmente no estoy seguro de que este sea el caso, pero si recuerdo que no es una declaración try / except implica una modificación en la forma en que el mecanismo de eliminación de basura de El compilador funciona, ya que borra las asignaciones de memoria de objetos de forma recursiva fuera de la pila. Es posible que no haya un objeto que deba aclararse en este caso o el bucle for puede constituir un cierre que el mecanismo de recolección de basura reconoce suficiente para imponer un método de recolección diferente. Probablemente no, pero pensé que valía la pena mencionarlo ya que no lo había visto en ninguna otra parte.


Una razón válida para volver a emitir excepciones puede ser que desee agregar información a la excepción, o tal vez incluir la excepción original en uno de sus propios creadores:

public static string SerializeDTO(DTO dto) {
  try {
      XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
      StringWriter sWriter = new StringWriter();
      xmlSer.Serialize(sWriter, dto);
      return sWriter.ToString();
  }
  catch(Exception ex) {
    string message = 
      String.Format("Something went wrong serializing DTO {0}", DTO);
    throw new MyLibraryException(message, ex);
  }
}




c# .net clr try-catch performance-testing