Sugerencias para mejorar código en el que la velocidad de ejecución es importante

Para programar código rápido es necesario comprender todos los aspectos de la aplicación y su interacción con el sistema. En este tema se sugieren alternativas para algunas de las técnicas de programación más obvias que le ayudarán a conseguir que las partes del código en las que es vital una ejecución rápida se ejecuten de forma satisfactoria.

En resumen, para mejorar las partes del código en las que es importante la velocidad de ejecución, debe:

  • Saber cuáles son las partes del código que tienen que ejecutarse rápidamente.

  • Conocer el tamaño y la velocidad de ejecución del código.

  • Conocer el precio de implementar nuevas características.

  • Conocer el mínimo trabajo necesario para realizar la tarea.

Para recopilar información acerca del rendimiento del código, puede utilizar el monitor de rendimiento (perfmon.exe).

  • Aciertos de caché y errores de página

  • Ordenar y buscar

  • MFC y bibliotecas de clases

  • Bibliotecas compartidas

  • Montones

  • Subprocesos

  • Espacio de trabajo pequeño

Errores de caché y errores de página

Los errores de caché en la caché interna y en la caché externa, así como los errores de página (al buscar un almacenamiento secundario de instrucciones y datos de programa) ralentizan la ejecución de un programa.

Un acierto de caché de CPU puede costar al programa entre 10 y 20 ciclos de reloj. Un acierto de caché externa puede costar entre 20 y 40 ciclos de reloj. Un error de página puede costar un millón de ciclos de reloj (suponiendo que se utiliza un procesador que puede procesar 500 millones de instrucciones por segundo y que un error de página supone 2 milisegundos). Por tanto, para agilizar la ejecución del programa es importante escribir código que reduzca el número de errores de caché y de errores de página.

Una de las razones de que un programa sea lento es que produzca errores de página o de caché innecesarios. Para evitar esto, es importante utilizar estructuras de datos con un grado de emplazamiento de referencia bueno, lo que significa mantener juntas las cosas relacionadas. A veces, una estructura de datos con buena apariencia resulta ser defectuosa a causa de un grado de emplazamiento de referencia bajo y a veces es verdad lo contrario. A continuación se describen dos ejemplos de esto:

  • Las listas vinculadas asignadas dinámicamente pueden reducir el rendimiento de un programa porque al buscar un elemento o al recorrer la lista hasta el final, cada vínculo omitido podría producir un error de página o de caché. Una implementación de la lista basada en matrices sencillas podría ser en realidad mucho más rápida, ya que se producirían menos errores de caché y de página; teniendo en cuenta que es más difícil aumentar el número de elementos de la matriz, esta implementación puede ser aún más rápida.

  • Las tablas hash que utilizan listas vinculadas asignadas dinámicamente pueden degradar el rendimiento. Por extensión, las tablas hash que utilizan listas vinculadas asignadas dinámicamente para almacenar el contenido podrían tener un rendimiento bastante peor. De hecho, en el análisis final, una simple búsqueda lineal en una matriz podría ser más rápida (en función de las circunstancias). Las tablas hash basadas en matrices (denominadas "de hashing cerrado") son una implementación que no se suele utilizar y que en general ofrece un rendimiento superior.

Ordenar y buscar

El proceso de ordenar es, por su esencia, una operación que consume mucho tiempo en comparación con muchas operaciones típicas. La mejor forma de eludir una ralentización innecesaria es evitar ordenar cuando una ejecución rápida es vital. Puede:

  • Aplazar el proceso de ordenar hasta que la ejecución alcance un punto en que la velocidad no sea fundamental.

  • Ordenar los datos en un momento anterior, en el que la velocidad de ejecución no es fundamental.

  • Ordenar únicamente la parte de los datos que hay que ordenar realmente.

A veces, puede generar la lista de forma ordenada. Tenga precaución al hacerlo ya que, si tiene que insertar datos de forma ordenada, puede que tenga que utilizar una estructura de datos con un grado de emplazamiento de referencia bajo, que provoca errores de página y de caché. No existe un enfoque que funcione en todos los casos. Pruebe varios enfoques diferentes y evalúe las diferencias.

A continuación se muestran algunas sugerencias generales para ordenar:

  • Utilice una ordenación de tipo stock para minimizar el número de errores.

  • Todo el trabajo preliminar que haga para reducir la complejidad de una operación de ordenación merecerá la pena. Si un barrido de los datos simplifica las comparaciones y reduce la complejidad de la ordenación de O(n log n) a O(n), es casi seguro que ganará tiempo*.*

  • Piense en el grado de emplazamiento de referencia del algoritmo de ordenación y los datos a los que va a aplicarlo.

Hay menos alternativas para buscar que para ordenar. Si es importante que la búsqueda sea rápida, una búsqueda binaria o una búsqueda en tabla hash es casi siempre lo mejor pero, como al ordenar, debe pensar en el grado de emplazamiento. Una búsqueda lineal en una matriz pequeña puede ser más rápida que una búsqueda binaria en una estructura de datos con muchos punteros que producen errores de página o de caché.

MFC y bibliotecas de clases

El uso de Microsoft Foundation Classes (MFC) puede simplificar en gran medida la escritura de código. Al escribir código en el que es importante la velocidad de ejecución, debe tener en cuenta la sobrecarga inherente a algunas de las clases. Examine el código MFC que utilizará el código para el que es importante la velocidad de ejecución a fin de ver si satisface los requisitos de rendimiento. En la lista siguiente se identifican funciones y clases MFC que debe conocer:

  • CString   MFC llama a la biblioteca en tiempo de ejecución de C para asignar memoria dinámicamente a un objeto CString. En general, CString es tan eficaz como cualquier otra cadena asignada dinámicamente. Como para cualquier cadena asignada dinámicamente, tiene la sobrecarga de asignación y liberación dinámica. A menudo, una simple matriz char de la pila puede ofrecer el mismo resultado de forma más rápida. No utilice un objeto CString para almacenar una constante de cadena. Utilice const char * en su lugar. Cualquier operación que realice con un objeto CString implica una cierta sobrecarga. El uso de las funciones de cadena de la biblioteca en tiempo de ejecución puede ofrecer mayor velocidad.

  • CArray   Un objeto CArray proporciona la flexibilidad que no ofrecen las matrices normales, pero es posible que su programa no lo necesite. Si conoce los límites específicos de la matriz, puede utilizar una matriz fija global. Si utiliza CArray, debe utilizar CArray::SetSize para establecer su tamaño y especificar el número de elementos en que se debe aumentar cuando sea necesario realizar una reasignación. Si no lo hace, cuando agregue elementos podría pasar que la matriz se reasignara y copiara con frecuencia, lo que resulta ineficaz y puede fragmentar la memoria. Tenga en cuenta también que si inserta un elemento en una matriz, CArray mueve los siguientes elementos de la memoria y puede necesitar aumentar el número de elementos de la matriz. Estas acciones pueden provocar errores de caché y de página. Si analiza el código que utiliza MFC, puede ver que es posible escribir código más específico de su escenario para mejorar el rendimiento. Como CArray es una plantilla, por ejemplo, puede proporcionar versiones de CArray adaptadas a tipos específicos.

  • CList   CList es una lista de vínculo doble, por lo que la inserción de elementos es rápida al principio y al final de la lista, y en una posición conocida (POSITION) de la lista. Sin embargo, la localización de un elemento por valor o por índice requiere una búsqueda secuencial que puede resultar lenta si la lista es larga. Si el código no requiere una lista doblemente vinculada, es aconsejable reconsiderar el uso de CList. Si utiliza una lista de vínculo simple, se ahorra tanto la sobrecarga de actualizar un puntero adicional para todas las operaciones como la memoria requerida por el puntero. La memoria adicional no es mucha, pero supone otra oportunidad para que se produzcan errores de caché o de página.

  • IsKindOf   Esta función puede generar muchas llamadas y tener acceso a mucha memoria en distintas áreas de datos, lo que provoca un grado de emplazamiento de referencia bajo. Es útil al generar una versión de depuración (en una llamada ASSERT, por ejemplo), pero debe evitar utilizarla en una versión de lanzamiento.

  • PreTranslateMessage   Use PreTranslateMessage cuando un árbol de ventanas concreto necesite distintos aceleradores de teclado o cuando tenga que insertar control de mensajes en el suministro de mensajes. PreTranslateMessage modifica mensajes de envío MFC. Si decide reemplazar PreTranslateMessage, hágalo sólo en el nivel necesario. Por ejemplo, no es necesario reemplazar CMainFrame::PreTranslateMessage si sólo tiene interés en mensajes destinados a objetos secundarios de una vista determinada. En su lugar, reemplace PreTranslateMessage para la clase de la vista.

    No evite la ruta de envío normal mediante PreTranslateMessage para controlar los mensajes enviados a cualquier ventana. Utilice procedimientos de ventana y mapas de mensajes MFC con ese fin.

  • OnIdle   Los eventos de inactividad pueden suceder de forma inesperada en determinados momentos (entre eventos WM_KEYDOWN y WM_KEYUP , por ejemplo). Los temporizadores pueden ser una forma más eficaz de desencadenar el código. No fuerce la llamada repetida a OnIdle generando mensajes falsos o devolviendo siempre TRUE desde un reemplazo de OnIdle, que nunca permitirá que el subproceso esté en suspensión. En este caso también, un temporizador o un subproceso independiente puede ser más apropiado.

Bibliotecas compartidas

Es deseable poder reutilizar código. Sin embargo, si va utilizar el código de otro programador, debe asegurarse de que conoce exactamente lo que hace este código en los casos en los que el rendimiento sea vital. La mejor manera de comprender esto es recorrer el código fuente o realizar medidas con herramientas como PView o el Monitor de rendimiento.

Montones

Cuando utilice varios montones, debe hacerlo con prudencia. Los montones adicionales creados con HeapCreate y HeapAlloc permiten administrar un conjunto relacionado de asignaciones y después eliminarlo. No asigne demasiada memoria. Si utiliza varios montones, debe tener especial cuidado con la cantidad de memoria asignada inicialmente.

En lugar de utilizar varios montones, se pueden utilizar las funciones auxiliares como interfaz con el código y el montón predeterminado. Las funciones auxiliares facilitan las estrategias de asignación personalizadas que permiten mejorar el rendimiento de la aplicación. Por ejemplo, si realiza con frecuencia asignaciones pequeñas, es posible que le interese ubicar estas asignaciones en una parte del montón predeterminado. Puede asignar un bloque de memoria grande y después utilizar una función auxiliar para realizar una subasignación desde ese bloque. Si lo hace, no tendrá montones adicionales con memoria no utilizada, ya que la asignación se realiza desde el montón predeterminado.

Sin embargo, en algunos casos, el uso del montón predeterminado puede reducir el grado de emplazamiento de referencia. Utilice Process Viewer, Spy++ o el Monitor de rendimiento para medir los efectos derivados de mover objetos entre montones.

Debe medir los montones para conocer cada asignación del montón. Utilice las rutinas de montón para depuración de la biblioteca en tiempo de ejecución de C para establecer puntos de comprobación y para realizar un volcado del montón. Puede leer el resultado en un programa de hoja de cálculo, como Microsoft Excel, y utilizar tablas dinámicas para ver los resultados. Anote el número total, el tamaño y la distribución de las asignaciones. Compare estos datos con el tamaño de los conjuntos de trabajo. Analice también la agrupación de tamaños relacionados.

También puede utilizar los contadores de rendimiento para supervisar el uso de la memoria.

Subprocesos

Para las tareas de segundo plano, un control eficaz de inactividad de eventos puede ofrecer mayor velocidad que el uso de subprocesos. Es más fácil comprender el grado de emplazamiento de referencia en un programa de un solo subproceso.

Una buena manera empírica es utilizar un subproceso sólo cuando se bloquee una notificación del sistema operativo que esté en la raíz del trabajo de segundo plano. Los subprocesos son la mejor solución en un caso así, ya que no es práctico bloquear un subproceso principal en un evento.

Los subprocesos también presentan problemas de comunicación. Debe administrar el vínculo de comunicación existente entre los subprocesos con una lista de mensajes o mediante asignación y uso de memoria compartida. La administración del vínculo de comunicación suele requerir sincronización para evitar condiciones de carrera y problemas de interbloqueo. Esta complejidad puede producir como efecto errores y problemas de rendimiento.

Para obtener información adicional, vea Procesamiento de bucles inactivos y Multithreading.

Espacio de trabajo pequeño

Los conjuntos de trabajo de menor tamaño significan mejor grado de emplazamiento de referencia, menos errores de página y más aciertos de caché. El espacio de trabajo de proceso es la métrica más cercana que proporciona el sistema operativo para medir el grado de emplazamiento de referencia.

  • Para establecer los límites superior e inferior del espacio de trabajo, utilice SetProcessWorkingSetSize.

  • Para obtener los límites superior e inferior del espacio de trabajo, utilice GetProcessWorkingSetSize.

  • Para ver el tamaño del espacio de trabajo, utilice Spy++.

Vea también

Referencia

Optimizar el código