Compartir a través de


Caso práctico: Guía para principiantes para optimizar el código y reducir los costes de proceso (C#, Visual Basic, C++, F#)

La reducción del tiempo de proceso significa reducir los costos, por lo que la optimización del código puede ahorrar dinero. En este caso práctico se usa una aplicación de ejemplo con problemas de rendimiento para demostrar cómo usar herramientas de generación de perfiles para mejorar la eficacia. Si desea comparar las herramientas de generación de perfiles, consulte ¿Qué herramienta debo elegir?

En este caso práctico se tratan estos temas:

  • La importancia de la optimización del código y su impacto en la reducción de costes de procesamiento.
  • Cómo usar las herramientas de generación de perfiles de Visual Studio para analizar el rendimiento de las aplicaciones.
  • Cómo interpretar los datos facilitados por estas herramientas para identificar cuellos de botella de rendimiento.
  • Cómo aplicar estrategias prácticas para optimizar el código, centrándose en el uso de la CPU, la asignación de memoria y las interacciones de bases de datos.

Siga estos pasos y aplique estas técnicas a sus propias aplicaciones para que sean más eficaces y rentables.

Caso práctico de optimización

La aplicación de ejemplo que se describe en este caso práctico es una aplicación .NET que ejecuta consultas en una base de datos de blogs y entradas de blog. Utiliza Entity Framework, un ORM (modelo relacional de objetos) popular de .NET, para interactuar con una base de datos local de SQLite. La aplicación está estructurada para ejecutar un gran número de consultas, simulando un escenario real en el que pueda ser necesario usar una aplicación .NET para controlar las numerosas tareas de recuperación de datos. La aplicación de ejemplo es una versión modificada del ejemplo de introducción a Entity Framework.

El problema principal del rendimiento con la aplicación de ejemplo radica en cómo administra los recursos de procesamiento e interactúa con la base de datos. La aplicación sufre de un cuello de botella en el rendimiento que afecta significativamente a su eficiencia y, por consiguiente, incurre en mayores costes de procesamiento relacionados con su ejecución. El problema presenta los siguientes síntomas:

  • Consumo elevado de la CPU: las aplicaciones pueden realizar cálculos ineficaces o tareas de procesamiento de una manera que consumen innecesariamente una gran cantidad de recursos de la CPU. Esto puede dar lugar a tiempos de respuesta lentos y un incremento de los costes operativos.

  • Asignación de memoria ineficaz: las aplicaciones a veces pueden tener problemas relacionados con el uso y la asignación de memoria. En las aplicaciones .NET, una gestión ineficaz de la memoria puede provocar un aumento de la recopilación de elementos no utilizados, lo que a su vez puede afectar al rendimiento de la aplicación.

  • Sobrecargas de interacciones de la base de datos: las aplicaciones que ejecutan un gran número de consultas en una base de datos pueden sufrir de cuellos de botella relacionados con las interacciones con la base de datos. Entre estos desarreglos se incluyen consultas poco eficientes, demasiadas llamadas a la base de datos y un uso deficiente de las funcionalidades de Entity Framework, lo que puede mermar el rendimiento.

El caso práctico tiene como objetivo dar solución a estos problemas mediante el uso de las herramientas de generación de perfiles de Visual Studio para analizar el rendimiento de la aplicación. Al comprender dónde y cómo se puede mejorar el rendimiento de la aplicación, los desarrolladores pueden implementar optimizaciones para reducir el consumo de la CPU, mejorar la eficacia de la asignación de memoria, simplificar las interacciones de la base de datos y optimizar el uso de recursos. El objetivo final es mejorar el rendimiento general de la aplicación, lo que permite que se ejecute de forma más eficaz y rentable.

Desafío

La solución a los problemas de rendimiento de la aplicación .NET de ejemplo presenta varias dificultades. Estas cuestiones se derivan de la complejidad al diagnosticar cuellos de botella de rendimiento. Los principales desafíos para solucionar los problemas descritos son los siguientes:

  • Diagnóstico de cuellos de botella de rendimiento: una de las principales dificultades consiste en identificar con precisión las causas principales de los problemas de rendimiento. El consumo elevado de la CPU, la asignación ineficaz de memoria y las sobrecargas de las interacciones de la base de datos pueden deberse a varios factores determinantes. Los desarrolladores deben usar herramientas de generación de perfiles de forma eficaz para diagnosticar estos problemas y, para ello, es necesario conocer cómo funcionan estas herramientas y cómo interpretar sus resultados.

  • Limitaciones en los conocimientos y recursos: por último, los equipos pueden verse limitados por restricciones relacionadas con los conocimientos, la experiencia y los recursos. Para la generación de perfiles y la optimización de una aplicación, se deben reunir una serie de aptitudes y experiencia específicas y no todos los equipos pueden tener acceso inmediato a estos recursos.

Para solucionar estas cuestiones, se necesita aplicar un método estratégico que combine el uso eficaz de herramientas de generación de perfiles, conocimientos técnicos y planificación y programas de pruebas exhaustivos. El caso práctico tiene como objetivo guiar a los desarrolladores a través de este proceso, aportándoles estrategias e información para superar estos desafíos y mejorar el rendimiento de la aplicación.

Estrategia

A continuación, se presenta una visión general del enfoque en este caso práctico:

  • Comenzamos la investigación tomando una tarea de seguimiento del consumo de la CPU. La herramienta Uso de CPU de Visual Studio suele ser útil para examinar y analizar el rendimiento y optimizar el código para reducir costes.
  • Después, para obtener información adicional que permita aislar problemas o mejorar el rendimiento, tomamos una tarea de seguimiento mediante una de las otras herramientas de generación de perfiles. Por ejemplo:
    • En este caso, echamos un vistazo al uso de la memoria. Con .NET, probamos primero la herramienta de asignación de objetos de .NET. (Tanto con .NET como con C++, puede consultar la herramienta Uso de memoria).
    • Con ADO.NET o Entity Framework, podemos usar la herramienta Base de datos para examinar las consultas SQL, las horas exactas de las consultas y demás.

Para la recopilación de datos, se deben realizar las siguientes tareas:

  • Configurar la aplicación en una compilación de versión.
  • Seleccionar la herramienta Uso de CPU en el Generador de perfiles de rendimiento (Alt+F2). (los pasos posteriores implican algunas de las otras herramientas).
  • En el Generador de perfiles de rendimiento, inicie la aplicación y recopile un seguimiento.

Inspección de áreas de uso elevado de CPU

Después de extraerla con la herramienta Uso de CPU y cargarla en Visual Studio, primero comprobamos la página inicial del informe .diagsession donde se muestran los datos resumidos. Use el vínculo Abrir detalles del informe.

Captura de pantalla de la apertura de detalles en la herramienta Uso de CPU.

En la vista de detalles del informe, abra la vista Árbol de llamadas. La ruta de acceso al código con el uso de CPU más elevado en la aplicación se denomina ruta de acceso activa. El icono de llama de la ruta de acceso activa (Captura de pantalla que muestra el icono de ruta activa.) puede ayudar a identificar rápidamente los problemas de rendimiento que podrían mejorarse.

En la vista Árbol de llamadas, puede ver un uso elevado de CPU para el método GetBlogTitleX en la aplicación, con aproximadamente un 60% del uso de CPU de la aplicación. Sin embargo, el valor de Solo CPU para GetBlogTitleX es bajo, solo alrededor del 0,10 %. A diferencia de CPU total, el valor de Solo CPU excluye el tiempo invertido en otras funciones, por lo que sabemos que debemos buscar más abajo en el árbol de llamadas para el cuello de botella real.

Captura de pantalla de la vista Árbol de llamadas en la herramienta Uso de CPU.

GetBlogTitleX realiza llamadas externas a dos archivos de la DLL de LINQ, que usan la mayor parte del tiempo de CPU, como se evidencia en los valores de Solo CPU muy altos. Esta es la primera pista de que una consulta LINQ podría ser un área para optimizar.

Captura de pantalla de la vista Árbol de llamadas en la herramienta de Uso de CPU con CPU propia resaltado.

Para obtener un árbol de llamadas visualizado y una vista diferente de los datos, abra la vista Gráfico de llamas. (O bien, haga clic con el botón derecho en GetBlogTitleX y elija Ver en gráfico de llamas). Una vez más, parece que el método GetBlogTitleX es responsable de gran parte del uso de la CPU de la aplicación (se muestra en amarillo). Las llamadas externas a los archivos DLL de LINQ se muestran debajo del cuadro GetBlogTitleX y usan todo el tiempo de la CPU para el método.

Captura de pantalla de la vista Grafo de llamas en la herramienta Uso de CPU.

Recopilación de datos adicionales

A menudo, otras herramientas pueden proporcionar información adicional para ayudar al análisis y aislar el problema. En este caso práctico, adoptamos el siguiente enfoque:

  • En primer lugar, fíjese en el uso de la memoria. Puede haber una correlación entre un uso elevado de CPU y un uso elevado de memoria, por lo que puede resultar útil examinar ambos para aislar el problema.
  • Dado que hemos identificado los archivos DLL de LINQ, también veremos la herramienta Base de datos.

Comprobar el uso de memoria

Para ver lo que sucede con la aplicación en términos de uso de la memoria, tomamos una tarea de seguimiento mediante la herramienta de asignación de objetos de .NET (con C++, puede usar la herramienta de uso de memoria). En la vista Árbol de llamadas en la tarea de seguimiento de memoria aparece la ruta de acceso activa y nos permite identificar un área con elevado uso de memoria. Como cabría esperar, parece que el método GetBlogTitleX está generando una gran cantidad de objetos. De hecho, hay más de 900 000 asignaciones de objetos.

Captura de pantalla de la vista Árbol de llamadas en la herramienta Asignación de objetos de .NET.

La mayoría de los objetos creados son cadenas, matrices de objetos e Int32s. Aquí convendría fijarnos en cómo se generan estos tipos examinando el código fuente.

Comprobación de la consulta en la herramienta Base de datos

En el Generador de perfiles de rendimiento, seleccionamos la herramienta Base de datos en lugar de Uso de CPU (o bien seleccionamos ambas). Una vez recopilada una tarea de seguimiento, abra la pestaña Consultas en la página de diagnóstico. En la pestaña Consultas del seguimiento de base de datos, puede ver que la primera fila muestra la consulta más larga, 2446 ms. La columna Registros muestra el número de registros que lee la consulta. Esta información se puede usar para realizar una comparación posterior.

Captura de pantalla de las consultas de base de datos en la herramienta Base de datos.

Al examinar la instrucción SELECT generada por LINQ en la columna Consulta, identificamos la primera fila como la consulta asociada al método GetBlogTitleX. Para ver la cadena de consulta completa, amplíe el ancho de la columna. La cadena de consulta completa es:

SELECT "b"."Url", "b"."BlogId", "p"."PostId", "p"."Author", "p"."BlogId", "p"."Content", "p"."Date", "p"."MetaData", "p"."Title"
FROM "Blogs" AS "b" LEFT JOIN "Posts" AS "p" ON "b"."BlogId" = "p"."BlogId" ORDER BY "b"."BlogId"

Tenga en cuenta que la aplicación recupera muchos valores de columna aquí, tal vez más de los que necesitamos. Echemos un vistazo al código fuente.

Optimizar código

Es el momento de echar un vistazo al código fuente de GetBlogTitleX. En la herramienta Base de datos, haga clic con el botón derecho en la consulta y elija Ir al archivo de origen. En el código fuente de GetBlogTitleX, encontramos el siguiente código, que usa LINQ para leer la base de datos.

foreach (var blog in db.Blogs.Select(b => new { b.Url, b.Posts }).ToList())
  {
    foreach (var post in blog.Posts)
    {
      if (post.Author == "Fred Smith")
      {
        Console.WriteLine($"Post: {post.Title}");
      }
  }
}

Este código usa bucles foreach para buscar en la base de datos todos los blogs cuyo autor sea "Fred Smith". Si lo examina, puede ver que se generan muchos objetos en memoria: una nueva matriz de objetos para cada blog de la base de datos, cadenas asociadas para cada dirección URL y valores de las propiedades contenidas en las entradas, como el identificador de blog.

Investigamos un poco más y vemos algunas recomendaciones comunes sobre cómo optimizar las consultas LINQ y crear este código.

Sugerencia

De manera alternativa, podemos ahorrar tiempo y dejar que Copilot haga la investigación por nosotros.

foreach (var x in db.Posts.Where(p => p.Author.Contains("Fred Smith")).Select(b => b.Title).ToList())
{
  Console.WriteLine("Post: " + x);
}

En este código, hemos realizado varios cambios para optimizar la consulta:

  • Se ha agregado la cláusula Where y se ha eliminado uno de los bucles foreach.
  • Se ha proyectado solo la propiedad Title en la instrucción Select, que es todo lo que necesitamos en este ejemplo.

Hecho esto, volvemos a probar con las herramientas de generación de perfiles.

Optimice el código con Copilot

Si usamos Copilot, podemos pedirle que investigue los problemas de rendimiento por nosotros. Seleccione Preguntar a Copilot en el menú contextual y escriba la siguiente pregunta:

Can you make the LINQ query in this method faster?

Sugerencia

Puede usar comandos de barra diagonal como /optimize para ayudar a formular preguntas apropiadas para Copilot.

En este ejemplo, Copilot ofrece los siguientes cambios de código sugeridos, similares a nuestra consulta optimizada, junto con una explicación.

public void GetBlogTitleX()
{
    var posts = db.Posts
        .Where(post => post.Author == "Fred Smith")
        .Select(post => post.Title)
        .ToList();

    foreach (var postTitle in posts)
    {
        Console.WriteLine($"Post: {postTitle}");
    }
}

Results

Después de modificar el código, volvemos a ejecutar la herramienta Uso de CPU para tomar una tarea de seguimiento. La vista Árbol de llamadas muestra que GetBlogTitleX solo se ejecuta 1754 ms, y usa un 37 % del total de la CPU de la aplicación, una mejora significativa del 59 %.

Captura de pantalla del uso mejorado de la CPU en la vista Árbol de llamadas de la herramienta Uso de CPU.

Pase a la vista Gráfico de llamas para tener otra visualización de la mejora. En esta vista, GetBlogTitleX también usa una parte más pequeña de la CPU.

Captura de pantalla del uso mejorado de la CPU en la vista Grafo de llamas de la herramienta Uso de CPU.

Compruebe los resultados en el seguimiento de la herramienta Base de datos y esta consulta solo leer dos registros, en lugar de 100 000. Además, la consulta se ha simplificado mucho y elimina el elemento LEFT JOIN innecesario que se generó anteriormente.

Captura de pantalla de un menor tiempo de consulta en la herramienta Base de datos.

A continuación, volvemos a comprobar los resultados en la herramienta Asignación de objetos de .NET y vemos que GetBlogTitleX solo es responsable de 56 000 asignaciones de objetos, lo que supone una reducción de casi el 95 % con respecto a 900 000.

Captura de pantalla del menor número de asignaciones de memoria en la herramienta Asignación de objetos de .NET.

Iteración

Es posible que se necesiten varias optimizaciones y podemos seguir realizando iteraciones con los cambios de código para ver qué cambios mejoran el rendimiento y ayudan a reducir el coste de procesamiento.

Pasos siguientes

Los siguientes artículos y entradas de blog proporcionan más información para ayudarle a aprender a usar las herramientas de rendimiento de Visual Studio de forma eficaz.