¿Los compiladores utilizan subprocesos múltiples para tiempos de compilación más rápidos?

16

Si recuerdo el curso de mi compilador correctamente, el compilador típico tiene el siguiente esquema simplificado:

  • Un analizador léxico escanea (o activa alguna función de escaneo) el código fuente carácter por carácter
  • La cadena de caracteres de entrada se compara con el diccionario de lexemas para verificar su validez.
  • Si el lexema es válido, se clasifica como el token al que corresponde
  • El analizador valida la sintaxis de la combinación de tokens; ficha por ficha .

¿Es teóricamente factible dividir el código fuente en cuartos (o cualquier denominador) y multiprocesar el proceso de escaneo y análisis? ¿Existen compiladores que utilizan subprocesos múltiples?

8protones
fuente
1
@RobertHarvey La respuesta principal del primer enlace escribió, "pero los compiladores en sí mismos siguen siendo de un solo subproceso". ¿Entonces eso es un no?
8protons
Le sugiero que lea el resto de las respuestas, especialmente esta , y el segundo enlace que publiqué.
Robert Harvey
2
@RobertHarvey El segundo enlace que publicaste, según tengo entendido, es hablar de un compilador que genera una versión multiproceso de tu aplicación compilada. No se trata del compilador en sí. Gracias por sus recursos compartidos y por tomarse el tiempo para responder.
8protons

Respuestas:

29

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:

  1. 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.
  2. La parte que se está paralelizando generalmente toma la gran mayoría del tiempo de compilación.
  3. 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.

fuente
2

La compilación es un problema "vergonzosamente paralelo".

A nadie le importa el tiempo para compilar un archivo. La gente se preocupa por el momento de compilar 1000 archivos. Y para 1000 archivos, cada núcleo del procesador puede compilar felizmente un archivo a la vez, manteniendo todos los núcleos totalmente ocupados.

Consejo: "make" usa múltiples núcleos si le da la opción de línea de comando correcta. Sin eso, compilará un archivo tras otro en un sistema de 16 núcleos. Lo que significa que puede compilarlo 16 veces más rápido con un cambio de una línea a sus opciones de compilación.

gnasher729
fuente