Как Go компилируется так быстро?

217

Я гуглил и ковырялся на сайте Go, но, похоже, не могу найти объяснения необыкновенному времени сборки Go. Являются ли они продуктами языковых возможностей (или их отсутствием), высоко оптимизированным компилятором или чем-то еще? Я не пытаюсь продвигать Go; Мне просто любопытно.

Эван Кроске
источник
12
@ Поддержите, я знаю об этом. Я думаю, что реализация компилятора таким образом, чтобы он компилировался с заметной быстротой, - это не преждевременная оптимизация. Скорее всего, это результат правильного проектирования и разработки программного обеспечения. Кроме того, я не могу видеть слова Кнута, вырванные из контекста и примененные неправильно.
Адам Кроссленд
55
Пессимистская версия этого вопроса: «Почему C ++ компилируется так медленно?» stackoverflow.com/questions/588884/...
dan04
14
Я проголосовал, чтобы вновь открыть этот вопрос, поскольку он не основан на мнении. Можно дать хороший технический (не мнительный) обзор языка и / или выбора компилятора, который обеспечивает скорость компиляции.
Мартин Турной
Для небольших проектов Go кажется мне медленным. Это потому, что я помню, что Turbo-Pascal работал намного быстрее на компьютере, который был, вероятно, в тысячи раз медленнее. prog21.dadgum.com/47.html?repost=true . Каждый раз, когда я набираю «go build», и в течение нескольких секунд ничего не происходит, я вспоминаю старые твердые компиляторы Фортрана и перфокарты. YMMV. TLDR: «медленный» и «быстрый» являются относительными терминами.
RedGrittyBrick
Определенно рекомендую прочитать dave.cheney.net/2014/06/07/five-things-that-make-go-fast для более детального понимания
Karthik

Ответы:

193

Анализ зависимостей.

Go FAQ используется , чтобы содержать следующую фразу:

Go предоставляет модель для конструирования программного обеспечения, которая упрощает анализ зависимостей и позволяет избежать значительных накладных расходов на файлы и библиотеки в стиле C.

Хотя этой фразы больше нет в FAQ, эта тема более подробно обсуждается в докладе Go на Google , в котором сравнивается подход к анализу зависимостей в C / C ++ и Go.

Это главная причина быстрой компиляции. И это по замыслу.

Игорь Кривокон
источник
Этой фразы больше нет в FAQ по Go, но более подробное объяснение темы «анализа зависимостей», в которой сравниваются подходы C / C ++ и Pascal / Modula / Go, доступно в
докладе
76

Я думаю, дело не в том, что компиляторы Go работают быстро , а в том, что другие компиляторы работают медленно .

Компиляторы C и C ++ должны анализировать огромное количество заголовков - например, компиляция C ++ «hello world» требует компиляции 18 тыс. Строк кода, что составляет почти половину мегабайта источников!

$ cpp hello.cpp | wc
  18364   40513  433334

Компиляторы Java и C # работают в виртуальной машине, а это означает, что прежде чем они смогут что-либо скомпилировать, операционная система должна загрузить всю виртуальную машину, затем они должны быть JIT-скомпилированы из байт-кода в собственный код, и все это занимает некоторое время.

Скорость компиляции зависит от нескольких факторов.

Некоторые языки предназначены для быстрой компиляции. Например, Pascal был разработан для компиляции с использованием однопроходного компилятора.

Компиляторы тоже могут быть оптимизированы. Например, компилятор Turbo Pascal был написан на оптимизированном вручную ассемблере, что в сочетании с языковым дизайном привело к действительно быстрому компилятору, работающему на оборудовании класса 286. Я думаю, что даже сейчас современные компиляторы Pascal (например, FreePascal) работают быстрее, чем компиляторы Go.

el.pescado
источник
19
Компилятор Microsoft C # не работает в виртуальной машине. Он все еще написан на C ++, в основном из соображений производительности.
blucz
19
Turbo Pascal и позже Delphi являются лучшими примерами для невероятно быстрых компиляторов. После того, как архитектор обоих перешел на Microsoft, мы увидели огромные улучшения как в компиляторах MS, так и в языках. Это не случайное совпадение.
TheBlastOne
7
18 тыс. Строк (18364, если быть точным) кода - 433334 байта (~ 0,5 МБ)
el.pescado
9
Компилятор C # был скомпилирован с C # с 2011 года. Просто обновление, если кто-нибудь прочтет это позже.
Курт Коллер
3
Однако компилятор C # и CLR, который запускает сгенерированный MSIL, - разные вещи. Я вполне уверен, что CLR не написан на C #.
jocull
39

Есть несколько причин, почему компилятор Go намного быстрее, чем большинство компиляторов C / C ++:

  • Основная причина : большинство компиляторов C / C ++ демонстрируют исключительно плохие дизайны (с точки зрения скорости компиляции). Кроме того, с точки зрения скорости компиляции, некоторые части экосистемы C / C ++ (такие как редакторы, в которых программисты пишут свои коды) не разработаны с учетом скорости компиляции.

  • Основная причина : быстрая скорость компиляции была осознанным выбором в компиляторе Go, а также в языке Go

  • Компилятор Go имеет более простой оптимизатор, чем компиляторы C / C ++

  • В отличие от C ++, Go не имеет шаблонов и встроенных функций. Это означает, что Go не нужно выполнять какой-либо шаблон или функцию создания экземпляра.

  • Компилятор Go генерирует низкоуровневый код сборки быстрее, и оптимизатор работает с кодом сборки, в то время как в типичном компиляторе C / C ++ оптимизация пропускает работу над внутренним представлением исходного исходного кода. Дополнительные издержки в компиляторе C / C ++ обусловлены тем фактом, что необходимо создать внутреннее представление.

  • Окончательное связывание (5l / 6l / 8l) программы Go может быть медленнее, чем связывание программы C / C ++, потому что компилятор Go просматривает весь использованный код сборки и, возможно, также выполняет другие дополнительные действия, которые выполняет C / C ++. линкеры не делают

  • Некоторые компиляторы C / C ++ (GCC) генерируют инструкции в текстовом виде (для передачи ассемблеру), в то время как компилятор Go генерирует инструкции в двоичном виде. Необходимо выполнить дополнительную работу (но не так много), чтобы преобразовать текст в двоичный файл.

  • Компилятор Go предназначен только для небольшого числа архитектур ЦП, в то время как компилятор GCC предназначен для большого количества ЦП.

  • Компиляторы, которые были разработаны с целью обеспечения высокой скорости компиляции, такие как Jikes, работают быстро. На процессоре с тактовой частотой 2 ГГц Jikes может компилировать более 20000 строк Java-кода в секунду (а инкрементный режим компиляции еще более эффективен).

user811773
источник
17
Компилятор Go содержит небольшие функции. Я не уверен, что нацеливание на небольшое количество процессоров делает вас быстрее, медленнее ... Я предполагаю, что gcc не генерирует код PPC, пока я компилирую для x86.
Брэд Фицпатрик
@BradFitzpatrick не терпит возрождения старого комментария, но, ориентируясь на меньшее количество платформ, разработчики компилятора могут тратить больше времени на его оптимизацию для каждого.
Постоянство
использование промежуточной формы позволяет поддерживать гораздо больше архитектур, поскольку теперь вам нужно всего лишь написать новый бэкэнд для каждой новой архитектуры
phuclv
34

Эффективность компиляции была главной целью проекта:

Наконец, он предназначен для быстрой работы: создание большого исполняемого файла на одном компьютере должно занимать не более нескольких секунд. Для достижения этих целей требуется решение ряда языковых проблем: выразительная, но легковесная система типов; параллелизм и сборка мусора; жесткая спецификация зависимостей; и так далее. Вопросы-Ответы

Часто задаваемые вопросы по языку довольно интересны в отношении специфических языковых особенностей, связанных с синтаксическим анализом:

Во-вторых, язык разработан так, чтобы его было легко анализировать и анализировать без таблицы символов.

Larry OBrien
источник
6
Это не правда. Вы не можете полностью разобрать исходный код Go без таблицы символов.
12
Я также не понимаю, почему сборка мусора увеличивает время компиляции. Это просто не так.
TheBlastOne
3
Это цитаты из FAQ: golang.org/doc/go_faq.html Я не могу сказать, не смогли ли они достичь своих целей (таблица символов) или их логика неверна (GC).
Ларри Обриен
5
@FUZxxl Перейдите на golang.org/ref/spec#Primary_expressions и рассмотрите две последовательности [Операнд, Вызов] и [Преобразование]. Пример Go исходный код: идентификатор1 (идентификатор2). Без таблицы символов невозможно решить, является ли этот пример вызовом или преобразованием. | Любой язык может быть в некоторой степени проанализирован без таблицы символов. Это правда, что большинство частей исходных кодов Go могут быть проанализированы без таблицы символов, но неверно, что можно распознать все элементы грамматики, определенные в спецификации golang.
3
@ Atom Вы усердно работаете, чтобы парсер никогда не был частью кода, который сообщает об ошибке. Синтаксические анализаторы, как правило, плохо сообщают о связных сообщениях об ошибках. Здесь вы создаете дерево разбора для выражения, как если бы aTypeоно было ссылкой на переменную, и позже на этапе семантического анализа, когда вы обнаружите, что это не вы печатаете значимую ошибку в то время.
Сэм Харуэлл,
26

Хотя большинство из вышесказанного является верным, есть один очень важный момент, который не был упомянут в действительности: управление зависимостями.

Go нужно только включить пакеты, которые вы импортируете напрямую (как те, которые уже импортированы, что им нужно). Это резко контрастирует с C / C ++, где каждый отдельный файл начинается с заголовков x, которые включают y заголовков и т. Д. Итог: Компиляция Go занимает линейное время по отношению к количеству импортируемых пакетов, где C / C ++ занимает экспоненциальное время.

Kosta
источник
22

Хорошим тестом для эффективности перевода компилятора является самокомпиляция: сколько времени требуется компилятору для компиляции? Для C ++ это занимает очень много времени (часов?). Для сравнения, компилятор Pascal / Modula-2 / Oberon скомпилирует себя менее чем за один секунду на современной машине [1].

Go был вдохновлен этими языками, но некоторые из основных причин такой эффективности:

  1. Четко определенный синтаксис, который математически обоснован, для эффективного сканирования и анализа.

  2. Безопасный по типу и статически скомпилированный язык, который использует отдельную компиляцию с проверкой зависимостей и типов через границы модуля, чтобы избежать ненужного повторного чтения файлов заголовков и повторной компиляции других модулей - в отличие от независимой компиляции, как в C / C ++, где никакие такие межмодульные проверки не выполняются компилятором (отсюда необходимость перечитывать все эти заголовочные файлы снова и снова, даже для простой однострочной программы "hello world").

  3. Эффективная реализация компилятора (например, однопроходный синтаксический анализ с рекурсивным спуском сверху вниз), чему, конечно, очень помогают пункты 1 и 2 выше.

Эти принципы уже были известны и в полной мере реализованы в 1970-х и 1980-х годах на таких языках, как Mesa, Ada, Modula-2 / Oberon и некоторых других, и только сейчас (в 2010-х) нашли свое отражение в современных языках, таких как Go (Google). , Swift (Apple), C # (Microsoft) и ряд других.

Будем надеяться, что это скоро станет нормой, а не исключением. Чтобы попасть туда, должны произойти две вещи:

  1. Во-первых, поставщики программных платформ, такие как Google, Microsoft и Apple, должны начать с поощрения разработчиков приложений использовать новую методологию компиляции, позволяя им повторно использовать существующую кодовую базу. Это то, что Apple сейчас пытается сделать с языком программирования Swift, который может сосуществовать с Objective-C (поскольку он использует ту же среду выполнения).

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

В любом случае, именно платформа стимулирует принятие языка, а не наоборот.

Ссылки:

[1] http://www.inf.ethz.ch/personal/wirth/ProjectOberon/PO.System.pdf , стр. 6: «Компилятор компилируется примерно за 3 секунды». Эта цитата предназначена для недорогой платы разработки Xilinx Spartan-3 FPGA, работающей на тактовой частоте 25 МГц и имеющей 1 МБайт основной памяти. Исходя из этого, легко экстраполировать до «менее 1 секунды» для современного процессора, работающего на тактовой частоте значительно выше 1 ГГц, и нескольких гигабайт основной памяти (т.е. на несколько порядков более мощных, чем плата FPGA Xilinx Spartan-3), даже с учетом скоростей ввода / вывода. Уже в 1990 году, когда Oberon работал на 25-МГц процессоре NS32X32 с 2-4 МБ основной памяти, компилятор скомпилировал себя всего за несколько секунд. Понятие фактического ожиданиячтобы компилятор заканчивал цикл компиляции, он был совершенно неизвестен программистам Oberon даже тогда. Для типичных программ всегда требовалось больше времени, чтобы убрать палец с кнопки мыши, которая вызвала команду компиляции, чем ждать, пока компилятор завершит компиляцию, только что запущенную. Это было действительно мгновенное удовлетворение с почти нулевым временем ожидания. И качество создаваемого кода, хотя и не всегда полностью на уровне лучших на тот момент компиляторов, было удивительно хорошим для большинства задач и в целом вполне приемлемым.

Andreas
источник
1
Компилятор Pascal / Modula-2 / Oberon / Oberon-2 скомпилирует себя менее чем за одну секунду на современной машине [цитата нужна]
CoffeeandCode
1
Цитирование добавлено, см. Ссылку [1].
Андреас
1
«... принципы ... находят путь к современным языкам, таким как Go (Google), Swift (Apple)» Не знаю, как Swift попал в этот список: компилятор Swift является ледниковым . На недавней встрече CocoaHeads в Берлине кто-то предоставил несколько цифр для платформы среднего размера, они достигли 16 LOC в секунду.
mpw
13

Go был разработан, чтобы быть быстрым, и это показывает.

  1. Управление зависимостями: нет заголовочного файла, вам просто нужно посмотреть на пакеты, которые импортируются напрямую (не нужно беспокоиться о том, что они импортируют), поэтому у вас есть линейные зависимости.
  2. Грамматика: грамматика языка проста, поэтому легко разбирается. Несмотря на то, что количество функций сокращено, сам код компилятора труден (несколько путей).
  3. Перегрузка не допускается: вы видите символ, вы знаете, к какому методу он относится.
  4. Тривиально возможно скомпилировать Go параллельно, потому что каждый пакет может быть скомпилирован независимо.

Обратите внимание, что GO - не единственный язык с такими возможностями (модули являются нормой в современных языках), но они справились хорошо.

Матье М.
источник
Точка (4) не совсем верно. Модули, которые зависят друг от друга, должны быть скомпилированы в порядке зависимости, чтобы обеспечить межмодульное встраивание и прочее.
Fuz
1
@FUZxxl: Это касается только стадии оптимизации, хотя вы можете иметь идеальный параллелизм вплоть до генерации внутреннего интерфейса; Таким образом, речь идет только о межмодульной оптимизации, что может быть сделано на этапе соединения, и в любом случае соединение не является параллельным. Конечно, если вы не хотите дублировать свою работу (повторный анализ), вам лучше компилировать «решетчато»: 1 / модули без зависимости, 2 / модули зависят только от (1), 3 / модули зависит только от (1) и (2), ...
Матье М.
2
Что очень легко сделать с помощью базовых утилит, таких как Makefile.
fuz
12

Цитата из книги « Язык программирования Go » Алана Донована и Брайана Кернигана:

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

негодяй
источник
9

Основная идея компиляции на самом деле очень проста. Парсер рекурсивного спуска, в принципе, может работать со скоростью, связанной с вводом / выводом. Генерация кода в основном очень простой процесс. Таблица символов и система базовых типов не требуют больших вычислений.

Тем не менее, нетрудно замедлить работу компилятора.

Если есть фаза препроцессора, с многоуровневыми включающими директивами, определениями макросов и условной компиляцией, какими бы полезными они ни были, загрузить их несложно. (Например, я имею в виду заголовочные файлы Windows и MFC.) Именно поэтому необходимы предварительно скомпилированные заголовки.

С точки зрения оптимизации сгенерированного кода, нет предела тому, сколько обработки может быть добавлено к этой фазе.

Майк Данлавей
источник
7

Просто (по моим собственным словам), потому что синтаксис очень прост (анализировать и анализировать)

Например, нет наследования типов означает, не проблемный анализ, чтобы выяснить, соответствует ли новый тип правилам, наложенным базовым типом.

Например, в этом примере кода: «интерфейсы» компилятор не проверяет, реализует ли заданный тип данный интерфейс при анализе этого типа. Только до тех пор, пока он не будет использован (и ЕСЛИ он используется), проверка выполняется.

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

Следующее не компилируется:

package main
func main() {
    var a int 
    a = 0
}
notused.go:3: a declared and not used

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

В целом все эти детали упрощают анализ языка, что приводит к быстрой компиляции.

Опять же своими словами.

оборота ОскарРыз
источник
3

Я думаю, что Go был разработан параллельно с созданием компилятора, поэтому они были лучшими друзьями с рождения. (ИМО)

Андрей
источник
0
  • Go импортирует зависимости один раз для всех файлов, поэтому время импорта не увеличивается экспоненциально с размером проекта.
  • Более простая лингвистика означает, что их интерпретация требует меньше вычислений.

Что еще?

Альберто Сальвия Новелла
источник