Большие программные проекты обычно состоят из множества модулей компиляции, которые могут быть скомпилированы относительно независимо, поэтому компиляция часто распараллеливается с очень грубой детализацией, вызывая компилятор несколько раз параллельно. Это происходит на уровне процессов ОС и координируется системой сборки, а не самим компилятором. Я понимаю, что это не то, что вы просили, но это наиболее близко к распараллеливанию в большинстве компиляторов.
Это почему? Ну, большая часть работы, которую выполняют компиляторы, не легко поддается распараллеливанию:
- Вы не можете просто разделить входные данные на несколько частей и добавить их независимо друг от друга. Для простоты вы хотите разделить границы лексмы (чтобы ни один поток не начинался в середине лексмы), но определение границ лексмы потенциально требует большого контекста. Например, когда вы переходите в середину файла, вы должны убедиться, что не перепрыгнули в строковый литерал. Но чтобы проверить это, вы должны взглянуть в основном на каждого персонажа, который был до этого, это почти такая же работа, как просто лексизм для начала. Кроме того, лексинг редко является узким местом в компиляторах для современных языков.
- Синтаксический анализ еще сложнее распараллелить. Все проблемы разделения входного текста для lexing еще более применимы к разделению токенов для синтаксического анализа - например, определить, где начинается функция, в основном так же сложно, как начать синтаксический анализ содержимого функции. Хотя могут также быть способы обойти это, они, вероятно, будут непропорционально сложными для небольшой выгоды. Разбор тоже не самое большое узкое место.
- После того как вы проанализировали, вам обычно нужно выполнить разрешение имен, но это приводит к огромной переплетенной сети отношений. Чтобы разрешить вызов метода здесь, вам, возможно, придется сначала разрешить импорт в этом модуле, но для этого необходимо разрешить имена в другом модуле компиляции и т. Д. То же самое для вывода типа, если ваш язык имеет это.
После этого становится немного легче. Проверка и оптимизация типов, а также генерация кода могут, в принципе, распараллеливаться на уровне детализации функций. Я до сих пор знаю мало, если какие-либо компиляторы делают это, возможно, потому что выполнение любой такой большой задачи одновременно довольно сложно. Вы также должны учитывать, что большинство крупных программных проектов содержат так много модулей компиляции, что подход «запускать несколько компиляторов параллельно» вполне достаточен для того, чтобы сохранить все ваши ядра занятыми (а в некоторых случаях даже целую ферму серверов). Кроме того, в больших задачах компиляции дисковый ввод-вывод может быть таким же узким местом, как и фактическая работа по компиляции.
Из всего сказанного я знаю компилятор, который распараллеливает работу генерации и оптимизации кода. Компилятор Rust может разделить внутреннюю работу (LLVM, которая фактически включает в себя оптимизации кода, которые традиционно считаются «промежуточными») между несколькими потоками. Это называется «единицами кода». В отличие от других возможностей распараллеливания, описанных выше, это экономично, потому что:
- Язык имеет довольно большие единицы компиляции (по сравнению, скажем, с C или Java), поэтому в полете может быть меньше единиц компиляции, чем у вас ядер.
- Параллелизируемая часть обычно занимает подавляющее большинство времени компиляции.
- Работа с бэкэндом, по большей части, смущающе параллельна - просто оптимизируйте и переводите в машинный код каждую функцию независимо. Конечно, существуют межпроцедурные оптимизации, и блоки codegen препятствуют им и, таким образом, влияют на производительность, но семантических проблем нет.