Esto me dejó preguntándome qué tan importante es Multithreading en el escenario actual de la industria.
En los campos críticos de rendimiento donde el rendimiento no proviene del código de terceros que hace el trabajo pesado, sino el nuestro, entonces tendería a considerar las cosas en este orden de importancia desde la perspectiva de la CPU (GPU es un comodín que gané entrar):
- Eficiencia de memoria (ej .: localidad de referencia).
- Algorítmico
- Multithreading
- SIMD
- Otras optimizaciones (sugerencias de predicción de rama estática, por ejemplo)
Tenga en cuenta que esta lista no se basa únicamente en la importancia, sino en muchas otras dinámicas, como el impacto que tienen en el mantenimiento, qué tan sencillas son (si no, vale la pena considerarlas más de antemano), sus interacciones con otros en la lista, etc.
Eficiencia de memoria
La mayoría podría sorprenderse de mi elección de eficiencia de memoria en lugar de algorítmica. Esto se debe a que la eficiencia de la memoria interactúa con los otros 4 elementos de esta lista, y es porque su consideración a menudo está muy en la categoría de "diseño" en lugar de la categoría de "implementación". Es cierto que aquí hay un pequeño problema de gallina o huevo, ya que comprender la eficiencia de la memoria a menudo requiere considerar los 4 elementos de la lista, mientras que los otros 4 elementos también requieren considerar la eficiencia de la memoria. Sin embargo, está en el corazón de todo.
Por ejemplo, si necesitamos una estructura de datos que ofrezca acceso secuencial en tiempo lineal e inserciones en tiempo constante en la parte posterior y nada más para elementos pequeños, la opción ingenua para alcanzar sería una lista vinculada. Eso sin tener en cuenta la eficiencia de la memoria. Cuando consideramos la eficiencia de la memoria en la mezcla, terminamos eligiendo estructuras más contiguas en este escenario, como estructuras basadas en arreglos de crecimiento o más nodos contiguos (por ejemplo: uno que almacena 128 elementos en un nodo) unidos entre sí, o al menos una lista vinculada respaldada por un asignador de agrupación. Estos tienen una ventaja dramática a pesar de tener la misma complejidad algorítmica. Del mismo modo, a menudo elegimos el ordenamiento rápido de una matriz sobre el tipo de combinación a pesar de una complejidad algorítmica inferior simplemente debido a la eficiencia de la memoria.
Del mismo modo, no podemos tener múltiples subprocesos eficientes si nuestros patrones de acceso a la memoria son tan granulares y de naturaleza dispersa que terminamos maximizando la cantidad de intercambio falso mientras bloqueamos los niveles más granulares en el código. Entonces, la eficiencia de la memoria multiplica la eficiencia de subprocesos múltiples. Es un requisito previo para aprovechar al máximo los hilos.
Cada uno de los elementos anteriores en la lista tiene una interacción compleja con los datos, y centrarse en cómo se representan los datos está en última instancia en la línea de la eficiencia de la memoria. Cada uno de estos anteriores puede tener un cuello de botella con una forma inapropiada de representar o acceder a los datos.
Otra razón por la eficiencia de la memoria es tan importante es que se puede aplicar a lo largo de una entera código base. En general, cuando la gente imagina que las ineficiencias se acumulan de pequeñas secciones de trabajo aquí y allá, es una señal de que necesitan tomar un perfilador. Sin embargo, los campos de baja latencia o los que se ocupan de hardware muy limitado encontrarán, incluso después de la creación de perfiles, sesiones que indican que no hay puntos de acceso claros (solo veces dispersos por todas partes) en una base de código que es claramente ineficiente con la forma en que se asigna, copia y accediendo a la memoria. Por lo general, esta es la única vez que una base de código completa puede ser susceptible a un problema de rendimiento que podría conducir a un conjunto completamente nuevo de estándares aplicados en toda la base de código, y la eficiencia de la memoria es a menudo el núcleo de la misma.
Algorítmico
Este es más o menos un hecho, ya que la elección en un algoritmo de clasificación puede marcar la diferencia entre una entrada masiva que toma meses para ordenar versus segundos para ordenar. Tiene el mayor impacto de todos si la elección es entre, digamos, algoritmos cuadráticos o cúbicos realmente por debajo del par y uno linealithmic, o entre lineal y logarítmico o constante, al menos hasta que tengamos 1,000,000 de máquinas centrales (en cuyo caso memoria la eficiencia se volvería aún más importante).
Sin embargo, no está en la parte superior de mi lista personal, ya que cualquier persona competente en su campo sabría usar una estructura de aceleración para el sacrificio de arranque, por ejemplo, estamos saturados de conocimiento algorítmico y de saber cosas como usar una variante de un método como un árbol de radix para búsquedas basadas en prefijos son cosas de bebé. Al carecer de este tipo de conocimiento básico del campo en el que estamos trabajando, la eficiencia algorítmica ciertamente se elevaría a la cima, pero a menudo la eficiencia algorítmica es trivial.
También inventar nuevos algoritmos puede ser una necesidad en algunos campos (por ejemplo, en el procesamiento de mallas he tenido que inventar cientos ya que antes no existían, o las implementaciones de características similares en otros productos eran secretos de propiedad, no publicados en un documento ) Sin embargo, una vez que hemos pasado la parte de resolución de problemas y encontramos una manera de obtener los resultados correctos, y una vez que la eficiencia se convierte en el objetivo, la única forma de obtenerla realmente es considerar cómo estamos interactuando con los datos (memoria). Sin comprender la eficiencia de la memoria, el nuevo algoritmo puede volverse innecesariamente complejo con esfuerzos inútiles para hacerlo más rápido, cuando lo único que necesitaba era un poco más de consideración de la eficiencia de la memoria para producir un algoritmo más simple y elegante.
Por último, los algoritmos tienden a estar más en la categoría de "implementación" que en la eficiencia de la memoria. A menudo son más fáciles de mejorar en retrospectiva, incluso con un algoritmo subóptimo utilizado inicialmente. Por ejemplo, un algoritmo de procesamiento de imágenes inferior a menudo solo se implementa en un lugar local en la base de código. Se puede cambiar por uno mejor más adelante. Sin embargo, si todos los algoritmos de procesamiento de imágenes están vinculados a una Pixel
interfaz que tiene una representación de memoria subóptima, pero la única forma de corregirla es cambiar la forma en que se representan múltiples píxeles (y no uno solo), entonces a menudo estamos SOL y tendrá que reescribir completamente la base de código hacia unImage
interfaz. El mismo tipo de cosas se aplica para reemplazar un algoritmo de clasificación: generalmente es un detalle de implementación, mientras que un cambio completo en la representación subyacente de los datos que se ordenan o la forma en que se pasan a través de los mensajes puede requerir el rediseño de las interfaces.
Multithreading
El subprocesamiento múltiple es difícil en el contexto del rendimiento, ya que es una optimización de nivel micro que juega con las características del hardware, pero nuestro hardware realmente está escalando en esa dirección. Ya tengo compañeros que tienen 32 núcleos (solo tengo 4).
Sin embargo, mulithreading es una de las micro optimizaciones más peligrosas que probablemente conoce un profesional si el propósito se usa para acelerar el software. La condición de carrera es prácticamente el error más mortal posible, ya que es de naturaleza tan indeterminada (tal vez solo aparece una vez cada pocos meses en la máquina de un desarrollador en el momento más inconveniente fuera del contexto de depuración, si es que lo hace). Por lo tanto, podría decirse que es la degradación más negativa en la mantenibilidad y la posible corrección del código entre todos estos, especialmente porque los errores relacionados con el subprocesamiento múltiple pueden pasar fácilmente por alto incluso las pruebas más cuidadosas.
Sin embargo, se está volviendo tan importante. Si bien puede que no siempre supere algo como la eficiencia de la memoria (que a veces puede hacer las cosas cien veces más rápido) dada la cantidad de núcleos que tenemos ahora, estamos viendo más y más núcleos. Por supuesto, incluso con máquinas de 100 núcleos, todavía pondría la eficiencia de la memoria en la parte superior de la lista, ya que la eficiencia de los hilos generalmente es imposible sin ella. Un programa puede usar cientos de subprocesos en una máquina de este tipo y aún ser lento sin una representación eficiente de memoria y patrones de acceso (que se vincularán con patrones de bloqueo).
SIMD
SIMD también es un poco incómodo ya que los registros en realidad se están ampliando, con planes para hacerlo aún más. Originalmente vimos registros MMX de 64 bits seguidos de registros XMM de 128 bits capaces de realizar 4 operaciones SPFP en paralelo. Ahora estamos viendo registros YMM de 256 bits capaces de 8 en paralelo. Y ya hay planes para registros de 512 bits que permitirían 16 en paralelo.
Estos interactuarían y se multiplicarían con la eficiencia del subprocesamiento múltiple. Sin embargo, SIMD puede degradar la capacidad de mantenimiento tanto como el subprocesamiento múltiple. Aunque los errores relacionados con ellos no son necesariamente tan difíciles de reproducir y corregir como un punto muerto o una condición de carrera, la portabilidad es incómoda y garantizar que el código pueda ejecutarse en la máquina de todos (y usar las instrucciones apropiadas basadas en sus capacidades de hardware) torpe.
Otra cosa es que, si bien los compiladores de hoy en día generalmente no superan el código SIMD escrito por expertos, sí superan fácilmente los intentos ingenuos. Podrían mejorar hasta el punto en que ya no tengamos que hacerlo manualmente, o al menos sin tener que hacerlo de manera tan manual como para escribir intrínsecos o código de ensamblaje directo (tal vez solo una pequeña guía humana).
Sin embargo, de nuevo, sin un diseño de memoria que sea eficiente para el procesamiento vectorizado, SIMD es inútil. Terminaremos cargando un campo escalar en un registro amplio solo para realizar una operación en él. En el corazón de todos estos elementos hay una dependencia de los diseños de memoria para ser verdaderamente eficientes.
Otras optimizaciones
A menudo, esto es lo que sugeriría que empecemos a llamar "micro" hoy en día si la palabra sugiere no solo ir más allá del enfoque algorítmico sino hacia cambios que tienen un impacto minúsculo en el rendimiento.
A menudo, tratar de optimizar la predicción de rama requiere un cambio en el algoritmo o la eficiencia de la memoria, por ejemplo, si esto se intenta simplemente a través de sugerencias y reordenando el código para la predicción estática, eso solo tiende a mejorar la ejecución por primera vez de dicho código, lo que hace que los efectos sean cuestionables si no suele ser despreciable.
Volver a Multithreading para rendimiento
De todos modos, ¿cuán importante es el subprocesamiento múltiple desde un contexto de rendimiento? En mi máquina de 4 núcleos, idealmente puede hacer las cosas 5 veces más rápido (lo que puedo obtener con hyperthreading). Sería considerablemente más importante para mi colega que tiene 32 núcleos. Y será cada vez más importante en los próximos años.
Entonces es bastante importante. Pero es inútil simplemente lanzar un montón de hilos al problema si la eficiencia de la memoria no está ahí para permitir que los bloqueos se usen con moderación, para reducir el intercambio falso, etc.
Multithreading fuera del rendimiento
El subprocesamiento múltiple no siempre se trata de un rendimiento absoluto en un sentido directo de rendimiento. A veces se usa para equilibrar una carga incluso con el posible costo de rendimiento para mejorar la capacidad de respuesta al usuario, o para permitir que el usuario realice más tareas múltiples sin esperar a que termine (por ejemplo: continuar navegando mientras descarga un archivo).
En esos casos, sugeriría que el subprocesamiento múltiple se eleve aún más hacia la parte superior (posiblemente incluso por encima de la eficiencia de la memoria), ya que se trata del diseño del usuario final en lugar de aprovechar al máximo el hardware. A menudo dominará los diseños de interfaz y la forma en que estructuramos toda nuestra base de código en tales escenarios.
Cuando no estamos simplemente paralelizando un circuito cerrado que accede a una estructura de datos masiva, el subprocesamiento múltiple va a la categoría de "diseño" realmente hardcore, y el diseño siempre triunfa sobre la implementación.
Entonces, en esos casos, diría que considerar el subprocesamiento múltiple es absolutamente crítico, incluso más que la representación y el acceso a la memoria.