Con la forma en que abordo las cosas, el tipo de subprocesamiento múltiple es gratuito y relativamente sencillo de aplicar en retrospectiva. Pero primero estoy pensando en los datos. No sé si esto funciona para todos los dominios, pero intentaré cubrir cómo lo hago.
Entonces, primero se trata del tipo de datos más grueso requerido para el software que se procesará con frecuencia. Si es un juego que podría ser mallas, sonidos, movimiento, emisores de partículas, luces, texturas, cosas de este tipo. Y, por supuesto, hay mucho en qué pensar si profundiza solo en mallas y piensa en cómo deberían representarse, pero lo omitiremos por ahora. En este momento estamos pensando en el nivel arquitectónico más amplio.
Y mi primer pensamiento es: "¿Cómo unificamos la representación de todas estas cosas para que podamos lograr un patrón de acceso relativamente uniforme para todos estos tipos de cosas?" Y mi primer pensamiento podría ser almacenar cada tipo de cosas en su propia matriz contigua con una forma de lista libre para reclamar espacios vacíos. Y eso tiende a unificar la API para que podamos usar más fácilmente, por ejemplo, el mismo tipo de código para serializar mallas como hacemos luces y texturas, al menos en cuanto a dónde y cómo se accede a estos componentes. Cuanto más podamos unificar cómo se representa todo, más el código que accede a esas cosas tiende a tomar una forma uniforme.
Eso es genial. Ahora también podemos señalar estas cosas con índices de 32 bits y solo tomar la mitad de la memoria de un puntero de 64 bits. Y oye, ahora podemos establecer intersecciones en tiempo lineal si podemos asociar un conjunto de bits paralelo, por ejemplo, también podemos asociar datos a cualquiera de estas cosas de forma muy económica en paralelo ya que estamos indexando todo. Ah, y ese conjunto de bits puede devolvernos un conjunto de índices ordenados para recorrer en orden secuencial para mejorar los patrones de acceso a la memoria, sin tener que volver a cargar la misma línea de caché varias veces en un solo bucle. Podemos probar 64 bits a la vez. Si no se establecen todos los 64 bits, podemos omitir más de 64 elementos a la vez. Si todos están configurados, podemos procesarlos todos a la vez. Si algunos están configurados pero no todos, podemos usar las instrucciones de FFS para determinar rápidamente qué bits están configurados.
Pero, espera, eso es algo costoso si solo quisiéramos asociar datos a unos cientos de miles de cosas. Entonces usemos una matriz dispersa, así:
Y oye, ahora que tenemos todo almacenado en matrices dispersas e indexándolas, sería bastante fácil hacer de esta una estructura de datos persistente.
Ahora podemos escribir funciones más baratas sin efectos secundarios, ya que no necesitan copiar en profundidad lo que no ha cambiado.
Y aquí ya me dieron una hoja de trucos después de aprender sobre los motores ECS, pero ahora pensemos en qué tipo de funciones amplias deberían estar operando en cada tipo de componente. Podemos llamar a estos "sistemas". El "SoundSystem" puede procesar componentes "Sound". Cada sistema es una función amplia que opera en uno o más tipos de datos.
Eso nos deja con muchos casos en los que, para cualquier tipo de componente dado, solo uno o dos sistemas generalmente accederán a ellos. Hmm, eso parece seguro que ayudaría con la seguridad del hilo y reduciría al mínimo la contención del hilo.
Además, trato de pensar en cómo hacer pases homogéneos sobre los datos. En lugar de como:
for each thing:
play with it
cuddle it
kill it
Intento dividirlo en múltiples pases más simples:
for each thing:
play with it
for each thing:
cuddle it
for each thing:
kill it
Eso a veces requiere almacenar algún estado intermedio para el siguiente paso diferido homogéneo para procesar, pero descubrí que realmente me ayuda a mantener y razonar sobre el código, sabiendo que cada ciclo tiene una lógica más simple y más uniforme. Y oye, eso parece que simplificaría la seguridad del hilo y reduciría la contención del hilo.
Y simplemente sigue así hasta que encuentra que tiene una arquitectura que es realmente fácil de paralelizar con confianza sobre la seguridad y la corrección de su hilo, pero todo inicialmente con el enfoque de unificar representaciones de datos, tener patrones de acceso a memoria más predecibles, reduciendo uso de memoria, simplificando los flujos de control a pasos más homogéneos, reduciendo la cantidad de funciones en su sistema que causan efectos secundarios sin incurrir en costos de copia profunda muy costosos, unificando su API, etc.
Cuando combina todas estas cosas, tiende a terminar con un sistema que minimiza la cantidad de estado compartido en el que tropezó con un diseño que es realmente amigable para la concurrencia. Y si es necesario compartir algún estado, a menudo encuentra que no tiene mucha contención donde es barato usar alguna sincronización sin causar un atasco de tráfico, y además, a menudo puede ser manejado por su estructura de datos central que unifica la representación de todas las cosas en el sistema para que no tenga que aplicar sincronizaciones de hilos a cientos de lugares diferentes, solo a un puñado.
Ahora, cuando profundizamos en uno de los componentes más complejos, como las mallas, repetimos el mismo proceso de diseño, comenzando por pensar primero en los datos. Y si lo hacemos bien, incluso podríamos ser capaces de paralelizar fácilmente el procesamiento de una sola malla, pero el diseño arquitectónico más amplio que establecimos ya nos permite paralelizar el procesamiento de múltiples mallas.