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

12

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

  • Un analizador léxico escanea (o llama a 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; token por token .

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

    
pregunta 8protons 16.06.2016 - 23:10

2 respuestas

22

Los proyectos de software grandes generalmente están compuestos de muchas unidades de compilación que pueden compilarse de manera relativamente independiente, por lo que la compilación a menudo se paraliza en una granularidad muy aproximada invocando el 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 apropiado. Me doy cuenta de que esto no es lo que pidió, pero eso es lo más parecido a la paralelización en la mayoría de los compiladores.

¿Por qué es eso? Bueno, gran parte del trabajo que hacen los compiladores no se presta fácilmente a la paralelización:

  • No puedes simplemente dividir la entrada en varios fragmentos y leerlos de forma independiente. Por simplicidad, querría dividirse en los límites de lexme (para que ningún hilo comience en medio de un lexme), pero determinar los límites de lexme potencialmente requiere mucho contexto. Por ejemplo, cuando saltas en la mitad del archivo, debes asegurarte de no saltar en una cadena literal. Pero para comprobar esto, tienes que mirar básicamente a todos los personajes que vinieron antes, que es casi tanto trabajo como simplemente decirlo para empezar. Además, el lexing rara vez es el cuello de botella en los compiladores para los idiomas modernos.
  • El análisis es incluso 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 para empezar. Si bien también puede haber formas de evitar esto, probablemente serán desproporcionadamente complejas para el pequeño beneficio. El análisis tampoco es el mayor cuello de botella.
  • Después de haber analizado, normalmente es necesario realizar una resolución de nombres, pero esto conduce a una enorme red de relaciones entrelazadas. Para resolver una llamada de método aquí, es posible que primero tenga que resolver las importaciones en este módulo, pero eso requiere resolver los nombres en la unidad de compilación otra , etc. Lo mismo para la inferencia de tipo si su idioma tiene eso.

Después de esto, se vuelve un poco más fácil. La comprobación de tipos y la optimización y la generación de código podrían, en principio, estar paralelizadas en la granularidad de la función. Todavía sé de pocos compiladores, si es que hay alguno, que hacen esto, tal vez porque hacer una tarea tan grande al mismo tiempo es bastante difícil. También debe considerar que la mayoría de los proyectos de software grandes contienen tantas unidades de compilación que el enfoque de "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 grandes tareas de compilación, la E / S del disco puede ser un cuello de botella tan grande como el trabajo real de compilación.

Dicho todo esto, sé de un compilador que paraliza 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 "middle-end") entre varios subprocesos. Esto se llama "unidades de código-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 podría haber menos unidades de compilación en vuelo de las que tiene núcleos.
  2. La parte que se está paralizando usualmente toma la mayor parte 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. Por supuesto, hay optimizaciones entre procedimientos, y las unidades de código sí las obstaculizan y, por lo tanto, afectan el rendimiento, pero no hay problemas semánticos.
respondido por el user7043 16.06.2016 - 23:36
1

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

A nadie le importa el tiempo para compilar un archivo. La gente se preocupa por la hora 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 das 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 hacerlo compilar 16 veces más rápido con un cambio de una línea a sus opciones de compilación.

    
respondido por el gnasher729 17.06.2016 - 11:05

Lea otras preguntas en las etiquetas