Используют ли компиляторы многопоточность для ускорения компиляции?

16

Если я правильно помню курс по компиляторам, типичный компилятор имеет следующую упрощенную схему:

  • Лексический анализатор сканирует (или вызывает некоторую функцию сканирования) исходный код посимвольно
  • Строка входных символов проверяется на соответствие словаря лексем
  • Если лексема действительна, она классифицируется как токен, которому она соответствует
  • Парсер проверяет синтаксис комбинации токенов; токен за токеном .

Теоретически возможно разделить исходный код на четверти (или любой знаменатель) и многопоточность процесса сканирования и анализа? Существуют ли компиляторы, которые используют многопоточность?

8protons
источник
1
@RobertHarvey Первый ответ первой ссылки написал: «Но сами компиляторы все еще однопоточные». Так что нет?
8протонов
Я предлагаю вам прочитать остальные ответы, особенно этот , и вторую ссылку, которую я разместил.
Роберт Харви
2
@RobertHarvey Вторая ссылка, которую вы разместили, насколько я понимаю, говорит о компиляторе, который генерирует многопоточную версию вашего скомпилированного приложения. Дело не в самом компиляторе. Спасибо за ваши общие ресурсы и нашли время, чтобы ответить.
8протонов

Ответы:

29

Большие программные проекты обычно состоят из множества модулей компиляции, которые могут быть скомпилированы относительно независимо, поэтому компиляция часто распараллеливается с очень грубой детализацией, вызывая компилятор несколько раз параллельно. Это происходит на уровне процессов ОС и координируется системой сборки, а не самим компилятором. Я понимаю, что это не то, что вы просили, но это наиболее близко к распараллеливанию в большинстве компиляторов.

Это почему? Ну, большая часть работы, которую выполняют компиляторы, не легко поддается распараллеливанию:

  • Вы не можете просто разделить входные данные на несколько частей и добавить их независимо друг от друга. Для простоты вы хотите разделить границы лексмы (чтобы ни один поток не начинался в середине лексмы), но определение границ лексмы потенциально требует большого контекста. Например, когда вы переходите в середину файла, вы должны убедиться, что не перепрыгнули в строковый литерал. Но чтобы проверить это, вы должны взглянуть в основном на каждого персонажа, который был до этого, это почти такая же работа, как просто лексизм для начала. Кроме того, лексинг редко является узким местом в компиляторах для современных языков.
  • Синтаксический анализ еще сложнее распараллелить. Все проблемы разделения входного текста для lexing еще более применимы к разделению токенов для синтаксического анализа - например, определить, где начинается функция, в основном так же сложно, как начать синтаксический анализ содержимого функции. Хотя могут также быть способы обойти это, они, вероятно, будут непропорционально сложными для небольшой выгоды. Разбор тоже не самое большое узкое место.
  • После того как вы проанализировали, вам обычно нужно выполнить разрешение имен, но это приводит к огромной переплетенной сети отношений. Чтобы разрешить вызов метода здесь, вам, возможно, придется сначала разрешить импорт в этом модуле, но для этого необходимо разрешить имена в другом модуле компиляции и т. Д. То же самое для вывода типа, если ваш язык имеет это.

После этого становится немного легче. Проверка и оптимизация типов, а также генерация кода могут, в принципе, распараллеливаться на уровне детализации функций. Я до сих пор знаю мало, если какие-либо компиляторы делают это, возможно, потому что выполнение любой такой большой задачи одновременно довольно сложно. Вы также должны учитывать, что большинство крупных программных проектов содержат так много модулей компиляции, что подход «запускать несколько компиляторов параллельно» вполне достаточен для того, чтобы сохранить все ваши ядра занятыми (а в некоторых случаях даже целую ферму серверов). Кроме того, в больших задачах компиляции дисковый ввод-вывод может быть таким же узким местом, как и фактическая работа по компиляции.

Из всего сказанного я знаю компилятор, который распараллеливает работу генерации и оптимизации кода. Компилятор Rust может разделить внутреннюю работу (LLVM, которая фактически включает в себя оптимизации кода, которые традиционно считаются «промежуточными») между несколькими потоками. Это называется «единицами кода». В отличие от других возможностей распараллеливания, описанных выше, это экономично, потому что:

  1. Язык имеет довольно большие единицы компиляции (по сравнению, скажем, с C или Java), поэтому в полете может быть меньше единиц компиляции, чем у вас ядер.
  2. Параллелизируемая часть обычно занимает подавляющее большинство времени компиляции.
  3. Работа с бэкэндом, по большей части, смущающе параллельна - просто оптимизируйте и переводите в машинный код каждую функцию независимо. Конечно, существуют межпроцедурные оптимизации, и блоки codegen препятствуют им и, таким образом, влияют на производительность, но семантических проблем нет.

источник
2

Компиляция - это «смущающая параллель» проблема.

Никто не заботится о времени для составления одного файла. Люди заботятся о времени составления 1000 файлов. А для 1000 файлов каждое ядро ​​процессора может скомпилировать по одному файлу за раз, поддерживая все ядра полностью занятыми.

Совет: «make» использует несколько ядер, если вы укажете ему правильную опцию командной строки. Без этого он будет компилировать один файл за другим в 16-ядерной системе. Это означает, что вы можете сделать компиляцию в 16 раз быстрее, изменив параметры сборки на одну строку.

gnasher729
источник