Estoy tratando de comprender mejor lo que se necesitaría para que un compilador pueda tomar decisiones inteligentes con respecto a la concurrencia en nombre del programador. Me doy cuenta de que hay muchos aspectos difíciles de este problema, por ejemplo:
- Asegurarse de que no hay condiciones de carrera
Asegurarse de que el código se ejecute simultáneamente no tendrá efectos secundarios que afecten el significado semántico del código
Decidir si vale la pena la sobrecarga de los subprocesos, dado el grado de paralelismo disponible en el código
Según tengo entendido, las dos representaciones intermedias principales que se utilizan en los compiladores modernos son asignaciones únicas estáticas para lenguajes de procedimiento y orientados a objetos y estilo de paso de continuación para lenguajes funcionales. Razonar sobre cualquiera de los problemas enumerados anteriormente parece difícil usando estas formas intermedias. Incluso los lenguajes que, en teoría, deberían tener la mejor oportunidad de paralelización automática (lenguajes funcionales puros como Haskell con la garantía de que no hay efectos secundarios) han tenido un progreso limitado en este frente.
Entonces, mi pregunta es, ¿qué representaciones intermedias se han utilizado para tratar de abordar este problema? ¿Hay otras representaciones que se hayan utilizado en la investigación académica que no conozca que se adapten mejor a esta tarea? ¿Es este un problema que el compilador tiene que resolver fundamentalmente manipulando el árbol de sintaxis abstracta antes de que la compilación alcance una representación intermedia?
Respuestas:
Uno supondría que la concurrencia de modelado explícitamente en la representación intermedia (IR) era un requisito necesario. Entonces, una respuesta sería: "cualquier IR utilizada para programas secuenciales con la adición de algunas operaciones de concurrencia", por ejemplo, "bifurcación y unión", "paralelo x y". Agregar estos hace posible razonar sobre algunos tipos de concurrencia, pero no necesariamente fácilmente. Tampoco es obvio cómo garantizar ciertas propiedades (libertad de carrera de datos) sin llegar a una representación completamente funcional (lo que dificulta modelar el paralelismo de manera útil).
Posiblemente, las redes de Petri coloreadas (CPN) son una buena opción para representar programas con concurrencia. (Los tokens en los CPN son "coloreados" [tienen un tipo] y pueden transportar valores; las "transiciones" en estados pueden realizar operaciones aritméticas arbitrarias en tokens entrantes para producir un token posiblemente de diferente color con un valor calculado en el "lugar"). Si piensa que los lugares son resultados calculados y transiciones como operadores de modelado (incluido uno especial para acceder a la memoria), esto le proporciona lo que equivale a un gráfico de flujo de datos con el que modelar programas. Puede usar esto fácilmente para dar una interpretación formal a las representaciones clásicas del compilador, como triples [operador, entrada1, entrada2, salida].
Existen muchas herramientas para analizar tales gráficos de CPN, incluidas las propiedades informáticas como la ausencia de puntos muertos, la limitación de los recuentos de tokens en lugares, etc. Los CPN jerárquicos le permiten modelar funciones y procedimientos y la noción de "llamadas".
Lo que estas representaciones no hacen claramente es facilitar razonar sobre dónde se podría paralelizar una aplicación. Trivialmente, dos subcomputaciones pueden ser paralelas si no afectan a los operandos compartidos (razón por la cual algunas personas aman los programas / representaciones funcionales). Si la representación de su programa modela una memoria compartida, puede modelarla como un monolito y obtener el conjunto habitual de problemas sobre el razonamiento sobre las interacciones en la memoria compartida, incluido el direccionamiento con alias, etc. Una forma de evitar esto es tratar la memoria como fragmentos aislados con el estado más amplio del programa es un ensamblaje (similar a un árbol) de estos; se puede pasar estos trozos en su representación intermedia. No hay interacción entre dos cálculos paralelos si no comparten fragmentos (por ejemplo, subárboles de memoria).
fuente