Identificando mejoras de rendimiento en .NET

por:

Identificando mejoras de rendimiento en .NET

¿Cuántas veces nos hemos preguntado entre dos opciones cuál es más óptima? Hoy os muestro una utilidad que os puede ayudar.

Conociendo nuestro lenguaje

Lo primero que hay que saber para ver cómo se optimiza en el lenguaje que trabajamos es ver cómo funciona el mismo. En el caso de C# (java es similar), hay 2 fases principales:

Proceso de compilar y optimizar el código en .Net para mejorar el rendimiento.

Viendo el gráfico anterior, podemos apreciar como el compilar puede realizar optimizaciones tanto en la generación del código C#, como en la generación del IL(Intermediate Language) y en tiempo de ejecución, en la generación de las instrucciones que ejecutará nuestra CPU.

Sabiendo esto, estaría bien poder usar una herramienta que nos muestre tanto el código IL, como el ASM, resultado de la compilación de nuestro código fuente C#. Esta tarea la podemos hacer online con SharpLab.io

Métodos vs expression-bodied members

Una de las características de C# 6, son los expression-bodied members, lo que nos permite utilizar esta sintaxis:

public string GetName() => "Juan";

En lugar de:

public string GetName(){
   return "Juan";
}

En el siguiente enlace podéis ver cómo se comporta el código anterior respecto a su rendimiento.

Rendimiento y código generado en modo debug

En modo debug, el IL resultante del método necesita ejecutar más instrucciones que en el caso del expression-bodied member. Además, a simple vista se puede ver que es más lento.

.method public hidebysig static 
        string GetName () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 11 (0xb)
        .maxstack 1
        .locals init (
            [0] string
        )

        IL_0000: nop
        IL_0001: ldstr "Hola Juan"
        IL_0006: stloc.0
        IL_0007: br.s IL_0009

        IL_0009: ldloc.0
        IL_000a: ret
    } // end of method C::GetName

    .method public hidebysig specialname static 
        string get_GetNameInline () cil managed 
    {
        // Method begins at RVA 0x2067
        // Code size 6 (0x6)
        .maxstack 8

        IL_0000: ldstr "Hola Juan"
        IL_0005: ret
    } // end of method C::get_GetNameInline

En el caso del ASM, también ocurre lo mismo:

C.GetName()
    L0000: push ebp
    L0001: mov ebp, esp
    L0003: push eax
    L0004: cmp dword [0x17db1578], 0x0
    L000b: jz L0012
    L000d: call 0x73864330
    L0012: xor edx, edx
    L0014: mov [ebp-0x4], edx
    L0017: nop
    L0018: mov eax, [0xf1cf73c]
    L001e: mov [ebp-0x4], eax
    L0021: nop
    L0022: jmp L0024
    L0024: mov eax, [ebp-0x4]
    L0027: mov esp, ebp
    L0029: pop ebp
    L002a: ret

C.get_GetNameInline()
    L0000: push ebp
    L0001: mov ebp, esp
    L0003: cmp dword [0x17db1578], 0x0
    L000a: jz L0011
    L000c: call 0x73864330
    L0011: mov eax, [0xf1cf73c]
    L0017: pop ebp
    L0018: ret

Rendimiento y código generado en modo release

Como todos sabréis, el rendimiento siempre se mide en modo release, que es donde el compilador aplica mayor número de optimizaciones. Ahora el IL es exactamente igual:

// Methods
    .method public hidebysig static 
        string GetName () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 6 (0x6)
        .maxstack 8

        IL_0000: ldstr "Hola Juan"
        IL_0005: ret
    } // end of method C::GetName

    .method public hidebysig specialname static 
        string get_GetNameInline () cil managed 
    {
        // Method begins at RVA 0x2057
        // Code size 6 (0x6)
        .maxstack 8

        IL_0000: ldstr "Hola Juan"
        IL_0005: ret
    } // end of method C::get_GetNameInline

En el caso del ASM, este es muy simple, carga el string ya generado en memoria a un registro y lo devuelve:

C.GetName()
    L0000: mov eax, [0xf1cf73c]
    L0006: ret

C.get_GetNameInline()
    L0000: mov eax, [0xf1cf73c]
    L0006: ret

Programación asíncrona

La programación asíncrona en C# permite delegar en el sistema operativo la gestión de las operaciones de entrada/salida bloqueantes (I/O), de manera que se pueden liberar los hilos que realizan estas operaciones, para ser reutilizados por otras partes del código. Esto permite grandes mejoras de rendimiento, como se puede ver en la siguiente infografía:

Programación asíncrona en C#

Fuente

 

Conclusiones

Como se puede ver en el artículo:

  • Hay mucha diferencia entre el código generado en modo Release y Debug, en producción siempre hay que usar el modo Release.
  • Algunas características que se añaden a los lenguajes son solo syntax sugar
  • Conociendo como funciona nuestro lenguaje podemos ver cómo optimiza el código resultante

Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.
Donald Knuth

Icono hecho por prettycons de www.flaticon.com bajo la licencia CC 3.0 BY

The following two tabs change content below.

Jorge Durán

Entusiasta de la tecnología desde los 10 años, desarrollador y creador de varios proyectos de software y autodidacta por naturaleza. Ingeniero Informático por la USAL

Deja una Respuesta