El modelo más antiguo para la ejecución de instrucciones es el modelo de Von Neumann: un programa es una lista de instrucciones que se refieren a ubicaciones de memoria para sus operandos y salidas; Se ejecutan secuencialmente.
Parece muy lógico y es muy fácil de programar. Sin embargo, no es cómo funciona el hardware, y cada vez más necesitamos capa tras capa de magia para que el hardware parezca comportarse de esa manera. Por ejemplo, las CPU modernas tienen múltiples instrucciones al mismo tiempo, en unidades aritméticas independientes o en la tubería de instrucciones generales. La siguiente instrucción no se inicia cuando finaliza la última, no, se hace de forma especulativa, superponiéndose con la actual, y si es necesario, el procesador dará marcha atrás en la decisión de iniciarla.
La memoria plana del modelo de Von Neumann también es una ficción. Era cierto hace 25 años (creo que el Pentium II fue el último procesador donde la memoria podía seguir el ritmo del procesador) pero ya no: la memoria es lenta, por lo que necesita cosas como cachés y TLB para almacenar datos más cerca del procesador que puede ser necesario más tarde, o captar secuencias para recuperar datos especulativamente.
Otra cosa incorrecta con el modelo de “flujo de instrucciones” es que un algoritmo a menudo tiene paralelismo intrínseco. Como acabo de señalar, el procesador también tiene paralelismo. Entonces, ¿por qué tenemos un modelo de instrucción sin paralelismo, donde necesitamos que el compilador descubra dónde fue el paralelismo que nuestro conjunto de instrucciones no podía expresar?
Ok, dos o tres propuestas.
1. Reemplace el modelo de “lista de instrucciones” por flujo de datos, donde las instrucciones forman un Gráfico Acíclico Dirigido, con cada instrucción indicando explícitamente de qué otras dependen. Si dos instrucciones tienen todas sus entradas disponibles, se pueden ejecutar en paralelo si el hardware lo permite.
1a. El flujo de datos definitivamente debería ser el modelo de programación, de modo que ya no necesitemos compiladores clarividentes.
1b. El flujo de datos probablemente también debería ser el modelo de ejecución. Ha habido algunos experimentos en la década de 1980 con computadoras de flujo de datos, y en cierto modo las GPU con CUDA son máquinas de flujo de datos. De hecho, el motor de ejecución fuera de orden de las CPU modernas ya es un pequeño motor de flujo de datos, simplemente invisible para el programador.
2. Actualmente, el costo del movimiento de datos es en realidad mucho más importante que la ejecución de instrucciones. Por lo tanto, debemos deshacernos de esta ficción de un modelo de memoria plana: los programadores deben hacer frente a la noción de localidad / no localidad de los datos. Será un buen truco crear un lenguaje de programación que permita al programador expresar esto sin que se convierta en una carga. (Tengo algunas ideas sobre esto en mi propia investigación)
3. Como alguien mencionó poderes de dos, seré contrario y abogaré por su abolición. ¡Utiliza primos de Mersenne en su lugar! El problema con las potencias de dos es que a menudo aparecen en nuestros algoritmos, pero si asigna datos o instrucciones que difieren en potencias de 2 al hardware que solo difiere mínimamente, obtendrá un uso desigual de los recursos. El ejemplo estándar, por supuesto, es el algoritmo FFT con su secuencia de potencias de dos. Esto afectará cada conflicto de caché y conflicto de banco de memoria, etc., que el hardware es capaz de tener. Ningún problema es que basamos todo en números primos. Los primos de Mersenne son atractivos porque están tan cerca de las potencias de dos, por lo que se pueden falsificar con una sobrecarga mínima en la parte superior del hardware de potencia de dos.