Los grandes proyectos de software generalmente están compuestos por muchas unidades de compilación que se pueden compilar de manera relativamente independiente, por lo que la compilación a menudo se paraleliza con una granularidad muy aproximada invocando al compilador varias veces en paralelo. Esto sucede a nivel de los procesos del sistema operativo y es coordinado por el sistema de compilación en lugar del compilador propiamente dicho. Me doy cuenta de que esto no es lo que pediste, pero eso es lo más parecido a la paralelización en la mayoría de los compiladores.
¿Porqué es eso? Bueno, gran parte del trabajo que realizan los compiladores no se presta fácilmente a la paralelización:
- No puede simplemente dividir la entrada en varios fragmentos y eliminarlos de forma independiente. Para simplificar, desearía dividirse en los límites de lexme (para que ningún hilo comience en el medio de un lexme), pero determinar los límites de lexme potencialmente requiere mucho contexto. Por ejemplo, cuando saltas en el medio del archivo, debes asegurarte de no saltar a un literal de cadena. Pero para verificar esto, tienes que mirar básicamente a todos los personajes que vinieron antes, lo que es casi tanto trabajo como simplemente dejarlo de lado. Además, el lexing rara vez es el cuello de botella en los compiladores para los idiomas modernos.
- Analizar es aún más difícil de paralelizar. Todos los problemas de dividir el texto de entrada para el lexing se aplican aún más a la división de los tokens para el análisis, por ejemplo, determinar dónde comienza una función es básicamente tan difícil como analizar el contenido de la función. Si bien también puede haber formas de evitar esto, probablemente serán desproporcionadamente complejos para el pequeño beneficio. Analizar tampoco es el mayor cuello de botella.
- Después de analizar, generalmente necesita realizar una resolución de nombre, pero esto conduce a una enorme red entrelazada de relaciones. Para resolver una llamada a un método aquí, es posible que primero deba resolver las importaciones en este módulo, pero éstas requieren resolver los nombres en otra unidad de compilación, etc. Lo mismo para la inferencia de tipos si su idioma lo tiene.
Después de esto, se vuelve un poco más fácil. La verificación y optimización de tipos y la generación de código podrían, en principio, ser paralelas a la granularidad de la función. Todavía sé de pocos compiladores que hagan esto, tal vez porque hacer cualquier tarea tan grande al mismo tiempo es bastante desafiante. También debe tener en cuenta que los proyectos de software más grandes contienen tantas unidades de compilación que el enfoque "ejecutar un montón de compiladores en paralelo" es completamente suficiente para mantener todos sus núcleos ocupados (y en algunos casos, incluso una granja de servidores completa). Además, en tareas de compilación grandes, la E / S de disco puede ser un cuello de botella tanto como el trabajo real de compilación.
Dicho todo esto, conozco un compilador que paraleliza el trabajo de generación y optimización de código. El compilador Rust puede dividir el trabajo de back-end (LLVM, que en realidad incluye optimizaciones de código que tradicionalmente se consideran "de gama media") entre varios subprocesos. Esto se llama "unidades code-gen". En contraste con las otras posibilidades de paralelización discutidas anteriormente, esto es económico porque:
- El lenguaje tiene unidades de compilación bastante grandes (en comparación con, por ejemplo, C o Java), por lo que puede haber menos unidades de compilación en vuelo que los núcleos.
- La parte que se está paralelizando generalmente toma la gran mayoría del tiempo de compilación.
- El trabajo de back-end es, en su mayor parte, vergonzosamente paralelo: simplemente optimice y traduzca al código de máquina cada función de forma independiente. Hay optimizaciones entre procedimientos, por supuesto, y las unidades de codegen sí las obstaculizan y, por lo tanto, afectan el rendimiento, pero no hay problemas semánticos.