Share via


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

Para escribir código rápidamente, es necesario comprender todos los aspectos de la aplicación y cómo interactúa con el sistema. En este artículo se sugieren alternativas a algunas de las técnicas de codificación más obvias para ayudarle a asegurarse de que las partes críticas del código funcionan satisfactoriamente.

A modo de resumen, para mejorar el código crítico en el tiempo, tiene que:

  • Saber qué partes del programa deben ser rápidas.

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

  • Conocer el coste de las nuevas características.

  • Saber cuál es el trabajo mínimo necesario para llevar a cabo el trabajo.

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

Secciones de este artículo

Errores de caché y errores de página

Los errores de aciertos de caché, tanto en la memoria caché interna como en la externa, y los errores de página (ir al almacenamiento secundario para obtener datos e instrucciones del programa) ralentizan el rendimiento de los programas.

Cada acierto de la caché de la CPU puede costarle a su programa entre 10 y 20 ciclos de reloj. Cada acierto de la caché externa puede costarle entre 20 y 40 ciclos de reloj. Un error de página puede costar un millón de ciclos de reloj (suponiendo que un procesador controle 500 millones de instrucciones/segundo y un tiempo de 2 milisegundos para un error de página). Por lo tanto, lo que más beneficiará a la ejecución del programa será escribir código que reduzca el número de errores de aciertos de caché y errores de página.

Uno de los motivos por los que algunos programas son lentos es que tienen más errores de página o errores de caché de los necesarios. Para evitar este problema, es importante usar estructuras de datos con buena localidad de referencia, lo que significa mantener las cosas relacionadas juntas. A veces, las estructuras de datos que parecen buenas resultan ser horribles debido a una mala localidad de referencia. Otras veces sucede lo contrario. Estos son dos ejemplos:

  • Las listas vinculadas asignadas dinámicamente pueden reducir el rendimiento del programa. Al buscar un elemento o al recorrer una lista al final, cada vínculo omitido podría perder la memoria caché o provocar un error de página. Una implementación de lista basada en matrices simples podría ser más rápida debido a un mejor almacenamiento en caché y menos errores de página. Incluso si se permite el hecho de que la matriz sería más difícil de crecer, podría ser más rápida.

  • Las tablas hash que utilizan listas vinculadas asignadas dinámicamente pueden reducir el rendimiento. Por extensión, las tablas hash que usan listas vinculadas asignadas dinámicamente para almacenar su contenido pueden tener un rendimiento considerablemente peor. De hecho, en el análisis final, una simple búsqueda lineal a través de una matriz podría ser más rápida (según las circunstancias). El uso de una tabla hash basada en matrices (denominado "hash cerrado") es una implementación que suele pasar por alto y que suele tener un rendimiento superior.

Ordenación y búsqueda

La ordenación, por naturaleza, requiere bastante tiempo si se compara con muchas operaciones típicas. La mejor forma de evitar ralentizaciones innecesarias es evitar la ordenación en los momentos críticos. Es posible que pueda:

  • Aplazar la ordenación hasta un momento en el que el rendimiento no sea crítico.

  • Ordenar los datos en un momento anterior en el que el rendimiento no sea crítico.

  • Ordenar solo la parte de los datos que realmente sea necesario ordenar.

A veces, se puede generar la lista ya ordenada. Pero cuidado porque, si necesita insertar datos ya ordenados, puede que requiera una estructura de datos más complicada con una localidad de referencia deficiente, y esto provocaría errores de caché y errores de página. No hay ningún enfoque que funcione en todos los casos. Pruebe con varios métodos y mida las diferencias.

Estas son algunas sugerencias generales para la ordenación:

  • Para minimizar los errores, utilice un orden de existencias.

  • Todo el trabajo que pueda realizar de antemano con el fin de reducir la complejidad de la ordenación valdrá la pena. Si un paso de un solo uso de los datos simplifica las comparaciones y reduce la ordenación de O(n log n) a O(n), casi seguramente saldrá adelante.

  • Piense en la localidad de referencia del algoritmo de ordenación y en los datos sobre los que espera que se ejecute.

Para las búsquedas hay menos alternativas que para la ordenación. Si la búsqueda es crítica en el tiempo, siempre será mejor usar una búsqueda binaria o una búsqueda en una tabla hash. Sin embargo, como con la ordenación, debe tener en cuenta la localidad. Una búsqueda lineal a través de una matriz pequeña puede ser más rápida que una búsqueda binaria a través de una estructura de datos con muchos punteros que provocan errores de página o errores de caché.

Bibliotecas de clases y MFC

Las Microsoft Foundation Classes (MFC) pueden simplificar en gran medida la escritura de código. Al escribir código crítico en el tiempo, debe tener presente la sobrecarga inherente a algunas de las clases. Examine el código MFC empleado por el código crítico en el tiempo para ver si ofrece el rendimiento que necesita. En la siguiente lista, se identifican las funciones y las clases MFC que debe conocer:

  • CString MFC llama a la biblioteca en tiempo de ejecución de C para asignar memoria de forma CString dinámica. Por lo general, CString es tan eficaz como cualquier otra cadena asignada dinámicamente. Como cualquier cadena asignada dinámicamente, tiene la sobrecarga de la asignación y liberación dinámicas. A menudo, una simple matriz de char en la pila puede cumplir el mismo fin más rápidamente. No use una CString para almacenar una cadena de constante. En su lugar, use const char *. Todas las operaciones que realice con objetos CString tendrán cierta sobrecarga. Puede que sea más rápido usar las funciones de cadena de la biblioteca en tiempo de ejecución.

  • CArray Proporciona CArray flexibilidad que no es una matriz normal, pero es posible que el programa no lo necesite. Si conoce los límites específicos de la matriz, puede utilizar en su lugar una matriz fija global. Si utiliza CArray, use CArray::SetSize para establecer su tamaño y especificar el número de elementos que aumentará cuando sea necesaria una reasignación. Si no lo hace, al agregar elementos, es posible que la matriz se reasigne y se copie con frecuencia: esto es ineficiente y puede fragmentar la memoria. Además, si inserta un elemento en una matriz, CArray mueve los elementos posteriores en la memoria y puede que tenga que aumentar la matriz. Estas acciones pueden provocar errores de caché y errores de página. Si revisa el código utilizado por MFC, tal vez observe que puede escribir algo más específico para su escenario y, así, mejorar el rendimiento. Dado que CArray es una plantilla, podría, por ejemplo, proporcionar especializaciones de CArray para determinados tipos.

  • CListCList es una lista vinculada doble, por lo que la inserción de elementos es rápida en la cabeza, cola y en una posición conocida (POSITION) de la lista. Sin embargo, para buscar elementos por valor o por índice, es necesaria una búsqueda secuencial, que puede resultar lenta si la lista es larga. Si el código no requiere una lista vinculada doble, puede que quiera reconsiderar el uso de CList. El uso de una lista vinculada singly guarda la sobrecarga de actualizar otro puntero para todas las operaciones y la memoria de ese puntero. La memoria adicional no es grande, pero es otra oportunidad para errores de caché o errores de página.

  • IsKindOf Esta función puede generar muchas llamadas y puede acceder a la memoria en diferentes áreas de datos, lo que conduce a una localidad incorrecta de referencia. Es útil para una compilación de depuración (en una llamada ASSERT, por ejemplo), pero intente evitar su uso en una compilación de versión.

  • PreTranslateMessage Use PreTranslateMessage cuando un determinado árbol de ventanas necesite aceleradores del teclado distintos o cuando tenga que insertar la administración de mensajes en el suministro de mensajes. PreTranslateMessage modifica los mensajes de distribución de MFC. Si invalida PreTranslateMessage, hágalo en el nivel que sea necesario. Por ejemplo, no es necesario invalidar CMainFrame::PreTranslateMessage si solo está interesado en los mensajes que van a elementos secundarios de una vista determinada. En lugar de ello, invalide PreTranslateMessage en la clase de vista.

    No evite la ruta de acceso de envío normal mediante PreTranslateMessage para controlar cualquier mensaje enviado a ninguna ventana. Para ello, use procedimientos de ventana y mapas de mensajes de MFC.

  • OnIdle Los eventos inactivos pueden producirse en ocasiones que no se esperan, como entre WM_KEYDOWN eventos y WM_KEYUP . Los temporizadores pueden ser una forma más eficiente de desencadenar el código. No obligue OnIdle a llamarse repetidamente mediante la generación de mensajes falsos o siempre devolviendo TRUE desde una invalidación de OnIdle, que nunca permitiría que el subproceso se suspenda. También en este caso, es posible que sea más adecuado usar un temporizador o un subproceso independiente.

Bibliotecas compartidas

Es recomendable reutilizar código. Sin embargo, si va a usar el código de otra persona, debe asegurarse de que sabe exactamente lo que hace en esos casos en los que el rendimiento es fundamental para usted. La mejor manera de entenderlo es recorrer el código fuente o medir con herramientas como PView o Monitor de rendimiento.

Montones

Si usa varios montones, hágalo con moderación. Los montones adicionales que se crean con HeapCreate y HeapAlloc le permiten administrar y, luego, desechar un conjunto relacionado de asignaciones. No asigne demasiada memoria. Si va a utilizar varios montones, preste especial atención a la cantidad de memoria que se asignó inicialmente.

En lugar de usar varios montones, puede emplear funciones del asistente que hagan de interfaz entre el código y el montón predeterminado. Las funciones del asistente facilitan estrategias de asignación personalizadas que pueden mejorar el rendimiento de la aplicación. Por ejemplo, si realiza asignaciones pequeñas a menudo, puede que le convenga localizar las asignaciones en una parte del montón predeterminado. Puede asignar un gran bloque de memoria y, después, usar una función del asistente para subasignar memoria de ese bloque. A continuación, no tendrá varios montones con memoria sin usar, ya que la asignación sale del montón predeterminado.

Sin embargo, en ciertos casos, usar el montón predeterminado puede reducir la localidad de referencia. Utilice el Visor de procesos, Spy++ o el monitor de rendimiento para medir los efectos del movimiento de objetos de un montón a otro.

Mida los montones para comprender cada asignación del montón. Use las rutinas del montón de depuración en tiempo de ejecución de C para establecer puntos de control en el montón y volcarlo. Puede leer la salida en un programa de hojas de cálculo como Microsoft Excel y usar tablas dinámicas para ver los resultados. Observe el número total de asignaciones, su tamaño y su distribución. Compare estos resultados con el tamaño de los conjuntos de trabajo. Mire también la agrupación en clústeres de los objetos de tamaño relacionado.

Además, puede usar los contadores de rendimiento para supervisar el uso de memoria.

Subprocesos

Para las tareas en segundo plano, si los eventos se administran de manera inactiva y eficiente, se puede lograr más rapidez que con subprocesos. La localidad de referencia es más fácil de entender en los programas con un solo subproceso.

Por regla general, solo se usa un subproceso si una notificación del sistema operativo sobre la que se bloquea está en la raíz del trabajo en segundo plano. Los subprocesos son la mejor solución en tal caso porque 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 asignando y usando memoria compartida. Para administrar el vínculo de comunicación, suele ser necesaria la sincronización, con el fin de evitar problemas de interbloqueo y condiciones de carrera. Esta complejidad puede convertirse fácilmente en errores y problemas de rendimiento.

Para obtener más información, consulte los temas sobre procesamiento de bucles inactivos y multithreading.

Espacio de trabajo pequeño

Con los espacios de trabajo menores, se obtienen una localidad de referencia mejor, menos errores de página y más aciertos de caché. El espacio de trabajo del proceso es la métrica más precisa que ofrece directamente el sistema operativo para medir la localidad de referencia.

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

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

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

Consulte también

Optimizar el código