Performance Tips and Tricks in .NET Applications (Sugerencias y trucos de rendimiento en aplicaciones .NET)
Manuel Schanzer
Microsoft Corporation
Agosto de 2001
Resumen: Este artículo es para desarrolladores que quieren ajustar sus aplicaciones para obtener un rendimiento óptimo en el mundo administrado. El código de ejemplo, las explicaciones y las directrices de diseño se tratan para las aplicaciones Database, Windows Forms y ASP, así como sugerencias específicas del lenguaje para Microsoft Visual Basic y C++administrado. (25 páginas impresas)
Contenido
Información general
Sugerencias de rendimiento para todas las aplicaciones
Sugerencias para acceso a bases de datos
Sugerencias de rendimiento para aplicaciones de ASP.NET
Sugerencias para migrar y desarrollar en Visual Basic
Sugerencias para migrar y desarrollar en C++ administrado
Recursos adicionales
Apéndice: Costo de las llamadas virtuales y las asignaciones
Información general
Este documento técnico está diseñado como referencia para desarrolladores que escriben aplicaciones para .NET y buscan varias maneras de mejorar el rendimiento. Si es un desarrollador que no está familiarizado con .NET, debe estar familiarizado con la plataforma y el lenguaje que prefiera. Este documento se basa estrictamente en ese conocimiento y supone que el programador ya sabe lo suficiente para ejecutar el programa. Si va a migrar una aplicación existente a .NET, merece la pena leer este documento antes de comenzar el puerto. Algunas de las sugerencias aquí son útiles en la fase de diseño y proporcionan información que debe tener en cuenta antes de comenzar el puerto.
Este documento se divide en segmentos, con sugerencias organizadas por tipo de proyecto y desarrollador. El primer conjunto de sugerencias es una lectura imprescindible para escribir en cualquier idioma y contiene consejos que le ayudarán con cualquier idioma de destino en Common Language Runtime (CLR). A continuación se muestra una sección relacionada con sugerencias específicas de ASP. El segundo conjunto de sugerencias se organiza por lenguaje, tratando con sugerencias específicas sobre el uso de C++ administrado y Microsoft® Visual Basic®.
Debido a las limitaciones de programación, el tiempo de ejecución de la versión 1 (v1) tenía que dirigirse primero a la funcionalidad más amplia y, a continuación, tratar con optimizaciones de mayúsculas y minúsculas especiales más adelante. Esto da como resultado algunos casos de palomar en los que el rendimiento se convierte en un problema. Por lo tanto, este documento cubre varias sugerencias diseñadas para evitar este caso. Estas sugerencias no serán relevantes en la próxima versión (vNext), ya que estos casos se identifican y optimizan sistemáticamente. Les señalaré a medida que avanzamos, y es para usted decidir si merece la pena el esfuerzo.
Sugerencias de rendimiento para todas las aplicaciones
Hay algunas sugerencias que recordar al trabajar en CLR en cualquier lenguaje. Estos son relevantes para todos y deben ser la primera línea de defensa al tratar con problemas de rendimiento.
Producir menos excepciones
La generación de excepciones puede ser muy costosa, así que asegúrese de que no se producen muchas de ellas. Use Perfmon para ver cuántas excepciones produce la aplicación. Es posible que le sorprenda que determinadas áreas de la aplicación produzcan más excepciones de las esperadas. Para mejorar la granularidad, también puede comprobar el número de excepción mediante programación mediante contadores de rendimiento.
Buscar y diseñar código pesado de excepciones puede dar lugar a un rendimiento decente. Tenga en cuenta que esto no tiene nada que ver con los bloques try/catch: solo incurre en el costo cuando se produce la excepción real. Puede usar tantos bloques try/catch como desee. El uso de excepciones de forma gratuita es donde se pierde el rendimiento. Por ejemplo, debe mantenerse alejado de cosas como el uso de excepciones para el flujo de control.
Este es un ejemplo sencillo de lo costoso que pueden ser las excepciones: simplemente ejecutaremos un bucle For , generando miles o excepciones y después finalizando. Pruebe a comentar la instrucción throw para ver la diferencia de velocidad: esas excepciones producen una sobrecarga enorme.
public static void Main(string[] args){
int j = 0;
for(int i = 0; i < 10000; i++){
try{
j = i;
throw new System.Exception();
} catch {}
}
System.Console.Write(j);
return;
}
- ¡Cuidado! El tiempo de ejecución puede producir excepciones por sí solas. Por ejemplo, Response.Redirect() produce una excepción ThreadAbort . Incluso si no inicia excepciones explícitamente, puede usar funciones que sí lo hacen. Asegúrese de comprobar Perfmon para obtener la historia real y el depurador para comprobar el origen.
- Para desarrolladores de Visual Basic: Visual Basic activa la comprobación int de forma predeterminada para asegurarse de que elementos como desbordamiento y división por cero inician excepciones. Es posible que quiera desactivar esta opción para obtener rendimiento.
- Si usa COM, debe tener en cuenta que HRESULTS puede devolver como excepciones. Asegúrese de realizar un seguimiento cuidadoso de estos.
Realizar llamadas fragmentada
Una llamada fragmentada es una llamada de función que realiza varias tareas, como un método que inicializa varios campos de un objeto. Esto se debe ver en las llamadas chatty, que realizan tareas muy sencillas y requieren varias llamadas para realizar tareas (por ejemplo, establecer cada campo de un objeto con una llamada diferente). Es importante realizar llamadas fragmentados, en lugar de chatty entre métodos en los que la sobrecarga es mayor que para las llamadas de método simples dentro de AppDomain. P/Invoke, interoperabilidad y llamadas de comunicación remota conllevan sobrecarga y desea usarlas con moderación. En cada uno de estos casos, debe intentar diseñar la aplicación para que no se base en llamadas pequeñas y frecuentes que conllevan tanta sobrecarga.
Una transición se produce siempre que se llama al código administrado desde código no administrado y viceversa. El tiempo de ejecución facilita al programador la interoperabilidad, pero esto conlleva un precio de rendimiento. Cuando se produce una transición, es necesario realizar los pasos siguientes:
- Realizar serialización de datos
- Corrección de la convención de llamada
- Protección de registros guardados por destinatarios
- Cambiar el modo de subproceso para que gc no bloquee subprocesos no administrados
- Erigir un marco de control de excepciones en llamadas a código administrado
- Tomar el control del subproceso (opcional)
Para acelerar el tiempo de transición, intente usar P/Invoke cuando pueda. La sobrecarga es tan pequeña como 31 instrucciones más el costo de serialización si se requiere serialización de datos y solo 8 de lo contrario. La interoperabilidad COM es mucho más cara, tomando al alza 65 instrucciones.
La serialización de datos no siempre es costosa. Los tipos primitivos no requieren casi ninguna serialización en absoluto y las clases con diseño explícito también son baratas. La ralentización real se produce durante la traducción de datos, como la conversión de texto de ASCI a Unicode. Asegúrese de que los datos que se pasan a través del límite administrado solo se convierten si es necesario: puede resultar que simplemente aceptando un determinado tipo de datos o formato en el programa, puede cortar una gran cantidad de sobrecarga de serialización.
Los siguientes tipos se denominan blittable, lo que significa que se pueden copiar directamente a través del límite administrado o no administrado sin serialización alguna: sbyte, byte, short, ushort, int, uint, long, ulong, float y double. Puede pasarlos de forma gratuita, así como ValueTypes y matrices unidimensionales que contengan tipos que se pueden transferir en bloque de bits. Los detalles de la serialización se pueden explorar más en MSDN Library. Te recomiendo leerlo cuidadosamente si pasas una gran cantidad de tiempo serializar.
Diseño con ValueTypes
Usa estructuras simples cuando puedas y cuando no hagas una gran cantidad de conversión boxing y unboxing. Este es un ejemplo sencillo para demostrar la diferencia de velocidad:
using System;
namespace ConsoleApplication{
public struct foo{
public foo(double arg){ this.y = arg; }
public double y;
}
public class bar{
public bar(double arg){ this.y = arg; }
public double y;
}
class Class1{
static void Main(string[] args){
System.Console.WriteLine("starting struct loop...");
for(int i = 0; i < 50000000; i++)
{foo test = new foo(3.14);}
System.Console.WriteLine("struct loop complete.
starting object loop...");
for(int i = 0; i < 50000000; i++)
{bar test2 = new bar(3.14); }
System.Console.WriteLine("All done");
}
}
}
Al ejecutar este ejemplo, verá que el bucle struct es órdenes de magnitud más rápido. Sin embargo, es importante tener en cuenta el uso de ValueTypes al tratarlos como objetos. Esto agrega sobrecarga adicional de conversión boxing y unboxing al programa, y puede acabar costando más de lo que haría si hubiera quedado bloqueado con objetos. Para ver esto en acción, modifique el código anterior para usar una matriz de foos y barras. Verá que el rendimiento es más o menos igual.
Equilibrios Los valueTypes son mucho menos flexibles que los objetos y terminan dañando el rendimiento si se usan incorrectamente. Debes tener mucho cuidado sobre cuándo y cómo los usas.
Pruebe a modificar el ejemplo anterior y almacene los foos y las barras dentro de matrices o tablas hash. Verá que la ganancia de velocidad desaparece, solo con una operación de conversión boxing y unboxing.
Puede realizar un seguimiento de la cantidad de conversión box y unboxing examinando las asignaciones y colecciones de GC. Esto se puede hacer mediante Perfmon externamente o Contadores de rendimiento en el código.
Consulte la explicación detallada de ValueTypes en Consideraciones de rendimiento de Run-Time Tecnologías en .NET Framework.
Usar AddRange para agregar grupos
Use AddRange para agregar una colección completa, en lugar de agregar cada elemento de la colección de forma iterativa. Casi todos los controles y colecciones de Windows tienen métodos Add y AddRange , y cada uno está optimizado para un propósito diferente. Agregar es útil para agregar un solo elemento, mientras que AddRange tiene cierta sobrecarga adicional, pero gana al agregar varios elementos. Estas son solo algunas de las clases que admiten Add y AddRange:
- StringCollection, TraceCollection, etc.
- HttpWebRequest
- Control de usuario
- ColumnHeader
Recorte del espacio de trabajo
Minimice el número de ensamblados que usa para mantener pequeño el espacio de trabajo. Si carga un ensamblado completo solo para usar un método, está pagando un enorme costo por muy poco beneficio. Vea si puede duplicar la funcionalidad de ese método mediante código que ya ha cargado.
Realizar un seguimiento de su espacio de trabajo es difícil y probablemente podría ser el tema de un documento completo. Estas son algunas sugerencias para ayudarle a salir:
- Use vadump.exe para realizar un seguimiento del espacio de trabajo. Esto se describe en otra notas del producto que abarca diversas herramientas para el entorno administrado.
- Mira perfmon o contadores de rendimiento. Pueden proporcionar comentarios detallados sobre el número de clases que se cargan o el número de métodos que se obtienen JITed. Puede obtener lecturas durante cuánto tiempo dedica el cargador o el porcentaje del tiempo de ejecución dedicado a la paginación.
Uso de bucles For para iteración de cadenas: versión 1
En C#, la palabra clave foreach permite recorrer los elementos de una lista, una cadena, etc. y realizan operaciones en cada elemento. Se trata de una herramienta muy eficaz, ya que actúa como enumerador de uso general en muchos tipos. El inconveniente de esta generalización es la velocidad y, si se basa en gran medida en la iteración de cadenas, debe usar un bucle For en su lugar. Dado que las cadenas son matrices de caracteres simples, se pueden recorrer con mucha menos sobrecarga que otras estructuras. El JIT es lo suficientemente inteligente (en muchos casos) para optimizar la comprobación de límites y otras cosas dentro de un bucle For , pero está prohibido hacerlo en paseos foreach . El resultado final es que, en la versión 1, un bucle For en cadenas es hasta cinco veces más rápido que usar foreach. Esto cambiará en versiones futuras, pero para la versión 1, esta es una manera definitiva de aumentar el rendimiento.
Este es un método de prueba sencillo para demostrar la diferencia de velocidad. Intente ejecutarlo y, a continuación, quite el bucle For y quite la marca de comentario de la instrucción foreach . En mi máquina, el bucle For tardó aproximadamente un segundo, con unos 3 segundos para la instrucción foreach .
public static void Main(string[] args) {
string s = "monkeys!";
int dummy = 0;
System.Text.StringBuilder sb = new System.Text.StringBuilder(s);
for(int i = 0; i < 1000000; i++)
sb.Append(s);
s = sb.ToString();
//foreach (char c in s) dummy++;
for (int i = 0; i < 1000000; i++)
dummy++;
return;
}
}
Los inconvenientesde Foreach
son mucho más legibles y, en el futuro, se convertirán tan rápido como un bucle For para casos especiales como cadenas. A menos que la manipulación de cadenas sea un verdadero hog de rendimiento para usted, es posible que el código ligeramente mesioso no valga la pena.
Uso de StringBuilder para la manipulación compleja de cadenas
Cuando se modifica una cadena, el tiempo de ejecución creará una nueva cadena y la devolverá, dejando el original para que se recopile como elemento no utilizado. La mayoría de las veces es una manera rápida y sencilla de hacerlo, pero cuando se modifica una cadena repetidamente, comienza a ser una carga en el rendimiento: todas esas asignaciones finalmente obtienen un costo. Este es un ejemplo sencillo de un programa que se anexa a una cadena 50 000 veces, seguido de uno que usa un objeto StringBuilder para modificar la cadena en contexto. El código de StringBuilder es mucho más rápido y, si los ejecuta, resulta inmediatamente obvio.
|
|
Intente ver Perfmon para ver cuánto tiempo se guarda sin asignar miles de cadenas. Examine el contador "% de tiempo en GC" en la lista Memoria clR de .NET. También puede realizar un seguimiento del número de asignaciones que guarde, así como las estadísticas de recopilación.
Inconvenientes= Hay cierta sobrecarga asociada a la creación de un objeto StringBuilder , tanto en tiempo como en memoria. En una máquina con memoria rápida, stringBuilder vale la pena si está realizando aproximadamente cinco operaciones. Como regla general, diría que 10 o más operaciones de cadena es una justificación de la sobrecarga en cualquier máquina, incluso una más lenta.
Precompilar aplicaciones de Windows Forms
Los métodos son JITed cuando se usan por primera vez, lo que significa que se paga una penalización de inicio mayor si la aplicación realiza una gran cantidad de llamadas a métodos durante el inicio. Windows Forms usar una gran cantidad de bibliotecas compartidas en el sistema operativo, y la sobrecarga al iniciarlas puede ser mucho mayor que otros tipos de aplicaciones. Aunque no siempre sucede, la precompilación de Windows Forms aplicaciones suele generar un resultado de rendimiento. En otros escenarios, suele ser mejor dejar que el JIT se ocupe de él, pero si es un desarrollador de Windows Forms es posible que quiera echar un vistazo.
Microsoft permite precompilar una aplicación mediante una llamada a ngen.exe
. Puede optar por ejecutar ngen.exe durante el tiempo de instalación o antes de distribuir la aplicación. Definitivamente tiene más sentido ejecutar ngen.exe durante el tiempo de instalación, ya que puede asegurarse de que la aplicación está optimizada para la máquina en la que se está instalando. Si ejecuta ngen.exe antes de enviar el programa, limite las optimizaciones a las disponibles en la máquina. Para darte una idea de cuánto puede ayudar la precompilación, he ejecutado una prueba informal en mi máquina. A continuación se muestran los tiempos de inicio en frío para ShowFormComplex, una aplicación winforms con aproximadamente cien controles.
Estado del código | Time |
---|---|
JITed de marco ShowFormComplex JITed |
3,4 s |
Framework precompilado, ShowFormComplex JITed | 2,5 segundos |
Framework precompilado, ShowFormComplex precompilado | 2.1sec |
Cada prueba se realizó después de un reinicio. Como puede ver, Windows Forms aplicaciones usan una gran cantidad de métodos por adelantado, lo que hace que sea un gran éxito de rendimiento para precompilar.
Usar matrices escalonadas: versión 1
El JIT v1 optimiza las matrices escalonadas (simplemente "matrices de matrices") de forma más eficaz que las matrices rectangulares, y la diferencia es bastante notable. Esta es una tabla que muestra la ganancia de rendimiento resultante del uso de matrices escalonadas en lugar de matrices rectangulares en C# y Visual Basic (los números más altos son mejores):
C# | Visual Basic 7 | |
---|---|---|
Asignación (escalonada) Asignación (rectangular) |
14.16 8.37 |
12.24 8,62 |
Red neuronal (escalonada) Red neuronal (rectangular) |
4.48 3.00 |
4.58 3.13 |
Orden numérico (escalonado) Ordenación numérica (rectangular) |
4.88 2,05 |
5,07 2,06 |
El banco de pruebas de asignación es un algoritmo de asignación simple, adaptado a partir de la guía paso a paso que se encuentra en La toma de decisiones cuantitativas para empresas (Gordon, Pressman y Cohn; Prentice-Hall; fuera de impresión). La prueba de red neuronal ejecuta una serie de patrones a través de una red neuronal pequeña y la ordenación numérica es autoexplicativa. En conjunto, estos puntos de referencia representan una buena indicación del rendimiento real.
Como puede ver, el uso de matrices escalonadas puede dar lugar a un aumento bastante dramático del rendimiento. Las optimizaciones realizadas en matrices escalonadas se agregarán a versiones futuras del JIT, pero para v1 puede ahorrar mucho tiempo mediante matrices escalonadas.
Mantener el tamaño del búfer de E/S entre 4 KB y 8 KB
Para casi todas las aplicaciones, un búfer entre 4 KB y 8 KB le proporcionará el máximo rendimiento. En el caso de instancias muy específicas, es posible que pueda obtener una mejora de un búfer mayor (cargando imágenes grandes de un tamaño predecible, por ejemplo), pero en el 99,99 % de los casos, solo desperdiciará memoria. Todos los búferes derivados de BufferedStream permiten establecer el tamaño en todo lo que desee, pero en la mayoría de los casos 4 y 8 le proporcionarán el mejor rendimiento.
Estar en lookout para oportunidades de E/S asincrónicas
En raras ocasiones, es posible que pueda beneficiarse de la E/S asincrónica. Un ejemplo podría estar descargando y descomprimiendo una serie de archivos: puede leer los bits de una secuencia, descodificarlos en la CPU y escribirlos en otro. Requiere mucho esfuerzo para usar la E/S asincrónica de forma eficaz y puede dar lugar a una pérdida de rendimiento si no se hace correctamente. La ventaja es que, cuando se aplica correctamente, la E/S asincrónica puede proporcionarle hasta diez veces el rendimiento.
Un excelente ejemplo de un programa que usa E/S asincrónica está disponible en MSDN Library.
- Una cosa que hay que tener en cuenta es que hay una pequeña sobrecarga de seguridad para las llamadas asincrónicas: al invocar una llamada asincrónica, el estado de seguridad de la pila del autor de la llamada se captura y se transfiere al subproceso que ejecutará realmente la solicitud. Esto puede no ser un problema si la devolución de llamada ejecuta una gran cantidad de código o si las llamadas asincrónicas no se usan excesivamente.
Sugerencias para acceso a bases de datos
La filosofía de ajuste para el acceso a la base de datos es usar solo la funcionalidad que necesita y diseñar en torno a un enfoque "desconectado": realizar varias conexiones en secuencia, en lugar de mantener abierta una única conexión durante mucho tiempo. Debe tener en cuenta este cambio y diseñarlo.
Microsoft recomienda una estrategia de N niveles para obtener el máximo rendimiento, en lugar de una conexión directa de cliente a base de datos. Tenga en cuenta esto como parte de su filosofía de diseño, ya que muchas de las tecnologías implementadas están optimizadas para aprovechar un escenario multi cansado.
Usar el Proveedor de datos administrados óptimo
Elija el proveedor administrado correcto, en lugar de confiar en un descriptor de acceso genérico. Hay proveedores administrados escritos específicamente para muchas bases de datos diferentes, como SQL (System.Data.SqlClient). Si usa una interfaz más genérica, como System.Data.Odbc, cuando pueda usar un componente especializado, perderá el rendimiento que trata con el nivel agregado de direccionamiento indirecto. El uso del proveedor óptimo también puede hacer que hable un lenguaje diferente: el cliente SQL administrado habla TDS a una base de datos SQL, lo que proporciona una mejora dramática sobre oleDbprotocol genérico.
Elegir lector de datos sobre el conjunto de datos cuando se puede
Use un lector de datos cada vez que no sea necesario mantener los datos en torno a ellos. Esto permite una lectura rápida de los datos, que se pueden almacenar en caché si el usuario lo desea. Un lector es simplemente una secuencia sin estado que permite leer datos a medida que llegan y, a continuación, quitarlos sin almacenarlos en un conjunto de datos para obtener más navegación. El enfoque de flujo es más rápido y tiene menos sobrecarga, ya que puede empezar a usar los datos inmediatamente. Debe evaluar con qué frecuencia necesita los mismos datos para decidir si el almacenamiento en caché para la navegación tiene sentido para usted. Esta es una tabla pequeña que muestra la diferencia entre DataReader y DataSet en proveedores ODBC y SQL al extraer datos de un servidor (los números más altos son mejores):
ADO | SQL | |
---|---|---|
DataSet | 801 | 2507 |
DataReader | 1083 | 4585 |
Como puede ver, el máximo rendimiento se logra al usar el proveedor administrado óptimo junto con un lector de datos. Cuando no es necesario almacenar en caché los datos, el uso de un lector de datos puede proporcionarle un enorme aumento del rendimiento.
Uso de Mscorsvr.dll para máquinas mp
En el caso de las aplicaciones independientes de nivel intermedio y servidor, asegúrese mscorsvr
de que se usa para máquinas multiprocesador. Mscorwks no está optimizado para el escalado o el rendimiento, mientras que la versión del servidor tiene varias optimizaciones que le permiten escalar bien cuando hay más de un procesador disponible.
Usar procedimientos almacenados siempre que sea posible
Los procedimientos almacenados son herramientas altamente optimizadas que dan como resultado un rendimiento excelente cuando se usan de forma eficaz. Configure procedimientos almacenados para controlar inserciones, actualizaciones y eliminaciones con el adaptador de datos. Los procedimientos almacenados no tienen que interpretarse, compilarse ni transmitirse desde el cliente, ni reducirse tanto en el tráfico de red como en la sobrecarga del servidor. Asegúrese de usar CommandType.StoredProcedure en lugar de CommandType.Text.
Tenga cuidado con las cadenas de conexión dinámicas.
La agrupación de conexiones es una manera útil de reutilizar las conexiones para varias solicitudes, en lugar de pagar la sobrecarga de abrir y cerrar una conexión para cada solicitud. Se realiza implícitamente, pero se obtiene un grupo por cadena de conexión única. Si va a generar cadenas de conexión dinámicamente, asegúrese de que las cadenas sean idénticas cada vez que se produzca la agrupación. Tenga en cuenta también que si se está produciendo la delegación, obtendrá un grupo por usuario. Hay una gran cantidad de opciones que puede establecer para el grupo de conexiones y puede realizar un seguimiento del rendimiento del grupo mediante perfmon para realizar un seguimiento de cosas como el tiempo de respuesta, las transacciones por segundo, etc.
Desactivar las características que no usa
Desactive la inscripción automática de transacciones si no es necesario. Para el Proveedor de datos administrados de SQL, se realiza a través de la cadena de conexión:
SqlConnection conn = new SqlConnection(
"Server=mysrv01;
Integrated Security=true;
Enlist=false");
Al rellenar un conjunto de datos con el adaptador de datos, no obtenga información de clave principal si no tiene que (por ejemplo, no establezca MissingSchemaAction.Add con clave):
public DataSet SelectSqlSrvRows(DataSet dataset,string connection,string query){
SqlConnection conn = new SqlConnection(connection);
SqlDataAdapter adapter = new SqlDataAdapter();
adapter.SelectCommand = new SqlCommand(query, conn);
adapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;
adapter.Fill(dataset);
return dataset;
}
Evitar comandos generados automáticamente
Al usar un adaptador de datos, evite los comandos generados automáticamente. Estos requieren viajes adicionales al servidor para recuperar metadatos y proporcionarle un nivel inferior de control de interacción. Aunque el uso de comandos generados automáticamente es cómodo, merece la pena hacerlo usted mismo en aplicaciones críticas para el rendimiento.
Tenga en cuenta el diseño heredado de ADO
Tenga en cuenta que, al ejecutar un comando o llamar a fill en el adaptador, se devuelve cada registro especificado por la consulta.
Si los cursores de servidor son absolutamente necesarios, se pueden implementar a través de un procedimiento almacenado en t-sql. Evite siempre que sea posible porque las implementaciones basadas en cursores de servidor no se escalan muy bien.
Si es necesario, implemente la paginación de forma sin estado y sin conexión. Puede agregar registros adicionales al conjunto de datos mediante:
- Asegurarse de que la información de PK está presente
- Cambiar el comando select del adaptador de datos según corresponda y
- Relleno de llamadas
Mantener la inclinación de los conjuntos de datos
Coloque solo los registros que necesita en el conjunto de datos. Recuerde que el conjunto de datos almacena todos sus datos en memoria y que cuantos más datos solicite, más tiempo tardará en transmitirse a través de la conexión.
Usar el acceso secuencial con la mayor frecuencia posible
Con un lector de datos, use CommandBehavior.SequentialAccess. Esto es esencial para tratar con tipos de datos de blobs, ya que permite que los datos se lean fuera de la conexión en fragmentos pequeños. Aunque solo puede trabajar con un fragmento de los datos a la vez, la latencia para cargar un tipo de datos grande desaparece. Si no necesita trabajar todo el objeto a la vez, el uso de Acceso secuencial le proporcionará un rendimiento mucho mejor.
Sugerencias de rendimiento para aplicaciones de ASP.NET
Almacenar en caché de forma agresiva
Al diseñar una aplicación mediante ASP.NET, asegúrese de diseñar con un ojo en el almacenamiento en caché. En las versiones de servidor del sistema operativo, tiene muchas opciones para ajustar el uso de cachés en el servidor y en el lado cliente. Hay varias características y herramientas en ASP que puede usar para obtener rendimiento.
Almacenamiento en caché de salida: almacena el resultado estático de una solicitud ASP. Se especifica mediante la <@% OutputCache %>
directiva :
- Duración: el elemento de tiempo existe en la memoria caché.
- VaryByParam: varía las entradas de caché por parámetros Get/Post
- VaryByHeader: varía las entradas de caché por encabezado Http
- VaryByCustom: varía las entradas de caché por explorador
- Invalide para variar según lo que desee:
Almacenamiento en caché de fragmentos: cuando no es posible almacenar una página completa (privacidad, personalización, contenido dinámico), puede usar el almacenamiento en caché de fragmentos para almacenar partes de ella para una recuperación más rápida más adelante.
a) VaryByControl: varía los elementos almacenados en caché por valores de un control.
API de caché: proporciona una granularidad extremadamente fina para el almacenamiento en caché manteniendo una tabla hash de objetos almacenados en caché en la memoria (System.web.UI.caching). También:
a) Incluye dependencias (clave, archivo, hora)
b) Expira automáticamente los elementos sin usar
c) Admite devoluciones de llamada
El almacenamiento en caché de forma inteligente puede ofrecer un rendimiento excelente y es importante pensar en qué tipo de almacenamiento en caché necesita. Imagine un sitio de comercio electrónico complejo con varias páginas estáticas para el inicio de sesión y, a continuación, una gran cantidad de páginas generadas dinámicamente que contienen imágenes y texto. Es posible que quiera usar el almacenamiento en caché de salida para esas páginas de inicio de sesión y, a continuación, el almacenamiento en caché de fragmentos para las páginas dinámicas. Por ejemplo, una barra de herramientas podría almacenarse en caché como un fragmento. Para mejorar aún mejor el rendimiento, podría almacenar en caché imágenes y texto reutilizable que aparecen con frecuencia en el sitio mediante cache API. Para obtener información detallada sobre el almacenamiento en caché (con código de ejemplo), consulte el sitio web de ASP. NET .
Use el estado de sesión solo si necesita
Una característica extremadamente eficaz de ASP.NET es su capacidad de almacenar el estado de sesión para los usuarios, como un carro de la compra en un sitio de comercio electrónico o un historial del explorador. Puesto que está activado de forma predeterminada, se paga el costo en memoria incluso si no se usa. Si no usa el estado de sesión, desactive y ahorre la sobrecarga agregando <@% EnabledSessionState = false %> a asp. Esto incluye otras opciones, que se explican en el sitio web de ASP. NET .
Para las páginas que solo leen el estado de sesión, puede elegir EnabledSessionState=readonly. Esto conlleva menos sobrecarga que el estado completo de la sesión de lectura y escritura y resulta útil cuando solo necesita parte de la funcionalidad y no quiere pagar por las funcionalidades de escritura.
Use el estado de vista solo si necesita
Un ejemplo de Estado de vista podría ser un formulario largo que los usuarios deben rellenar: si hacen clic en Atrás en el explorador y, a continuación, devuelven, el formulario permanecerá rellenado. Cuando no se usa esta funcionalidad, este estado consume memoria y rendimiento. Quizás la purga de rendimiento más grande aquí es que se debe enviar una señal de ida y vuelta a través de la red cada vez que se carga la página para actualizar y comprobar la memoria caché. Puesto que está activado de forma predeterminada, deberá especificar que no desea usar El estado de vista con <@% EnabledViewState = false %>. Debe obtener más información sobre View State (Ver estado) en el sitio web ASP. NET para obtener información sobre algunas de las otras opciones y configuraciones a las que tiene acceso.
Evitar STA COM
Apartment COM está diseñado para tratar el subproceso en entornos no administrados. Hay dos tipos de Apartment COM: uniproceso y multiproceso. MTA COM está diseñado para controlar el multithreading, mientras que STA COM se basa en el sistema de mensajería para serializar las solicitudes de subprocesos. El mundo administrado es un subproceso libre y el uso de COM de apartamento de subprocesos únicos requiere que todos los subprocesos no administrados compartan esencialmente un único subproceso para la interoperabilidad. Esto da como resultado un impacto masivo en el rendimiento y debe evitarse siempre que sea posible. Si no puede portar el objeto COM apartment al mundo administrado, use <@%AspCompat = "true" %> para las páginas que las usan. Para obtener una explicación más detallada de STA COM, consulte MSDN Library.
Compilación por lotes
Compile siempre por lotes antes de implementar una página grande en la Web. Esto se puede iniciar realizando una solicitud a una página por directorio y esperando hasta que la CPU vuelva a estar inactiva. Esto impide que el servidor web se atascado con compilaciones mientras también intenta atender páginas.
Eliminación de módulos HTTP innecesarios
En función de las características usadas, quite los módulos HTTP no usados o innecesarios de la canalización. Recuperar la memoria agregada y los ciclos de desperdiciado pueden proporcionarle un pequeño aumento de velocidad.
Evitar la característica Autoeventwireup
En lugar de basarse en autoeventwireup, invalide los eventos de Page. Por ejemplo, en lugar de escribir un método Page_Load(), intente sobrecargar el método public void OnLoad(). Esto permite que el tiempo de ejecución tenga que hacer un createDelegate() para cada página.
Codificación mediante ASCII cuando no se necesita UTF
De forma predeterminada, ASP.NET viene configurado para codificar solicitudes y respuestas como UTF-8. Si ASCII es todo lo que necesita la aplicación, la sobrecarga UTF puede devolverle algunos ciclos. Tenga en cuenta que esto solo se puede hacer por aplicación.
Usar el procedimiento de autenticación óptima
Hay varias maneras diferentes de autenticar a un usuario y algunas de más caras que otras (para aumentar el costo: Ninguno, Windows, Forms, Passport). Asegúrese de usar el más barato que mejor se adapte a sus necesidades.
Sugerencias para migrar y desarrollar en Visual Basic
Ha cambiado mucho en segundo plano de Microsoft® Visual Basic® 6 a Microsoft® Visual Basic® 7 y el mapa de rendimiento ha cambiado con él. Debido a las restricciones de seguridad y funcionalidad agregadas de CLR, algunas funciones simplemente no se pueden ejecutar tan rápidamente como lo hicieron en Visual Basic 6. De hecho, hay varias áreas en las que Visual Basic 7 se trounce por su predecesor. Afortunadamente, hay dos partes de buenas noticias:
- La mayoría de las peores ralentizaciones se producen durante las funciones únicas, como cargar un control por primera vez. El costo está ahí, pero solo lo paga una vez.
- Hay muchas áreas en las que Visual Basic 7 es más rápida y estas áreas tienden a residir en funciones que se repiten durante el tiempo de ejecución. Esto significa que la ventaja crece con el tiempo y, en varios casos, superará los costos únicos.
La mayoría de los problemas de rendimiento proceden de áreas en las que el tiempo de ejecución no admite una característica de Visual Basic 6 y se debe agregar para conservar la característica en Visual Basic 7. Trabajar fuera del tiempo de ejecución es más lento, lo que hace que algunas características sean mucho más costosas de usar. El lado brillante es que puede evitar estos problemas con un poco de esfuerzo. Hay dos áreas principales que requieren trabajo para optimizar el rendimiento y algunos ajustes sencillos que puede hacer aquí y allí. En conjunto, estos pueden ayudarle a desplazarse por las purgas de rendimiento y aprovechar las funciones que son mucho más rápidas en Visual Basic 7.
Tratamiento de errores
La primera preocupación es el control de errores. Esto ha cambiado mucho en Visual Basic 7 y hay problemas de rendimiento relacionados con el cambio. Básicamente, la lógica necesaria para implementar OnErrorGoto y Resume es extremadamente costosa. Sugiero echar un vistazo rápido al código y resaltar todas las áreas donde se usa el objeto Err o cualquier mecanismo de control de errores. Ahora examine cada una de estas instancias y compruebe si puede volver a escribirlas para usar try/catch. Muchos desarrolladores encontrarán que pueden convertir a try/catch fácilmente para la mayoría de estos casos, y deberían ver una buena mejora del rendimiento en su programa. La regla general es "si puede ver fácilmente la traducción, hacerlo".
Este es un ejemplo de un sencillo programa de Visual Basic que usa On Error Goto en comparación con la versión try/catch .
|
|
El aumento de velocidad es notable. SubWithError() toma 244 milisegundos con OnErrorGoto y solo 169 milisegundos con try/catch. La segunda función toma 179 milisegundos en comparación con 164 milisegundos para la versión optimizada.
Uso del enlace anticipado
La segunda preocupación se ocupa de los objetos y la difusión de tipos. Visual Basic 6 realiza una gran cantidad de trabajo en segundo plano para admitir la conversión de objetos y muchos programadores ni siquiera lo conocen. En Visual Basic 7, se trata de un área que puede exprimir mucho rendimiento. Al compilar, use el enlace anticipado. Esto indica al compilador que inserte una coerción de tipos solo se realiza cuando se menciona explícitamente. Esto tiene dos efectos principales:
- Los errores extraños son más fáciles de rastrear.
- Se eliminan las coerciones innecesarias, lo que da lugar a mejoras considerables en el rendimiento.
Cuando se usa un objeto como si fuera de un tipo diferente, Visual Basic coercerá el objeto por usted si no se especifica. Esto es útil, ya que el programador tiene que preocuparse por menos código. El inconveniente es que estas coerciones pueden hacer cosas inesperadas y el programador no tiene control sobre ellas.
Hay instancias en las que tiene que usar el enlace en tiempo de ejecución, pero la mayoría de las veces si no está seguro, puede salir con el enlace anticipado. En el caso de los programadores de Visual Basic 6, esto puede resultar un poco incómodo al principio, ya que tiene que preocuparse por los tipos más que en el pasado. Esto debería ser fácil para los nuevos programadores y los usuarios familiarizados con Visual Basic 6 lo recogerán en ningún momento.
Activar option Strict y Explicit
Con Option Strict activado, se protege del enlace en tiempo de ejecución accidental y se aplica un mayor nivel de materia de codificación. Para obtener una lista de las restricciones presentes con Option Strict, vea MSDN Library. La advertencia es que todas las coerciones de tipos de restricción deben especificarse explícitamente. Sin embargo, esto en sí mismo puede descubrir otras secciones del código que están haciendo más trabajo de lo que había pensado anteriormente, y puede ayudarle a robar algunos errores en el proceso.
Option Explicit es menos restrictivo que Option Strict, pero sigue obligando a los programadores a proporcionar más información en su código. En concreto, debe declarar una variable antes de usarla. Esto mueve la inferencia de tipos del tiempo de ejecución al tiempo de compilación. Esta comprobación eliminada se traduce en un rendimiento adicional.
Te recomiendo que empieces por Option Explicit y, a continuación, active Option Strict. Esto le protegerá de un deslugio de errores del compilador y le permitirá empezar a trabajar gradualmente en el entorno más estricto. Cuando se usan ambas opciones, se garantiza el máximo rendimiento de la aplicación.
Usar comparación binaria para texto
Al comparar texto, use la comparación binaria en lugar de la comparación de texto. En tiempo de ejecución, la sobrecarga es mucho más ligera para el binario.
Minimizar el uso de Format()
Cuando pueda, use toString() en lugar de format().. En la mayoría de los casos, le proporcionará la funcionalidad que necesita, con mucha menos sobrecarga.
Uso de Charw
Use charw en lugar de char. CLR usa Unicode internamente y char
debe traducirse en tiempo de ejecución si se usa. Esto puede dar lugar a una pérdida sustancial de rendimiento y especificar que los caracteres son una palabra completa larga (el uso de charw)
elimina esta conversión.
Optimizar asignaciones
Use exp += val en lugar de exp = exp + val. Dado que exp
puede ser arbitrariamente complejo, esto puede dar lugar a un gran trabajo innecesario. Esto obliga al JIT a evaluar ambas copias de exp y muchas veces esto no es necesario. La primera instrucción se puede optimizar mucho mejor que la segunda, ya que el JIT puede evitar evaluar el exp dos veces.
Evitar direccionamiento indirecto innecesario
Cuando se usa byRef, se pasan punteros en lugar del objeto real. Muchas veces esto tiene sentido (funciones de efecto secundario, por ejemplo), pero no siempre lo necesita. Pasar punteros da como resultado un direccionamiento más indirecto, que es más lento que acceder a un valor que se encuentra en la pila. Cuando no es necesario pasar por el montón, es mejor evitarlo.
Colocar concatenaciones en una expresión
Si tiene varias concatenaciones en varias líneas, intente pegarlas todas en una expresión. El compilador puede optimizar modificando la cadena en su lugar, lo que proporciona una velocidad y un aumento de memoria. Si las instrucciones se dividen en varias líneas, el compilador de Visual Basic no generará el lenguaje intermedio de Microsoft (MSIL) para permitir la concatenación en contexto. Consulte el ejemplo de StringBuilder descrito anteriormente.
Incluir instrucciones Return
Visual Basic permite que una función devuelva un valor sin usar la instrucción return . Aunque Visual Basic 7 admite esto, el uso explícito de return permite al JIT realizar ligeramente más optimizaciones. Sin una instrucción return, a cada función se le asignan varias variables locales en la pila para admitir de forma transparente la devolución de valores sin la palabra clave . Mantenerlas en torno dificulta la optimización de JIT y puede afectar al rendimiento del código. Examine las funciones e inserte la devolución según sea necesario. No cambia la semántica del código y puede ayudarle a obtener más velocidad de la aplicación.
Sugerencias para migrar y desarrollar en C++ administrado
Microsoft tiene como destino C++ administrado (MC++) en un conjunto específico de desarrolladores. MC++ no es la mejor herramienta para cada trabajo. Después de leer este documento, puede decidir que C++ no es la mejor herramienta y que los costos compensados no valen la pena. Si no está seguro de MC++, hay muchos recursos buenos para ayudarle a tomar su decisión. Esta sección está dirigida a desarrolladores que ya han decidido que quieren usar MC++ de alguna manera y quieren conocer los aspectos de rendimiento.
Para los desarrolladores de C++, trabajar con C++ requiere que se tomen varias decisiones. ¿Estás portar código antiguo? Si es así, ¿desea mover todo el elemento al espacio administrado o está planeando implementar un contenedor? Me centraré en la opción "port-everything" o trataré de escribir MC++ desde cero para los fines de esta discusión, ya que son los escenarios en los que el programador observará una diferencia de rendimiento.
Ventajas del mundo administrado
La característica más eficaz de C++ administrado es la capacidad de mezclar y combinar código administrado y no administrado en el nivel de expresión. Ningún otro lenguaje le permite hacer esto, y hay algunas ventajas eficaces que puede obtener de él si se usa correctamente. Le guiaré por algunos ejemplos de esto más adelante.
El mundo administrado también le da enormes victorias de diseño, en que muchos problemas comunes se ocupan de usted. La administración de memoria, la programación de subprocesos y las coerciones de tipos se pueden dejar en tiempo de ejecución si lo desea, lo que le permite centrar sus energías en las partes del programa que la necesitan. Con MC++, puede elegir exactamente cuánto control desea conservar.
Los programadores de MC++ tienen el lujo de poder usar el back-end de Microsoft Visual C® 7 (VC7) al compilar en IL y, a continuación, usar JIT encima de eso. Los programadores que se usan para trabajar con el compilador de Microsoft C++ se usan para las cosas que son muy rápidas. El JIT se diseñó con diferentes objetivos y tiene un conjunto diferente de puntos fuertes y débiles. El compilador VC7, no enlazado por las restricciones de tiempo del JIT, puede realizar ciertas optimizaciones que el JIT no puede, como el análisis de todo el programa, la inserción y la inscripción más agresivas. También hay algunas optimizaciones que solo se pueden realizar en entornos de typesafe, dejando más espacio para la velocidad que permite C++.
Debido a las diferentes prioridades del JIT, algunas operaciones son más rápidas que antes, mientras que otras son más lentas. Hay inconvenientes que usted hace para la seguridad y la flexibilidad del lenguaje, y algunos de ellos no son baratos. Afortunadamente, hay cosas que un programador puede hacer para minimizar los costos.
Migración: todo el código de C++ se puede compilar en MSIL
Antes de continuar, es importante tener en cuenta que puede compilar cualquier código de C++ en MSIL. Todo funcionará, pero no hay ninguna garantía de seguridad de tipo y usted paga la penalización de serialización si hace una gran cantidad de interoperabilidad. ¿Por qué resulta útil compilar en MSIL si no obtiene ninguna de las ventajas? En situaciones en las que va a portar una base de código grande, esto le permite migrar gradualmente el código en partes. Puede dedicar tiempo a portar más código, en lugar de escribir contenedores especiales para pegar el código portado y aún no portado juntos si usa MC++, lo que puede dar lugar a una gran victoria. Hace que las aplicaciones de portabilidad sea un proceso muy limpio. Para obtener más información sobre la compilación de C++ en MSIL, eche un vistazo a la opción del compilador /clr.
Sin embargo, simplemente compilar el código de C++ en MSIL no proporciona la seguridad ni la flexibilidad del mundo administrado. Debe escribir en MC++y en v1, lo que significa renunciar a algunas características. La lista siguiente no se admite en la versión actual de CLR, pero puede estar en el futuro. Microsoft eligió admitir primero las características más comunes y tuvo que cortar algunas otras para enviarse. No hay nada que impida que se agreguen más adelante, pero mientras tanto tendrá que hacer sin ellos:
- Herencia múltiple
- Plantillas
- Finalización determinista
Siempre puede interoperar con código no seguro si necesita esas características, pero pagará la penalización del rendimiento de serializar los datos hacia atrás y hacia atrás. Y tenga en cuenta que esas características solo se pueden usar dentro del código no administrado. El espacio administrado no tiene conocimiento de su existencia. Si decide portar el código, piense en cuánto se basa en esas características en el diseño. En algunos casos, el rediseño es demasiado caro y querrá seguir con código no administrado. Esta es la primera decisión que debe tomar, antes de empezar a piratear.
Ventajas de MC++ sobre C# o Visual Basic
Procedente de un fondo no administrado, MC++ conserva una gran cantidad de la capacidad de controlar el código no seguro. La capacidad de MC++de mezclar código administrado y no administrado sin problemas proporciona al desarrollador mucha potencia, y puede elegir dónde se encuentra el degradado que desea sentar al escribir el código. En un extremo, puede escribir todo en C++ recto, sindulzar y simplemente compilar con /clr. Por otro lado, puede escribir todo como objetos administrados y tratar las limitaciones del lenguaje y los problemas de rendimiento mencionados anteriormente.
Pero el verdadero poder de MC++ viene cuando eliges en algún lugar entre ellos. MC++ le permite ajustar algunos de los aciertos de rendimiento inherentes al código administrado, ya que le proporciona un control preciso sobre cuándo usar características no seguras. C# tiene parte de esta funcionalidad en la palabra clave unsafe , pero no es una parte integral del lenguaje y es mucho menos útil que MC++. Veamos algunos ejemplos que muestran la granularidad más fina disponible en MC++, y hablaremos sobre las situaciones en las que resulta útil.
Punteros "byref" generalizados
En C# solo puede tomar la dirección de algún miembro de una clase pasándola a un parámetro ref . En MC++, un puntero byref es una construcción de primera clase. Puede tomar la dirección de un elemento en medio de una matriz y devolver esa dirección de una función:
Byte* AddrInArray( Byte b[] ) {
return &b[5];
}
Aprovechamos esta característica para devolver un puntero a los "caracteres" de un System.String a través de nuestra rutina auxiliar, y incluso podemos recorrer matrices mediante estos punteros:
System::Char* PtrToStringChars(System::String*);
for( Char*pC = PtrToStringChars(S"boo");
pC != NULL;
pC++ )
{
... *pC ...
}
También puede realizar un recorrido de lista vinculada con inyección en MC++ tomando la dirección del campo "siguiente" (que no puede hacer en C#):
Node **w = &Head;
while(true) {
if( *w == 0 || val < (*w)->val ) {
Node *t = new Node(val,*w);
*w = t;
break;
}
w = &(*w)->next;
}
En C#, no puede apuntar a "Head" ni tomar la dirección del campo "next", por lo que tiene que hacer un caso especial donde se va a insertar en la primera ubicación o si "Head" es null. Además, tiene que buscar un nodo con antelación todo el tiempo en el código. Compare esto con lo que produciría un buen C#:
if( Head==null || val < Head.val ) {
Node t = new Node(val,Head);
Head = t;
}else{
// we know at least one node exists,
// so we can look 1 node ahead
Node w=Head;
while(true) {
if( w.next == null || val < w.next.val ){
Node t = new Node(val,w.next.next);
w.next = t;
break;
}
w = w.next;
}
}
Acceso de usuario a tipos boxed
Un problema de rendimiento común con los lenguajes de OO es el tiempo dedicado a la conversión boxing y unboxing de valores. MC++ proporciona mucho más control sobre este comportamiento, por lo que no tendrá que desaboxar dinámicamente (o estáticamente) para acceder a los valores. Se trata de otra mejora de rendimiento. Simplemente coloque __box palabra clave antes de cualquier tipo para representar su forma boxed:
__value struct V {
int i;
};
int main() {
V v = {10};
__box V *pbV = __box(v);
pbV->i += 10; // update without casting
}
En C# tiene que desenlace a una "v" y, a continuación, actualizar el valor y volver a volver a realizar la conversión boxing en un objeto:
struct B { public int i; }
static void Main() {
B b = new B();
b.i = 5;
object o = b; // implicit box
B b2 = (B)o; // explicit unbox
b2.i++; // update
o = b2; // implicit re-box
}
Colecciones de STL frente a colecciones administradas: v1
Las malas noticias: en C++, el uso de las colecciones de STL a menudo era tan rápido como escribir esa funcionalidad a mano. Los marcos CLR son muy rápidos, pero sufren problemas de conversión boxing y unboxing: todo es un objeto y sin plantilla o soporte genérico, todas las acciones deben comprobarse en tiempo de ejecución.
La buena noticia: a largo plazo, puede apostar que este problema desaparecerá a medida que se agreguen genéricos al tiempo de ejecución. El código que implemente hoy experimentará el aumento de velocidad sin cambios. A corto plazo, puede usar la conversión estática para evitar la comprobación, pero esto ya no es seguro. Recomiendo usar este método en código estricto donde el rendimiento es absolutamente crítico y ha identificado dos o tres puntos de acceso frecuente.
Uso de objetos administrados de pila
En C++, especifique que la pila o el montón deben administrar un objeto. Todavía puede hacerlo en MC++, pero hay restricciones que debe tener en cuenta. CLR usa ValueTypes para todos los objetos administrados por la pila y hay limitaciones en lo que ValueTypes puede hacer (por ejemplo, sin herencia). Hay más información disponible en MSDN Library.
Caso de esquina: tenga en cuenta las llamadas indirectas en el código administrado: v1
En tiempo de ejecución v1, todas las llamadas a funciones indirectas se realizan de forma nativa y, por tanto, requieren una transición al espacio no administrado. Cualquier llamada de función indirecta solo se puede realizar desde el modo nativo, lo que significa que todas las llamadas indirectas del código administrado necesitan una transición administrada a no administrada. Se trata de un problema grave cuando la tabla devuelve una función administrada, ya que se debe realizar una segunda transición para ejecutar la función. En comparación con el costo de ejecutar una sola instrucción call , el costo es cincuenta y cien veces más lento que en C++.
Afortunadamente, cuando se llama a un método que reside dentro de una clase recopilada por elementos no utilizados, la optimización lo quita. Sin embargo, en el caso específico de un archivo C++ normal que se ha compilado mediante /clr, el método devuelto se considerará administrado. Puesto que esto no se puede quitar mediante la optimización, se alcanza con el costo completo de doble transición. A continuación se muestra un ejemplo de este tipo de caso.
//////////////////////// a.h: //////////////////////////
class X {
public:
void mf1();
void mf2();
};
typedef void (X::*pMFunc_t)();
////////////// a.cpp: compiled with /clr /////////////////
#include "a.h"
int main(){
pMFunc_t pmf1 = &X::mf1;
pMFunc_t pmf2 = &X::mf2;
X *pX = new X();
(pX->*pmf1)();
(pX->*pmf2)();
return 0;
}
////////////// b.cpp: compiled without /clr /////////////////
#include "a.h"
void X::mf1(){}
////////////// c.cpp: compiled with /clr ////////////////////
#include "a.h"
void X::mf2(){}
Hay varias maneras de evitar esto:
- Convertir la clase en una clase administrada ("__gc")
- Quite la llamada indirecta, si es posible.
- Deje la clase compilada como código no administrado (por ejemplo, no use /clr).
Minimizar aciertos de rendimiento: versión 1
Hay varias operaciones o características que simplemente son más costosas en MC++ en la versión 1 JIT. Los enumeraré y les daré una explicación y luego hablaremos de lo que puede hacer sobre ellos.
- Abstracciones: se trata de un área en la que el compilador back-end de C++ más lento gana en gran medida sobre el JIT. Si ajusta un valor int dentro de una clase con fines de abstracción y tiene acceso a él estrictamente como un valor int, el compilador de C++ puede reducir la sobrecarga del contenedor a prácticamente nada. Puede agregar muchos niveles de abstracción al contenedor, sin aumentar el costo. JiT no puede tardar el tiempo necesario para eliminar este costo, lo que hace que las abstracciones profundas sean más costosas en MC++.
- Punto flotante: el JIT v1 no realiza actualmente todas las optimizaciones específicas de FP que realiza el back-end de VC++, lo que hace que las operaciones de punto flotante sean más costosas por ahora.
- Matrices multidimensionales: el JIT es mejor para controlar matrices escalonadas que las multidimensionales, por lo que usa matrices escalonadas en su lugar.
- Aritmética de 64 bits: en versiones futuras, se agregarán optimizaciones de 64 bits al JIT.
Qué puede hacer
En cada fase del desarrollo, hay varias cosas que puede hacer. Con MC++, la fase de diseño es quizás el área más importante, ya que determinará cuánto trabajo termina haciendo y cuánto rendimiento obtiene a cambio. Al sentarse para escribir o portar una aplicación, debe tener en cuenta lo siguiente:
- Identifique las áreas en las que se usan varias herencias, plantillas o finalización determinista. Tendrá que deshacerse de estos, o bien dejar esa parte del código en un espacio no administrado. Piense en el costo del rediseño e identifique las áreas que se pueden migrar.
- Busque puntos activos de rendimiento, como abstracciones profundas o llamadas de función virtual en el espacio administrado. También requerirán una decisión de diseño.
- Busque objetos que se hayan especificado como administrados por pila. Asegúrese de que se pueden convertir en ValueTypes. Marque los demás para la conversión a objetos administrados por montón.
Durante la fase de codificación, debe tener en cuenta las operaciones que son más costosas y las opciones que tiene para tratarlas. Una de las cosas más agradables sobre MC++ es que llega a tener todos los problemas de rendimiento por adelantado, antes de empezar a codificar: esto es útil para analizar el trabajo más adelante. Sin embargo, todavía hay algunos ajustes que puede realizar mientras se codifica y depura.
Determine qué áreas hacen un uso intensivo de las matrices aritméticas de punto flotante, matrices multidimensionales o funciones de biblioteca. ¿Cuál de estas áreas es fundamental para el rendimiento? Use generadores de perfiles para elegir los fragmentos en los que la sobrecarga le cuesta más y elegir cuál es la mejor opción:
- Mantenga todo el fragmento en un espacio no administrado.
- Use conversiones estáticas en los accesos a la biblioteca.
- Pruebe a ajustar el comportamiento de conversión boxing/unboxing (se explica más adelante).
- Codifique su propia estructura.
Por último, trabaje para minimizar el número de transiciones que realice. Si tiene algún código no administrado o una llamada de interoperabilidad que se encuentra en un bucle, haga que todo el bucle no esté administrado. De este modo, solo pagará el costo de transición dos veces, en lugar de por cada iteración del bucle.
Recursos adicionales
Entre los temas relacionados sobre el rendimiento de .NET Framework se incluyen:
Vea los artículos futuros que se encuentran actualmente en desarrollo, incluida una visión general de las filosofías de diseño, arquitectura y codificación, un tutorial de herramientas de análisis de rendimiento en el mundo administrado y una comparación de rendimiento de .NET con otras aplicaciones empresariales disponibles actualmente.
Apéndice: Costo de las llamadas virtuales y las asignaciones
Tipo de llamada | # Llamadas por segundo |
---|---|
Llamada no virtual ValueType | 809971805.600 |
Llamada no virtual de clase | 268478412.546 |
Llamada virtual de clase | 109117738.369 |
Llamada a ValueType Virtual (método Obj) | 3004286.205 |
Llamada a ValueType Virtual (método Obj invalidado) | 2917140.844 |
Tipo de carga mediante newing (no estático) | 1434.720 |
Tipo de carga mediante newing (métodos virtuales) | 1369.863 |
Nota La máquina de prueba es un PIII de 733Mhz, que ejecuta Windows 2000 Professional con Service Pack 2.
En este gráfico se compara el costo asociado a diferentes tipos de llamadas a métodos, así como el costo de crear instancias de un tipo que contiene métodos virtuales. Cuanto mayor sea el número, se pueden realizar más llamadas o instancias por segundo. Aunque estos números variarán sin duda en diferentes máquinas y configuraciones, el costo relativo de realizar una llamada a través de otra sigue siendo significativa.
- ValueType Non-Virtual Call: esta prueba llama a un método no virtual vacío incluido en un ValueType.
- Llamada no virtual de clase: esta prueba llama a un método no virtual vacío contenido en una clase.
- Llamada virtual de clase: esta prueba llama a un método virtual vacío contenido en una clase.
- Llamada a ValueType Virtual (método Obj): esta prueba llama a ToString() (un método virtual) en un ValueType, que recurre al método de objeto predeterminado.
- Llamada a ValueType Virtual (método Obj invalidado): esta prueba llama a ToString() (un método virtual) en un ValueType que ha invalidado el valor predeterminado.
- Load Type by Newing (Static): esta prueba asigna espacio para una clase con solo métodos estáticos.
- Tipo de carga mediante newing (métodos virtuales): esta prueba asigna espacio para una clase con métodos virtuales.
Una conclusión que puede dibujar es que las llamadas a función virtual son aproximadamente dos veces tan costosas como llamadas normales cuando se llama a un método en una clase. Tenga en cuenta que las llamadas son baratas para comenzar, por lo que no eliminaría todas las llamadas virtuales. Siempre debe usar métodos virtuales cuando tenga sentido hacerlo.
- JiT no puede insertar métodos virtuales, por lo que pierde una posible optimización si se elimina de métodos no virtuales.
- Asignar espacio para un objeto que tiene métodos virtuales es ligeramente más lento que la asignación de un objeto sin ellos, ya que se debe realizar un trabajo adicional para encontrar espacio para las tablas virtuales.
Observe que llamar a un método no virtual dentro de un ValueType es más de tres veces tan rápido como en una clase, pero una vez que se trata como una clase , pierde terriblemente. Esta es una característica de ValueTypes: tratarlas como estructuras y son de iluminación rápida. Tratarlas como clases y son dolorosamente lentas. ToString() es un método virtual, por lo que antes de que se pueda llamar, la estructura debe convertirse en un objeto del montón. En lugar de ser dos veces más lento, llamar a un método virtual en un ValueType ahora es ocho veces tan lento! ¿La moral de la historia? No trate ValueTypes como clases.
Si tiene preguntas o comentarios sobre este artículo, póngase en contacto con el administrador de programas de .NET Framework para problemas de rendimiento de .NET Framework.