Jak Go kompiluje się tak szybko?

217

Przejrzałem Google i przeszukałem witrynę Go, ale nie mogę znaleźć wytłumaczenia dla niezwykłych czasów kompilacji Go. Czy są to produkty funkcji językowych (lub ich brak), wysoce zoptymalizowany kompilator, czy coś innego? Nie próbuję promować Go; Jestem po prostu ciekawy.

Evan Kroske
źródło
12
@ Wsparcie, jestem tego świadomy. Myślę, że implementacja kompilatora w taki sposób, że kompiluje się z zauważalną szybkością, jest niczym innym jak przedwczesną optymalizacją. Jest bardziej niż prawdopodobne, że stanowi wynik dobrych praktyk w zakresie projektowania i opracowywania oprogramowania. Ponadto nie mogę znieść widoku słów Knutha wyjętych z kontekstu i zastosowanych nieprawidłowo.
Adam Crossland
55
Wersja tego pytania pesymisty brzmi: „Dlaczego C ++ kompiluje się tak wolno?” stackoverflow.com/questions/588884/…
dan04
14
Głosowałem za ponownym otwarciem tego pytania, ponieważ nie jest ono oparte na opiniach. Można podać dobry techniczny (nieoceniony) przegląd wyboru języka i / lub kompilatora, który przyspiesza kompilację.
Martin Tournoij,
W przypadku małych projektów Go wydaje mi się powolny. To dlatego, że pamiętam, że Turbo-Pascal był znacznie szybszy na komputerze, który był prawdopodobnie tysiące razy wolniejszy. prog21.dadgum.com/47.html?repost=true . Za każdym razem, gdy wpisuję „go build” i przez kilka sekund nic się nie dzieje, wracam myślami do starych, skorumpowanych kompilatorów Fortrana i kart perforowanych. YMMV. TLDR: „wolny” i „szybki” są względnymi terminami.
RedGrittyBrick
Zdecydowanie polecam przeczytanie dave.cheney.net/2014/06/07/five-things-that-make-go-fast, aby uzyskać bardziej szczegółowe informacje
Karthik

Odpowiedzi:

193

Analiza zależności.

Go FAQ wykorzystywane zawierać następujące zdanie:

Go zapewnia model konstrukcji oprogramowania, który ułatwia analizę zależności i pozwala uniknąć dużych kosztów związanych z plikami typu C i bibliotekami.

Chociaż tego wyrażenia nie ma już w FAQ, ten temat jest rozwinięty w wykładzie Go w Google , który porównuje podejście do analizy zależności C / C ++ i Go.

To jest główny powód szybkiej kompilacji. I to jest z założenia.

Igor Krivokon
źródło
To wyrażenie nie jest już w Go FAQ, ale bardziej szczegółowe wyjaśnienie tematu „analizy zależności” porównującego podejście C / C ++ i Pascal / Modula / Go jest dostępne w
wykładzie
76

Myślę, że to nie jest tak, że kompilatory Go są szybkie , tylko że inne kompilatory działają wolno .

Kompilatory C i C ++ muszą analizować ogromne ilości nagłówków - na przykład kompilacja „hello world” C ++ wymaga skompilowania 18 000 wierszy kodu, co stanowi prawie pół megabajta źródeł!

$ cpp hello.cpp | wc
  18364   40513  433334

Kompilatory Java i C # działają na maszynie wirtualnej, co oznacza, że ​​zanim cokolwiek będą mogły kompilować, system operacyjny musi załadować całą maszynę wirtualną, a następnie muszą zostać skompilowane z kodu bajtowego na kod macierzysty, co zajmuje trochę czasu.

Szybkość kompilacji zależy od kilku czynników.

Niektóre języki są zaprojektowane do szybkiej kompilacji. Na przykład Pascal został zaprojektowany do kompilacji przy użyciu kompilatora jednoprzebiegowego.

Same kompilatory można również zoptymalizować. Na przykład kompilator Turbo Pascal został napisany w asemblerze zoptymalizowanym ręcznie, co w połączeniu z projektem językowym zaowocowało naprawdę szybkim kompilatorem działającym na sprzęcie klasy 286. Myślę, że nawet teraz nowoczesne kompilatory Pascal (np. FreePascal) są szybsze niż kompilatory Go.

el.pescado
źródło
19
Kompilator C # firmy Microsoft nie działa na maszynie wirtualnej. Nadal jest napisany w C ++, głównie ze względu na wydajność.
blucz
19
Turbo Pascal i późniejsze Delphi to najlepsze przykłady niesamowicie szybkich kompilatorów. Po migracji architekta obu do Microsoft, zauważyliśmy znaczną poprawę zarówno w kompilatorach MS, jak i językach. To nie przypadek.
TheBlastOne,
7
18 tys. Wierszy (dokładnie 18364) kodu to 433334 bajtów (~ 0,5 MB)
el.pescado 28.04.15
9
Kompilator C # jest kompilowany z C # od 2011 roku. Tylko aktualizacja na wypadek, gdyby ktoś przeczytał to później.
Kurt Koller,
3
Kompilator C # i CLR, który uruchamia wygenerowany MSIL, to jednak różne rzeczy. Jestem całkiem pewien, że CLR nie jest napisany w języku C #.
jocull
39

Istnieje wiele powodów, dla których kompilator Go jest znacznie szybszy niż większość kompilatorów C / C ++:

  • Główny powód : większość kompilatorów C / C ++ wykazuje wyjątkowo złe projekty (z perspektywy prędkości kompilacji). Ponadto, z perspektywy prędkości kompilacji, niektóre części ekosystemu C / C ++ (takie jak edytory, w których programiści piszą swoje kody) nie są zaprojektowane z myślą o szybkości kompilacji.

  • Główny powód : Szybka kompilacja była świadomym wyborem w kompilatorze Go, a także w języku Go

  • Kompilator Go ma prostszy optymalizator niż kompilatory C / C ++

  • W przeciwieństwie do C ++ Go nie ma szablonów ani funkcji wbudowanych. Oznacza to, że Go nie musi wykonywać żadnej instancji szablonu ani funkcji.

  • Kompilator Go generuje kod asemblera wcześniej, a optymalizator działa na kod asemblera, podczas gdy w typowym kompilatorze C / C ++ optymalizacja przekazuje pracę na wewnętrznej reprezentacji oryginalnego kodu źródłowego. Dodatkowy narzut w kompilatorze C / C ++ wynika z faktu, że należy wygenerować wewnętrzną reprezentację.

  • Końcowe linkowanie (5l / 6l / 8l) programu Go może być wolniejsze niż linkowanie programu C / C ++, ponieważ kompilator Go przechodzi przez cały użyty kod asemblera i może wykonuje także inne dodatkowe czynności niż C / C ++ linkery nie działają

  • Niektóre kompilatory C / C ++ (GCC) generują instrukcje w formie tekstowej (przekazywane do asemblera), podczas gdy kompilator Go generuje instrukcje w formie binarnej. Należy wykonać dodatkową pracę (ale niewiele), aby przekształcić tekst w plik binarny.

  • Kompilator Go jest ukierunkowany tylko na niewielką liczbę architektur CPU, podczas gdy kompilator GCC jest ukierunkowany na dużą liczbę procesorów

  • Kompilatory zaprojektowane z myślą o wysokiej prędkości kompilacji, takie jak Jikes, są szybkie. Na procesorze 2GHz Jikes może skompilować ponad 20000 linii kodu Java na sekundę (a przyrostowy tryb kompilacji jest jeszcze bardziej wydajny).

użytkownik811773
źródło
17
Kompilator Go wbudowuje małe funkcje. Nie jestem pewien, jak kierowanie na małą liczbę procesorów powoduje, że jesteś wolniejszy ... Zakładam, że gcc nie generuje kodu PPC podczas kompilacji dla x86.
Brad Fitzpatrick
@BradFitzpatrick nienawidzi wskrzeszenia starego komentarza, ale celując w mniejszą liczbę platform, programiści kompilatora mogą poświęcić więcej czasu na jego optymalizację.
Trwałość
użycie formularza pośredniego pozwala na obsługę znacznie większej liczby architektur, ponieważ teraz wystarczy napisać nowy backend dla każdej nowej architektury
phuclv
34

Wydajność kompilacji była głównym celem projektu:

Wreszcie, ma być szybki: zbudowanie dużego pliku wykonywalnego na pojedynczym komputerze powinno zająć najwyżej kilka sekund. Aby osiągnąć te cele, konieczne było rozwiązanie szeregu problemów językowych: wyrazisty, ale lekki system pisma; współbieżność i wyrzucanie elementów bezużytecznych; sztywna specyfikacja zależności; i tak dalej. FAQ

Często zadawane pytania dotyczące języka są dość interesujące w odniesieniu do określonych funkcji językowych związanych z analizą:

Po drugie, język został zaprojektowany tak, aby był łatwy do analizy i można go analizować bez tablicy symboli.

Larry OBrien
źródło
6
To nieprawda. Nie można w pełni przeanalizować kodu źródłowego Go bez tabeli symboli.
12
Nie rozumiem też, dlaczego odśmiecanie poprawia czasy kompilacji. Po prostu nie.
TheBlastOne,
3
Oto cytaty z FAQ: golang.org/doc/go_faq.html Nie mogę powiedzieć, czy nie udało im się osiągnąć swoich celów (tablica symboli) lub czy ich logika jest wadliwa (GC).
Larry OBrien
5
@FUZxxl Przejdź do golang.org/ref/spec#Primary_expressions i rozważ dwie sekwencje [Operand, Call] i [Conversion]. Przykład Go kod źródłowy: identyfikator1 (identyfikator2). Bez tablicy symboli nie można zdecydować, czy ten przykład jest wywołaniem czy konwersją. | Dowolny język można w pewnym stopniu przeanalizować bez tablicy symboli. Prawdą jest, że większość części kodów źródłowych Go można analizować bez tablicy symboli, ale nie jest prawdą, że można rozpoznać wszystkie elementy gramatyczne zdefiniowane w specyfikacji golang.
3
@Atom Ciężko pracujesz, aby parser nie był nigdy fragmentem kodu, który zgłasza błąd. Analizatory składni zwykle źle wykonują raportowanie spójnych komunikatów o błędach. Tutaj tworzysz drzewo parsowania dla wyrażenia, tak jakby aTypeto było odwołanie do zmiennej, a później w fazie analizy semantycznej, kiedy okazuje się, że nie drukujesz znaczącego błędu w tym czasie.
Sam Harwell,
26

Chociaż większość powyższych stwierdzeń jest prawdziwa, istnieje jedna bardzo ważna kwestia, o której tak naprawdę nie wspomniano: zarządzanie zależnościami.

Go musi tylko dołączyć pakiety, które importujesz bezpośrednio (ponieważ te już zaimportowały to, czego potrzebują). Jest to wyraźny kontrast w stosunku do C / C ++, gdzie każdy pojedynczy plik zaczyna się od x nagłówków, które zawierają y nagłówki itp. Konkluzja: Kompilacja Go zajmuje liniowy czas wrt do liczby importowanych pakietów, gdzie C / C ++ zajmuje wykładniczy czas.

Kosta
źródło
22

Dobrym sprawdzianem wydajności tłumaczenia kompilatora jest samokompilacja: jak długo kompiluje się dany kompilator? W przypadku C ++ zajmuje to bardzo dużo czasu (godziny?). Dla porównania, kompilator Pascal / Modula-2 / Oberon by opracować się w ciągu mniej niż jednej sekundy na nowoczesne maszyny [1].

Języki Go zostały zainspirowane tymi językami, ale niektóre z głównych przyczyn tej wydajności obejmują:

  1. Jasno zdefiniowana składnia, która jest matematycznie poprawna, dla wydajnego skanowania i analizowania.

  2. Bezpieczny i statycznie skompilowany język, który wykorzystuje osobną kompilację z kontrolą zależności i typu między granicami modułów, aby uniknąć niepotrzebnego ponownego odczytu plików nagłówkowych i ponownej kompilacji innych modułów - w przeciwieństwie do niezależnej kompilacji, takiej jak w C / C ++, gdzie kompilator nie przeprowadza takich kontroli między modułami (stąd potrzeba ponownego czytania wszystkich plików nagłówkowych w kółko, nawet w przypadku prostego, jednoliniowego programu „hello world”).

  3. Wydajna implementacja kompilatora (np. Parsowanie z góry na dół w trybie pojedynczego przejścia, z rekurencyjnym opadaniem) - co oczywiście w dużym stopniu pomaga punkt 1 i 2 powyżej.

Zasady te były już znane i w pełni wdrożone w latach 70. i 80. XX wieku w językach takich jak Mesa, Ada, Modula-2 / Oberon i kilka innych, a dopiero teraz (w 2010 r.) Trafiają do nowoczesnych języków, takich jak Go (Google) , Swift (Apple), C # (Microsoft) i kilka innych.

Miejmy nadzieję, że wkrótce będzie to norma, a nie wyjątek. Aby się tam dostać, muszą się zdarzyć dwie rzeczy:

  1. Po pierwsze, dostawcy platform oprogramowania, tacy jak Google, Microsoft i Apple, powinni zacząć od zachęcania twórców aplikacji do korzystania z nowej metodologii kompilacji, jednocześnie umożliwiając im ponowne wykorzystanie istniejącej bazy kodu. Właśnie to Apple próbuje teraz zrobić z językiem programowania Swift, który może współistnieć z Objective-C (ponieważ używa tego samego środowiska wykonawczego).

  2. Po drugie, same platformy oprogramowania powinny ostatecznie zostać z czasem przepisane przy użyciu tych zasad, jednocześnie przeprojektowując hierarchię modułów w tym procesie, aby stały się mniej monolityczne. Jest to oczywiście ogromne zadanie i może zająć większą część dekady (jeśli są wystarczająco odważni, by to zrobić - czego wcale nie jestem pewien w przypadku Google).

W każdym razie to platforma napędza adopcję języka, a nie na odwrót.

Bibliografia:

[1] http://www.inf.ethz.ch/personal/wirth/ProjectOberon/PO.System.pdf , strona 6: „Kompilator kompiluje się w około 3 sekundy”. Ten cytat dotyczy taniej płytki rozwojowej FPGA Xilinx Spartan-3 działającej na częstotliwości taktowania 25 MHz i posiadającej 1 MB pamięci głównej. Z tego można łatwo ekstrapolować na „mniej niż 1 sekundę” dla nowoczesnego procesora pracującego na częstotliwości taktowania znacznie powyżej 1 GHz i kilku GB głównej pamięci (tj. Kilka rzędów wielkości większej mocy niż karta FPGA Xilinx Spartan-3), nawet biorąc pod uwagę prędkości we / wy. Już w 1990 r., Kiedy Oberon był uruchomiony na procesorze NS32X32 25 MHz z 2-4 MB pamięci głównej, kompilator skompilował się w kilka sekund. Pojęcie faktycznego czekaniakompilator kończący cykl kompilacji był zupełnie nieznany programistom Oberona już wtedy. W przypadku typowych programów usuwanie palca z przycisku myszy, który wywołał polecenie kompilacji , zawsze trwało dłużej niż oczekiwanie na zakończenie kompilacji. To była naprawdę natychmiastowa satysfakcja z niemal zerowym czasem oczekiwania. A jakość wytworzonego kodu, choć nie zawsze w pełni porównywalna z najlepszymi dostępnymi wówczas kompilatorami, była wyjątkowo dobra dla większości zadań i ogólnie akceptowalna.

Andreas
źródło
1
Kompilator Pascal / Modula-2 / Oberon / Oberon-2 skompilowałby się w niecałą sekundę na nowoczesnej maszynie [potrzebne źródło]
CoffeeandCode
1
Cytowanie dodane, patrz odniesienie [1].
Andreas
1
„... zasady… znajdując drogę do nowoczesnych języków, takich jak Go (Google), Swift (Apple)” Nie jestem pewien, jak Swift trafił na tę listę: kompilator Swift jest lodowaty . Na ostatnim spotkaniu CocoaHeads Berlin ktoś podał kilka liczb dla średniej wielkości frameworka, osiągnął 16 LOC na sekundę.
mpw
13

Go został zaprojektowany, aby być szybki i to pokazuje.

  1. Zarządzanie zależnościami: brak pliku nagłówka, wystarczy spojrzeć na pakiety, które są bezpośrednio importowane (nie musisz się martwić o to, co importują), więc masz zależności liniowe.
  2. Gramatyka: gramatyka języka jest prosta, dlatego łatwo ją przeanalizować. Chociaż liczba funkcji jest zmniejszona, sam kod kompilatora jest napięty (kilka ścieżek).
  3. Niedozwolone przeciążenie: widzisz symbol, wiesz, do której metody się odnosi.
  4. Równolegle można kompilować Go równolegle, ponieważ każdy pakiet można skompilować niezależnie.

Pamiętaj, że GO nie jest jedynym językiem z takimi funkcjami (moduły są normą w nowoczesnych językach), ale zrobiły to dobrze.

Matthieu M.
źródło
Punkt (4) nie jest do końca prawdziwy. Moduły, które od siebie zależą, powinny być kompilowane w kolejności zależności, aby umożliwić wstawianie między modułami i takie tam.
fuz
1
@FUZxxl: Dotyczy to tylko etapu optymalizacji, możesz mieć idealną równoległość aż do generacji IR zaplecza; dotyczy to tylko optymalizacji między modułami, co można zrobić na etapie połączenia, a połączenie i tak nie jest równoległe. Oczywiście, jeśli nie chcesz powielać swojej pracy (ponowne parsowanie), lepiej skompiluj w sposób „kratowy”: 1 / moduły bez zależności, 2 / moduły zależne tylko od (1), 3 / moduły zależnie tylko od (1) i (2), ...
Matthieu M.
2
Jest to całkowicie łatwe do wykonania przy użyciu podstawowych narzędzi, takich jak Makefile.
fuz
12

Cytując z książki „ The Go Programming Language ” Alana Donovana i Briana Kernighana:

Kompilacja Go jest zauważalnie szybsza niż w większości innych skompilowanych języków, nawet przy tworzeniu od zera. Są trzy główne przyczyny szybkości kompilatora. Po pierwsze, wszystkie importy muszą być wyraźnie wymienione na początku każdego pliku źródłowego, aby kompilator nie musiał czytać i przetwarzać całego pliku w celu ustalenia jego zależności. Po drugie, zależności pakietu tworzą ukierunkowany wykres acykliczny, a ponieważ nie ma cykli, pakiety można kompilować osobno i być może równolegle. Wreszcie plik obiektowy skompilowanego pakietu Go zapisuje informacje o eksporcie nie tylko dla samego pakietu, ale także dla jego zależności. Podczas kompilowania pakietu kompilator musi czytać jeden plik obiektowy dla każdego importu, ale nie musi wychodzić poza te pliki.

Niegodziwiec
źródło
9

Podstawowa idea kompilacji jest w rzeczywistości bardzo prosta. Parser rekurencyjno-opadający w zasadzie może działać z prędkością związaną z I / O. Generowanie kodu jest w zasadzie bardzo prostym procesem. Tablica symboli i podstawowy system typów nie wymagają wiele obliczeń.

Spowolnienie kompilatora nie jest jednak trudne.

Jeśli istnieje faza preprocesor, z wielopoziomowego obejmują dyrektyw, definicje makr i kompilacja warunkowa, tak przydatne, jak te rzeczy są, to nie jest trudne, aby załadować go w dół. (Na przykład myślę o plikach nagłówkowych Windows i MFC.) Dlatego właśnie prekompilowane nagłówki są potrzebne.

Jeśli chodzi o optymalizację generowanego kodu, nie ma ograniczeń co do ilości przetwarzania, które można dodać do tej fazy.

Mike Dunlavey
źródło
7

Po prostu (własnymi słowami), ponieważ składnia jest bardzo łatwa (do analizy i parsowania)

Na przykład brak dziedziczenia typu oznacza bezproblemową analizę, aby dowiedzieć się, czy nowy typ jest zgodny z regułami narzuconymi przez typ podstawowy.

Na przykład w tym przykładzie kodu: „interfejsy” kompilator nie idzie i sprawdza, czy zamierzony typ implementuje dany interfejs podczas analizy tego typu. Tylko do momentu użycia (i JEŻELI zostanie użyty) kontrola jest wykonywana.

Innym przykładem jest kompilator informujący, czy deklarujesz zmienną i jej nie używasz (lub czy powinieneś zachować wartość zwracaną, a nie jesteś)

Następujące elementy się nie kompilują:

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

Tego rodzaju egzekwowania i zasady sprawiają, że powstały kod jest bezpieczniejszy, a kompilator nie musi wykonywać dodatkowych weryfikacji, które może zrobić programista.

Zasadniczo wszystkie te szczegóły ułatwiają analizowanie języka, co powoduje szybkie kompilacje.

Ponownie, własnymi słowami.

OscarRyz
źródło
3

myślę, że Go został zaprojektowany równolegle z tworzeniem kompilatora, więc byli najlepszymi przyjaciółmi od urodzenia. (IMO)

Andrey
źródło
0
  • Idź importuje zależności raz dla wszystkich plików, więc czas importu nie rośnie wykładniczo wraz z rozmiarem projektu.
  • Prostsza lingwistyka oznacza, że ​​ich interpretacja wymaga mniejszej ilości obliczeń.

Co jeszcze?

Alberto Salvia Novella
źródło