Pregunta:
El consenso de la industria del software es que el código limpio y simple es fundamental para la viabilidad a largo plazo de la base del código y la organización que lo posee. Estas propiedades conducen a menores costos de mantenimiento y a una mayor probabilidad de que se continúe la base del código.
Sin embargo, el código SIMD es diferente al código de aplicación general, y me gustaría saber si existe un consenso similar con respecto al código limpio y simple que se aplica específicamente al código SIMD.
Antecedentes de mi pregunta.
Escribo mucho código SIMD (instrucción única, datos múltiples) para varias tareas de procesamiento y análisis de imágenes. Recientemente también tuve que portar una pequeña cantidad de estas funciones de una arquitectura (SSE2) a otra (ARM NEON).
El código está escrito para software reducido, por lo tanto, no puede depender de lenguajes propietarios sin derechos de redistribución sin restricciones, como MATLAB.
Un ejemplo de estructura de código típica:
- Utilizando el tipo de matriz de OpenCV (
Mat
) para toda la memoria, el búfer y la gestión de la vida útil. - Después de verificar el tamaño (dimensiones) de los argumentos de entrada, se toman los punteros a la dirección inicial de cada fila de píxeles.
- El recuento de píxeles y las direcciones de inicio de cada fila de píxeles de cada matriz de entrada se pasan a algunas funciones de C ++ de bajo nivel.
- Estas funciones de C ++ de bajo nivel utilizan intrínsecos SIMD (para Intel Architecture y ARM NEON ), cargando y guardando en direcciones de puntero sin formato.
- Características de estas funciones de C ++ de bajo nivel:
- Exclusivamente unidimensional (consecutiva en memoria)
- No trata con asignaciones de memoria.
(Cada asignación, incluidos los temporales, son manejados por el código externo utilizando las instalaciones de OpenCV). - El rango de longitud de los nombres de los símbolos (intrínsecos, nombres de variables, etc.) son aproximadamente de 10 a 20 caracteres, lo cual es bastante excesivo.
(Se lee como techno-balbuceo). - Se desaconseja la reutilización de las variables SIMD porque los compiladores son bastante defectuosos al analizar correctamente el código que no está escrito en el estilo de codificación de "asignación única".
(He presentado varios informes de errores del compilador).
¿Qué aspectos de la programación SIMD causarían que la discusión difiera del caso general? O, ¿por qué es diferente SIMD?
En términos de costo de desarrollo inicial
- Es bien sabido que el costo de desarrollo inicial del código SIMD C ++ con buen rendimiento es de aproximadamente 10x - 100x (con un amplio margen) en comparación con el código C ++ escrito casualmente .
- Como se señaló en las respuestas a Elegir entre rendimiento vs código legible / más limpio? , la mayoría del código (incluido el código escrito casualmente y el código SIMD) inicialmente no es ni limpio ni rápido .
- Se desaconsejan las mejoras evolutivas en el rendimiento del código (tanto en el código escalar como en el SIMD) (porque se considera una especie de retrabajo del software ), y no se realiza un seguimiento del costo y el beneficio.
En términos de propensión
(por ejemplo, el principio de Pareto, también conocido como la regla 80-20 )
- Incluso si el procesamiento de imágenes solo comprende el 20% de un sistema de software (tanto en tamaño de código como en funcionalidad), el procesamiento de imágenes es relativamente lento (cuando se ve como un porcentaje del tiempo de CPU invertido), lo que lleva más del 80% del tiempo.
- Esto se debe al efecto del tamaño de los datos: un tamaño de imagen típico se mide en megabytes, mientras que el tamaño típico de los datos que no son de imagen se mide en kilobytes.
- Dentro del código de procesamiento de imágenes, un programador SIMD está capacitado para reconocer automáticamente el código del 20% que comprende los puntos calientes mediante la identificación de la estructura de bucle en el código C ++. Por lo tanto, desde la perspectiva de un programador SIMD, el 100% del "código que importa" es un cuello de botella en el rendimiento.
- A menudo, en un sistema de procesamiento de imágenes, existen varios puntos de acceso y ocupan proporciones de tiempo comparables. Por ejemplo, puede haber 5 puntos calientes cada uno ocupando (20%, 18%, 16%, 14%, 12%) del tiempo total. Para lograr una ganancia de alto rendimiento, todos los puntos de acceso deben reescribirse en SIMD.
- Esto se resume como la regla de reventar globos: un globo no se puede reventar dos veces.
- Supongamos que hay algunos globos, digamos 5 de ellos. La única forma de diezmarlos es hacerlos estallar uno por uno.
- Una vez que aparece el primer globo, los 4 globos restantes ahora comprenden un mayor porcentaje del tiempo total de ejecución.
- Para obtener más ganancias, uno debe reventar otro globo.
(Esto es en desafío a la regla de optimización 80-20: un buen resultado económico puede lograrse después de que se ha recogido el 20% de las frutas más bajo colgantes.)
En términos de legibilidad y mantenimiento.
El código SIMD es patentemente difícil de leer.
- Esto es cierto incluso si se siguen todas las mejores prácticas de ingeniería de software, por ejemplo, nomenclatura, encapsulación, corrección constante (y hacer obvios los efectos secundarios), descomposición de funciones, etc.
- Esto es cierto incluso para programadores SIMD experimentados.
El código SIMD óptimo está muy contorsionado (ver observación) en comparación con su código prototipo C ++ equivalente.
- Hay muchas formas de contorsionar el código SIMD, pero solo 1 de cada 10 de estos intentos alcanzará resultados aceptablemente rápidos.
- (Es decir, en las melodías de ganancias de rendimiento 4x-10x para justificar el alto costo de desarrollo. Incluso se han observado ganancias más altas en la práctica).
(Observación)
Esta es la tesis principal del proyecto MIT Halide , citando el título del artículo al pie de la letra:
"algoritmos de desacoplamiento de programaciones para una fácil optimización de las canalizaciones de procesamiento de imágenes"
En términos de aplicabilidad futura
- El código SIMD está estrictamente vinculado a una sola arquitectura. Cada nueva arquitectura (o cada ampliación de los registros SIMD) requiere una reescritura.
- A diferencia de la mayoría del desarrollo de software, cada parte del código SIMD generalmente se escribe con un único propósito que nunca cambia.
(Con la excepción de portar a otras arquitecturas). - Algunas arquitecturas mantienen una compatibilidad hacia atrás perfecta (Intel); algunos se quedan cortos en una cantidad trivial (ARM AArch64, reemplazando
vtbl
convtblq
) pero que es suficiente para causar que algún código no se compile.
En cuanto a habilidades y entrenamiento
- No está claro qué requisitos previos de conocimiento se requieren para capacitar adecuadamente a un nuevo programador para escribir y mantener el código SIMD.
- Los graduados universitarios que han aprendido la programación SIMD en la escuela parecen despreciarla y descartarla como una carrera profesional poco práctica.
- La lectura de desmontaje y el perfil de rendimiento de bajo nivel se citan como dos habilidades fundamentales para escribir código SIMD de alto rendimiento. Sin embargo, no está claro cómo capacitar sistemáticamente a los programadores en estas dos habilidades.
- La arquitectura moderna de la CPU (que difiere significativamente de lo que se enseña en los libros de texto) hace que la capacitación sea aún más difícil.
En términos de corrección y costos relacionados con defectos
- Una sola función de procesamiento SIMD es en realidad lo suficientemente cohesiva como para establecer la corrección mediante:
- Aplicar métodos formales (con lápiz y papel) , y
- Verificación de rangos enteros de salida (con código prototipo y realizado fuera del tiempo de ejecución) .
- Sin embargo, el proceso de verificación es muy costoso (pasa el 100% del tiempo en la revisión del código y el 100% del tiempo en la verificación del modelo de prototipo), lo que triplica el costo de desarrollo ya costoso del código SIMD.
- Si un error de alguna manera logra pasar este proceso de verificación, es casi imposible "reparar" (arreglar) excepto reemplazar (reescribir) la función defectuosa sospechosa.
- El código SIMD adolece de defectos contundentes en el compilador de C ++ (generador de código de optimización).
- El código SIMD generado usando plantillas de expresión C ++ también sufre en gran medida por defectos del compilador.
En términos de innovaciones disruptivas
Se han propuesto muchas soluciones de la academia, pero pocas están viendo un uso comercial generalizado.
- Haluro de MIT
- Stanford Darkroom
- NT2 (Caja de herramientas de plantilla numérica) y el Boost.SIMD relacionado
Las bibliotecas con uso comercial generalizado no parecen estar muy habilitadas para SIMD.
- Las bibliotecas de código abierto parecen tibias para SIMD.
- Recientemente tengo esta observación de primera mano de esto después de perfilar una gran cantidad de funciones de API OpenCV, a partir de la versión 2.4.9.
- Muchas otras bibliotecas de procesamiento de imágenes que he perfilado tampoco hacen un uso intensivo de SIMD, o pierden los verdaderos puntos de acceso.
- Las bibliotecas comerciales parecen evitar SIMD por completo.
- En algunos casos, incluso he visto bibliotecas de procesamiento de imágenes que revierten el código optimizado SIMD en una versión anterior a código no SIMD en una versión posterior, lo que resulta en severas regresiones de rendimiento.
(La respuesta del proveedor es que era necesario evitar errores del compilador).
- En algunos casos, incluso he visto bibliotecas de procesamiento de imágenes que revierten el código optimizado SIMD en una versión anterior a código no SIMD en una versión posterior, lo que resulta en severas regresiones de rendimiento.
- Las bibliotecas de código abierto parecen tibias para SIMD.
La pregunta de este programador: ¿el código de baja latencia a veces tiene que ser "feo"? está relacionado, y anteriormente escribí una respuesta a esa pregunta para explicar mis puntos de vista hace unos años.
Sin embargo, esa respuesta es más o menos "apaciguamiento" al punto de vista de "optimización prematura", es decir, al punto de vista de que:
- Todas las optimizaciones son prematuras por definición (o, a corto plazo por naturaleza ), y
- La única optimización que tiene un beneficio a largo plazo es hacia la simplicidad.
Pero tales puntos de vista se disputan en este artículo de ACM .
Todo eso me lleva a preguntar: el
código SIMD es diferente al código de aplicación general, y me gustaría saber si existe un consenso similar en la industria con respecto al valor del código limpio y simple para el código SIMD.
Respuestas:
No escribí mucho código SIMD para mí, pero sí mucho código de ensamblador hace algunas décadas. AFAIK que usa intrínsecos SIMD es esencialmente una programación de ensamblador, y toda su pregunta podría reformularse simplemente reemplazando "SIMD" por la palabra "ensamblaje". Por ejemplo, los puntos que ya mencionaste, como
el código tarda de 10x a 100x en desarrollarse que el "código de alto nivel"
está vinculado a una arquitectura específica
el código nunca es "limpio" ni fácil de refactorizar
necesitas expertos para escribir y mantenerlo
depurar y mantener es difícil, evoluciona muy duro
de ninguna manera son "especiales" para SIMD: estos puntos son ciertos para cualquier tipo de lenguaje ensamblador, y todos son "consenso de la industria". Y la conclusión en la industria del software también es más o menos la misma que para el ensamblador:
no lo escriba si no es necesario: use un lenguaje de alto nivel siempre que sea posible y deje que los compiladores hagan el trabajo duro
Si los compiladores no son suficientes, encapsule al menos las partes de "bajo nivel" en algunas bibliotecas, pero evite distribuir el código por todo el programa.
Como es casi imposible escribir un ensamblador "autodocumentado" o un código SIMD, intente equilibrar esto con mucha documentación.
Por supuesto, existe una diferencia en la situación con el ensamblaje "clásico" o el código de máquina: hoy en día, los compiladores modernos suelen producir código de máquina de alta calidad a partir de un lenguaje de alto nivel, que a menudo se optimiza mejor que el código de ensamblador escrito manualmente. Para las arquitecturas SIMD que son populares hoy en día, la calidad de los compiladores disponibles es AFAIK muy por debajo de eso, y tal vez nunca llegue a eso, ya que la vectorización automática sigue siendo un tema de investigación científica. Ver, por ejemplo, este artículo que describe las diferencias en la optimización entre un compilador y un humano, dando la idea de que podría ser muy difícil crear buenos compiladores SIMD.
Como ya describió en su pregunta, también existe un problema de calidad con las bibliotecas actuales de última generación. Entonces, en mi humilde opinión, lo mejor que podemos esperar es que en los próximos años la calidad de los compiladores y las bibliotecas aumente, tal vez el hardware SIMD tenga que cambiar para ser más "compilador", tal vez lenguajes de programación especializados que admitan una vectorización más fácil (como Halide, que que mencionaste dos veces) se volverá más popular (¿no era ya una fortaleza de Fortran?). Según Wikipedia , SIMD se convirtió en "un producto en masa" hace unos 15 a 20 años (y Halide tiene menos de 3 años, cuando interpreto los documentos correctamente). Compare esto con los compiladores de tiempo para el lenguaje ensamblador "clásico" necesario para madurar. De acuerdo con este artículo de WikipediaPasaron casi 30 años (desde ~ 1970 hasta finales de la década de 1990) hasta que los compiladores excedieron el desempeño de los expertos humanos (en la producción de código máquina no paralelo). Por lo tanto, es posible que tengamos que esperar más de 10 a 15 años hasta que ocurra lo mismo con los compiladores habilitados para SIMD.
fuente
Mi organización se ha ocupado de este problema exacto. Nuestros productos están en el espacio de video, pero gran parte del código que escribimos es procesamiento de imágenes que también funcionaría para imágenes fijas.
"Resolvimos" (o tal vez "resolvimos") el problema escribiendo nuestro propio compilador. Esto no es tan loco como parece al principio. Tiene un conjunto restringido de entradas. Sabemos que todo el código está trabajando en imágenes, principalmente imágenes RGBA. Establecemos algunas restricciones, como que los búferes de entrada y salida nunca pueden superponerse, por lo que no hay alias de puntero. Ese tipo de cosas.
Luego escribimos nuestro código en OpenGL Shading Language (glsl). Se compila en código escalar, SSE, SSE2, SSE3, AVX, Neon y, por supuesto, glsl real. Cuando necesitamos soportar una nueva plataforma, actualizamos el compilador al código de salida para esa plataforma.
También hacemos mosaico de las imágenes para mejorar la coherencia de caché, y cosas así. Pero al mantener el procesamiento de imágenes en un kernel pequeño y al usar glsl (que ni siquiera admite punteros), reducimos en gran medida la complejidad de compilar el código.
Este enfoque no es para todos y tiene sus propios problemas (por ejemplo, debe garantizar la corrección del compilador). Pero nos ha funcionado bastante bien.
fuente
No parece agregar demasiada sobrecarga de mantenimiento si considera usar un lenguaje de nivel superior:
vs
Por supuesto, tendrá que enfrentar las limitaciones de la biblioteca, pero no la mantendrá usted mismo. Podría haber un buen equilibrio entre el costo de mantenimiento y el rendimiento ganado.
http://blogs.msdn.com/b/dotnet/archive/2014/04/07/the-jit-finally-proposed-jit-and-simd-are-getting-married.aspx
http://blogs.msdn.com/b/dotnet/archive/2014/05/13/update-to-simd-support.aspx
fuente
He hecho programación de ensamblaje en el pasado, no programación SIMD recientemente.
¿Has considerado usar un compilador compatible con SIMD como Intel? ¿Es interesante una guía de vectorización con compiladores Intel® C ++ ?
Varios de sus comentarios, como "reventar globos", sugieren utilizar un compilador (para obtener beneficios si no tiene un solo punto de acceso).
fuente