¿Cómo documentar y enseñar a otros código "optimizado más allá del reconocimiento" computacionalmente intensivo?

11

Ocasionalmente, existe el 1% del código que es computacionalmente lo suficientemente intensivo y necesita el tipo más pesado de optimización de bajo nivel. Ejemplos son el procesamiento de video, el procesamiento de imágenes y todo tipo de procesamiento de señales, en general.

Los objetivos son documentar y enseñar las técnicas de optimización, para que el código no sea imposible de mantener y propenso a su eliminación por parte de los desarrolladores más nuevos. (*)

(*) No obstante la posibilidad de que la optimización particular sea completamente inútil en algunas CPU futuras imprevisibles, de modo que el código se eliminará de todos modos.

Teniendo en cuenta que las ofertas de software (comercial o de código abierto) conservan su ventaja competitiva al tener el código más rápido y hacer uso de la arquitectura de CPU más nueva, los escritores de software a menudo necesitan ajustar su código para que se ejecute más rápido y obtener la misma salida para un determinado tarea, lista blanca que tolera una pequeña cantidad de errores de redondeo.

Por lo general, un escritor de software puede mantener muchas versiones de una función como documentación de cada reescritura de algoritmo / optimización que tiene lugar. ¿Cómo se hacen disponibles estas versiones para que otros estudien sus técnicas de optimización?

Relacionado:

rwong
fuente
1
Podrías mantener las diferentes versiones en el código, comentadas, con muchos comentarios diciéndole al lector lo que está sucediendo.
Mike Dunlavey
1
Y no solo les diga qué está haciendo el código, sino por qué es más rápido de esa manera. Incluya enlaces a algoritmos si es necesario, ya sea el suyo, wiki, documentos o recursos disponibles en Internet (solo tenga en cuenta la rotura de enlaces en ese caso, puede ser conveniente copiarlo en su propio sistema de documentos con un enlace al original .)
Marjan Venema
1
@MikeDunlavey: Ay, por favor no lo comentes . Solo tiene varias implementaciones de la misma función y llame a la que sea más rápida. De esa manera, puede cambiar fácilmente a una versión diferente del código y compararlos a todos.
sleske
2
@sleske A veces, simplemente tener más código binario puede ralentizarlo.
quant_dev
@quant_dev: Sí, eso puede suceder. Simplemente creo que es importante que el código se construya y ejecute (idealmente) regularmente, para mantenerlo actualizado. Tal vez compilarlo solo en modo de depuración.
sleske

Respuestas:

10

Respuesta corta

Mantenga las optimizaciones locales, hágalas obvias, documentelas bien y facilite la comparación de las versiones optimizadas entre sí y con la versión no optimizada, tanto en términos de código fuente como de rendimiento en tiempo de ejecución.

Respuesta completa

Si tales optimizaciones son realmente importantes para su producto, entonces necesita saber no solo por qué las optimizaciones fueron útiles antes, sino también proporcionar suficiente información para ayudar a los desarrolladores a saber si serán útiles en el futuro.

Idealmente, debe incorporar las pruebas de rendimiento en su proceso de compilación, para saber cuándo las nuevas tecnologías invalidan las optimizaciones antiguas.

Recuerda:

La primera regla de optimización del programa: no lo hagas.

La segunda regla de optimización de programas (¡solo para expertos!): No lo hagas todavía ".

- Michael A. Jackson

Para saber si ahora es el momento, se requiere evaluación comparativa y pruebas.

Como mencionó, el mayor problema con el código altamente optimizado es que es difícil de mantener, por lo que, en la medida de lo posible, debe mantener las porciones optimizadas separadas de las porciones no optimizadas. Ya sea que haga esto mediante la vinculación en tiempo de compilación, las llamadas a funciones virtuales en tiempo de ejecución o algo intermedio no debería importar. Lo que debería importar es que cuando ejecuta sus pruebas, desea poder probar todas las versiones en las que está interesado actualmente.

Me inclinaría a construir un sistema de tal manera que la versión básica no optimizada del código de producción siempre pudiera usarse para comprender la intención del código, luego construir diferentes módulos optimizados junto con este que contenga la versión o versiones optimizadas, documentando explícitamente donde sea La versión optimizada difiere de la línea base. Cuando ejecuta sus pruebas (unidad e integración), lo ejecuta en la versión no optimizada y en todos los módulos optimizados actuales.

Ejemplo

Por ejemplo, supongamos que tiene una función de Transformación rápida de Fourier . Quizás tengas una implementación algorítmica básica fft.cy pruebas en fft_tests.c.

Luego viene el Pentium y usted decide implementar la versión de punto fijo al fft_mmx.cusar las instrucciones MMX . Más tarde, el Pentium 3 llega y decide añadir una versión que utiliza las extensiones Streaming SIMD en fft_sse.c.

Ahora desea agregar CUDA , así que agrega fft_cuda.c, pero descubra que con el conjunto de datos de prueba que ha estado utilizando durante años, ¡la versión CUDA es más lenta que la versión SSE! Hace un análisis y termina agregando un conjunto de datos que es 100 veces más grande y obtiene la aceleración que espera, pero ahora sabe que el tiempo de configuración para usar la versión CUDA es significativo y que con conjuntos de datos pequeños debe usar un algoritmo sin ese costo de instalación.

En cada uno de estos casos, está implementando el mismo algoritmo, todos deberían comportarse de la misma manera, pero funcionarán con diferentes eficiencias y velocidades en diferentes arquitecturas (si es que funcionan). Sin embargo, desde el punto de vista del código, puede comparar cualquier par de archivos de origen para averiguar por qué la misma interfaz se implementa de diferentes maneras y, por lo general, la forma más fácil será volver a consultar la versión original no optimizada.

Lo mismo ocurre con una implementación OOP donde una clase base que implementa el algoritmo no optimizado y las clases derivadas implementan diferentes optimizaciones.

Lo importante es mantener las mismas cosas que son iguales , para que las diferencias sean obvias .

Mark Booth
fuente
7

Específicamente, ya que ha tomado el ejemplo del procesamiento de video e imagen, uno puede mantener el código como parte de la misma versión pero activo o inactivo según el contexto.

Si bien no lo ha mencionado, supongo que Caquí.

La forma más simple en el Ccódigo, uno hace la optimización (y también se aplica cuando se trata de hacer que las cosas sean portátiles) es mantener

 
#ifdef OPTIMIZATION_XYZ_ENABLE 
   // your optimzied code here... 
#else  
   // your basic code here...

Cuando habilita #define OPTIMIZATION_XYZ_ENABLEdurante la compilación en Makefile, todo funciona en consecuencia.

Por lo general, cortar algunas líneas de código en el medio de las funciones podría volverse desordenado cuando hay demasiadas funciones optimizadas. Por lo tanto, en este caso, se definen diferentes punteros de función para realizar una función específica.

el código principal siempre se ejecuta a través de un puntero de función como


   codec->computed_idct(blocks); 

Pero los punteros de función se definen según el tipo de ejemplo (por ejemplo, aquí la función idct está optimizada para diferentes arquitecturas de CPU.



if(OPTIMIZE_X86) {
  codec->computed_idct = compute_idct_x86; 
}
else if(OPTIMZE_ARM) {
  codec->computed_idct = compute_idct_ARM;
}
else {
  codec->computed_idct = compute_idct_C; 
}

debería ver el código libjpeg y el código libmpeg2 y puede ser ffmpeg para tales técnicas.

Dipan Mehta
fuente
6

Como investigador, termino escribiendo un poco del código de "cuello de botella". Sin embargo, una vez que se pone en producción, la responsabilidad de integrarlo en el producto y proporcionar soporte posterior recae en los desarrolladores. Como puede imaginar, comunicar con claridad qué y cómo se supone que debe operar el programa es de suma importancia.

He descubierto que hay tres ingredientes esenciales para completar este paso con éxito

  1. El algoritmo utilizado debe ser absolutamente claro.
  2. El propósito de cada línea de implementación debe ser claro.
  3. Las desviaciones de los resultados esperados deben identificarse lo antes posible.

Para el primer paso, siempre escribo un documento técnico corto que documenta el algoritmo. El objetivo aquí es escribirlo para que otra persona pueda implementarlo desde cero usando solo el documento técnico. Si es un algoritmo conocido y publicado, es suficiente para dar las referencias y repetir las ecuaciones clave. Si se trata de un trabajo original, deberá ser un poco más explícito. Esto le dirá lo que se supone que debe hacer el código .

La implementación real que se entrega al desarrollo debe documentarse de tal manera que todas las sutilezas se hagan explícitas. Si adquiere bloqueos en un orden particular para evitar un punto muerto, agregue un comentario. Si itera sobre las columnas en lugar de sobre las filas de una matriz debido a problemas de coherencia de caché, agregue un comentario. Si haces algo incluso un poco inteligente, coméntalo. Si puede garantizar el documento técnico y el código nunca se separará (a través de VCS o un sistema similar), puede consultar el documento técnico. El resultado puede ser fácilmente superior al 50% de comentarios. Eso está bien. Esto le dirá por qué el código hace lo que hace.

Finalmente, debe poder garantizar la corrección ante los cambios. Afortunadamente, somos una herramienta útil en pruebas automatizadas y plataformas de integración continua . Estos le dirán qué está haciendo realmente el código .

Mi recomendación más cordial sería no escatimar en ninguno de los pasos. Los necesitarás más tarde;)

drxzcl
fuente
Gracias por su respuesta integral. Estoy de acuerdo con todos tus argumentos. En términos de pruebas automatizadas, encuentro que cubrir adecuadamente el rango numérico de la aritmética de punto fijo y el código SIMD es difícil, algo que me ha quemado dos veces. No siempre se cumplieron las condiciones previas que se indicaron solo en los comentarios (sin código para reforzar).
rwong
La razón por la que aún no he aceptado su respuesta es porque necesito más orientación sobre lo que significa "un documento técnico breve", y qué esfuerzo debe hacerse para producirlo. Para algunas industrias, esto es parte de la línea principal de negocios, pero en otras industrias se debe considerar el costo y se deben tomar atajos legalmente disponibles.
rwong
En primer lugar, siento su dolor con respecto a las pruebas automatizadas, la aritmética de coma flotante y el código paralelo. Me temo que no hay una solución válida para todos los casos. Por lo general, trabajo con tolerancias bastante liberales, pero en su industria eso podría no ser posible.
drxzcl
2
En la práctica, el documento técnico a menudo se parece al primer borrador de un documento científico, sin las partes "esponjosas" (sin introducción significativa, sin resumen, conclusiones / discusión mínimas y solo las referencias necesarias para comprenderlo). Veo cómo escribir el documento como un informe y una parte integral del desarrollo de algoritmos y / o la selección de algoritmos. Decidió implementar este algoritmo (por ejemplo, FFT espectral). ¿Qué es exactamente? ¿Por qué elegiste este sobre los demás? ¿Cuáles son sus características de paralelización? El esfuerzo debe ser proporcional al trabajo de selección / desarrollo.
drxzcl
5

Creo que esto se resuelve mejor a través de comentarios completos del código, hasta el punto en que cada bloque significativo de código tenga comentarios explicativos de antemano.

Los comentarios deben incluir citas de las especificaciones o material de referencia de hardware.

Utilice la terminología de la industria y los nombres de algoritmos cuando corresponda, por ejemplo, 'la arquitectura X genera trampas de CPU para lecturas no alineadas, por lo que este dispositivo de Duff se llena hasta el próximo límite de alineación'.

Usaría el nombre de variables en su cara para garantizar que no haya malentendidos de lo que está sucediendo. No es húngaro, pero cosas como 'zancadas' para describir la distancia en bytes entre dos píxeles verticales.

También complementaría esto con un documento corto, legible por humanos que tiene diagramas de alto nivel y diseño de bloques.

JBRWilkinson
fuente
1
Usar una terminología consistente para una sola cosa (por ejemplo, usar "zancada" sobre términos de significados similares, por ejemplo, "paso", "alineación") en el mismo proyecto ayudaría. Esto es algo difícil cuando se integra el código base de varios proyectos en un solo proyecto.
rwong