Come si compila Go così velocemente?


217

Ho cercato su Google e cercato sul sito Web di Go, ma non riesco a trovare una spiegazione per i straordinari tempi di costruzione di Go. Sono prodotti delle caratteristiche del linguaggio (o della loro mancanza), un compilatore altamente ottimizzato o qualcos'altro? Non sto cercando di promuovere Go; Sono solo curioso.


12
@Supporto, ne sono consapevole. Penso che implementare un compilatore in modo tale da essere compilato con notevole rapidità sia tutt'altro che ottimizzazione prematura. Più che probabile, rappresenta il risultato di buone pratiche di progettazione e sviluppo del software. Inoltre, non sopporto di vedere le parole di Knuth estratte dal contesto e applicate in modo errato.
Adam Crossland,

55
La versione del pessimista di questa domanda è "Perché il C ++ si compila così lentamente?" stackoverflow.com/questions/588884/...
dan04

14
Ho votato per riaprire questa domanda in quanto non è basata sull'opinione pubblica. Si può dare una buona panoramica tecnica (non supponente) della lingua e / o delle scelte del compilatore che facilitano la velocità di compilazione.
Martin Tournoij,

Per i piccoli progetti, Go mi sembra lento. Questo perché ricordo che Turbo-Pascal era molto più veloce su un computer che era probabilmente migliaia di volte più lento. prog21.dadgum.com/47.html?repost=true . Ogni volta che scrivo "go build" e per alcuni secondi non succede nulla, ripenso ai vecchi compilatori Fortran e alle schede perforate. YMMV. TLDR: "lento" e "veloce" sono termini relativi.
RedGrittyBrick,

Consiglio vivamente di leggere dave.cheney.net/2014/06/07/five-things-that-make-go-fast per approfondimenti più dettagliati
Karthik

Risposte:


193

Analisi delle dipendenze.

Le FAQ di Go utilizzate per contenere la seguente frase:

Go fornisce un modello per la costruzione di software che semplifica l'analisi delle dipendenze ed evita gran parte del sovraccarico di file e librerie in stile C.

Mentre la frase non è più nelle FAQ, questo argomento viene approfondito nel talk Go at Google , che confronta l'approccio di analisi delle dipendenze di C / C ++ e Go.

Questa è la ragione principale della compilazione veloce. E questo è di progettazione.


Questa frase non è più nella FAQ di Go, ma una spiegazione più dettagliata dell'argomento "analisi delle dipendenze" che confronta l'approccio C / C ++ e Pascal / Modula / Go è disponibile nel talk Go at Google
rob74,

76

Penso che non sia che i compilatori Go siano veloci , è che altri compilatori sono lenti .

I compilatori C e C ++ devono analizzare enormi quantità di intestazioni - ad esempio, compilare "ciao mondo" in C ++ richiede la compilazione di 18k righe di codice, che è quasi mezzo megabyte di fonti!

$ cpp hello.cpp | wc
  18364   40513  433334

I compilatori Java e C # vengono eseguiti in una macchina virtuale, il che significa che prima di poter compilare qualsiasi cosa, il sistema operativo deve caricare l'intera macchina virtuale, quindi devono essere compilati JIT dal bytecode al codice nativo, il che richiede del tempo.

La velocità di compilazione dipende da diversi fattori.

Alcune lingue sono progettate per essere compilate rapidamente. Ad esempio, Pascal è stato progettato per essere compilato utilizzando un compilatore a passaggio singolo.

Anche i compilatori possono essere ottimizzati. Ad esempio, il compilatore Turbo Pascal è stato scritto in un assemblatore ottimizzato a mano, che, combinato con il design del linguaggio, ha portato a un compilatore molto veloce che lavora su hardware di classe 286. Penso che anche adesso i compilatori Pascal moderni (ad esempio FreePascal) siano più veloci dei compilatori Go.


19
Il compilatore C # di Microsoft non viene eseguito in una macchina virtuale. È ancora scritto in C ++, principalmente per motivi di prestazioni.
blucz,

19
Turbo Pascal e successivamente Delphi sono i migliori esempi per compilatori incredibilmente veloci. Dopo che l'architetto di entrambi è migrato a Microsoft, abbiamo riscontrato enormi miglioramenti sia nei compilatori MS che nelle lingue. Non è una coincidenza casuale.
TheBlastOne,

7
18k righe (18364 per l'esattezza) di codice sono 433334 byte (~ 0,5 MB)
el.pescado

9
Il compilatore C # è stato compilato con C # dal 2011. Solo un aggiornamento nel caso in cui qualcuno lo legga in seguito.
Kurt Koller,

3
Il compilatore C # e il CLR che esegue MSIL generato sono comunque diversi. Sono abbastanza sicuro che il CLR non sia scritto in C #.
jocull,

39

Esistono diversi motivi per cui il compilatore Go è molto più veloce della maggior parte dei compilatori C / C ++:

  • Motivo principale : la maggior parte dei compilatori C / C ++ mostra progetti eccezionalmente cattivi (dal punto di vista della velocità di compilazione). Inoltre, dal punto di vista della velocità di compilazione, alcune parti dell'ecosistema C / C ++ (come gli editor in cui i programmatori scrivono i loro codici) non sono progettate tenendo conto della velocità di compilazione.

  • Motivo principale : la velocità di compilazione rapida è stata una scelta consapevole nel compilatore Go e anche nella lingua Go

  • Il compilatore Go ha un ottimizzatore più semplice rispetto ai compilatori C / C ++

  • A differenza di C ++, Go non ha modelli e funzioni incorporate. Ciò significa che Go non deve eseguire alcuna istanza di modello o funzione.

  • Il compilatore Go genera prima un codice assembly di basso livello e l'ottimizzatore funziona sul codice assembly, mentre in un tipico compilatore C / C ++ i passaggi di ottimizzazione funzionano su una rappresentazione interna del codice sorgente originale. L'overhead aggiuntivo nel compilatore C / C ++ deriva dal fatto che è necessario generare la rappresentazione interna.

  • Il collegamento finale (5l / 6l / 8l) di un programma Go può essere più lento rispetto al collegamento di un programma C / C ++, perché il compilatore Go sta attraversando tutto il codice assembly usato e forse sta anche facendo altre azioni extra che C / C ++ i linker non stanno facendo

  • Alcuni compilatori C / C ++ (GCC) generano istruzioni in formato testo (da passare all'assemblatore), mentre il compilatore Go genera istruzioni in formato binario. È necessario un lavoro extra (ma non molto) per trasformare il testo in binario.

  • Il compilatore Go ha come target solo un numero limitato di architetture CPU, mentre il compilatore GCC ha come target un numero elevato di CPU

  • I compilatori progettati con l'obiettivo di un'elevata velocità di compilazione, come Jikes, sono veloci. Su una CPU da 2 GHz, Jikes può compilare oltre 20000 righe di codice Java al secondo (e la modalità incrementale di compilazione è ancora più efficiente).


17
Il compilatore di Go incorpora piccole funzioni. Non sono sicuro di come il targeting di un piccolo numero di CPU ti renda più veloce ... Suppongo che gcc non stia generando codice PPC mentre sto compilando per x86.
Brad Fitzpatrick,

@BradFitzpatrick odia risuscitare un vecchio commento ma prendendo di mira un numero inferiore di piattaforme gli sviluppatori del compilatore possono dedicare più tempo a ottimizzarlo per ognuno.
Persistenza

l'utilizzo di un modulo intermedio ti consente di supportare molte più architetture poiché ora devi solo scrivere un nuovo backend per ogni nuova architettura
phuclv,

34

L'efficienza della compilazione era un obiettivo di progettazione importante:

Infine, è destinato a essere veloce: dovrebbero essere necessari al massimo alcuni secondi per creare un eseguibile di grandi dimensioni su un singolo computer. Per raggiungere questi obiettivi è stato necessario affrontare una serie di questioni linguistiche: un sistema di tipo espressivo ma leggero; concorrenza e raccolta dei rifiuti; specifica di dipendenza rigida; e così via. FAQ

Le FAQ sulla lingua sono piuttosto interessanti per quanto riguarda le caratteristiche linguistiche specifiche relative all'analisi:

In secondo luogo, la lingua è stata progettata per essere facile da analizzare e può essere analizzata senza una tabella dei simboli.


6
Non è vero. Non è possibile analizzare completamente il codice sorgente di Go senza una tabella dei simboli.

12
Inoltre non vedo perché la garbage collection migliora i tempi di compilazione. Semplicemente no.
TheBlastOne

3
Queste sono citazioni dalle FAQ: golang.org/doc/go_faq.html Non posso dire se non sono riusciti a raggiungere i loro obiettivi (tabella dei simboli) o se la loro logica è errata (GC).
Larry OBrien,

5
@FUZxxl Vai a golang.org/ref/spec#Primary_expressions e considera le due sequenze [Operando, Chiama] e [Conversione]. Esempio Codice sorgente Go: identifier1 (identifier2). Senza una tabella dei simboli è impossibile decidere se questo esempio è una chiamata o una conversione. | Qualsiasi lingua può essere in una certa misura analizzata senza una tabella dei simboli. È vero che la maggior parte delle parti dei codici sorgente Go può essere analizzata senza una tabella dei simboli, ma non è vero che è possibile riconoscere tutti gli elementi grammaticali definiti nelle specifiche del Golang.

3
@Atom Lavorate sodo per evitare che il parser sia mai il pezzo di codice che segnala un errore. I parser in genere fanno un cattivo lavoro nel riportare messaggi di errore coerenti. Qui, si crea un albero di analisi per l'espressione come se aTypefosse un riferimento variabile, e successivamente nella fase di analisi semantica quando si scopre che non si stampa in quel momento un errore significativo.
Sam Harwell,

26

Sebbene la maggior parte di quanto sopra sia vero, c'è un punto molto importante che non è stato veramente menzionato: la gestione delle dipendenze.

Go deve solo includere i pacchetti che si stanno importando direttamente (poiché quelli che hanno già importato ciò di cui hanno bisogno). Ciò è in netto contrasto con C / C ++, in cui ogni singolo file inizia incluse le intestazioni x, che includono le intestazioni y ecc. In conclusione: la compilazione di Go richiede tempo lineare rispetto al numero di pacchetti importati, dove C / C ++ richiede tempo esponenziale.


22

Un buon test per l'efficienza della traduzione di un compilatore è l'auto-compilazione: quanto tempo impiega un determinato compilatore per compilarsi? Per C ++ ci vuole molto tempo (ore?). In confronto, un compilatore Pascal / Modula-2 / Oberon si compilerebbe in meno di un secondo su una macchina moderna [1].

Go è stato ispirato da queste lingue, ma alcuni dei motivi principali di questa efficienza includono:

  1. Una sintassi chiaramente definita che è matematicamente valida, per un'analisi e un'analisi efficienti.

  2. Un linguaggio sicuro per i tipi e compilato staticamente che utilizza una compilazione separata con dipendenze e verifica dei tipi oltre i confini del modulo, per evitare la rilettura non necessaria dei file di intestazione e la ricompilazione di altri moduli, al contrario della compilazione indipendente come in C / C ++ dove il compilatore non esegue tali controlli tra i moduli (da qui la necessità di rileggere ripetutamente tutti quei file di intestazione, anche per un semplice programma "ciao mondo" di una riga).

  3. Un'implementazione efficiente del compilatore (ad es. Analisi top-down a discesa singola, passiva e ricorsiva), che ovviamente è notevolmente aiutata dai precedenti punti 1 e 2.

Questi principi sono già stati conosciuti e pienamente implementati negli anni '70 e '80 in lingue come Mesa, Ada, Modula-2 / Oberon e molti altri, e solo ora (nel 2010) si stanno facendo strada nelle lingue moderne come Go (Google) , Swift (Apple), C # (Microsoft) e molti altri.

Speriamo che questa sarà presto la norma e non l'eccezione. Per arrivarci, devono accadere due cose:

  1. In primo luogo, i fornitori di piattaforme software come Google, Microsoft e Apple dovrebbero iniziare incoraggiando gli sviluppatori di applicazioni a utilizzare la nuova metodologia di compilazione, consentendo loro di riutilizzare la loro base di codice esistente. Questo è ciò che Apple sta ora cercando di fare con il linguaggio di programmazione Swift, che può coesistere con Objective-C (poiché utilizza lo stesso ambiente di runtime).

  2. In secondo luogo, le stesse piattaforme software sottostanti dovrebbero eventualmente essere riscritte nel tempo usando questi principi, riprogettando contemporaneamente la gerarchia dei moduli nel processo per renderli meno monolitici. Questo è ovviamente un compito mastodontico e potrebbe richiedere la parte migliore di un decennio (se sono abbastanza coraggiosi da farlo effettivamente - cosa che non sono affatto sicuro nel caso di Google).

In ogni caso, è la piattaforma che guida l'adozione della lingua e non viceversa.

Riferimenti:

[1] http://www.inf.ethz.ch/personal/wirth/ProjectOberon/PO.System.pdf , pagina 6: "Il compilatore si compila in circa 3 secondi". Questa citazione è per una scheda di sviluppo FPGA Xilinx Spartan-3 a basso costo con frequenza di clock di 25 MHz e 1 MByte di memoria principale. Da questo si può facilmente estrapolare a "meno di 1 secondo" per un moderno processore in esecuzione con una frequenza di clock ben superiore a 1 GHz e diversi GByte di memoria principale (ovvero diversi ordini di grandezza più potenti della scheda FPGA Xilinx Spartan-3), anche quando si tiene conto delle velocità di I / O. Già nel 1990, quando Oberon era in esecuzione su un processore NS32X32 a 25 MHz con 2-4 MByte di memoria principale, il compilatore si compilò da solo in pochi secondi. L'idea di aspettare davveroper il compilatore terminare un ciclo di compilazione era completamente sconosciuto ai programmatori Oberon anche allora. Per i programmi tipici, è sempre stato necessario più tempo per rimuovere il dito dal pulsante del mouse che ha attivato il comando di compilazione piuttosto che attendere che il compilatore completi la compilazione appena attivata. È stata davvero una gratificazione istantanea, con tempi di attesa quasi nulli. E la qualità del codice prodotto, anche se non sempre alla pari con i migliori compilatori disponibili allora, era straordinariamente buona per la maggior parte delle attività e abbastanza accettabile in generale.


1
Un compilatore Pascal / Modula-2 / Oberon / Oberon-2 si compilerebbe da solo in meno di un secondo su una macchina moderna [citazione necessaria]
CoffeeandCode

1
Citazione aggiunta, vedere riferimento [1].
Andreas,

1
"... principi ... trovando la loro strada nei linguaggi moderni come Go (Google), Swift (Apple)" Non sono sicuro di come Swift sia entrato in quella lista: il compilatore Swift è glaciale . In un recente incontro di CocoaHeads a Berlino, qualcuno ha fornito alcuni numeri per un quadro di medie dimensioni, arrivando a 16 LOC al secondo.
mpw,

13

Go è stato progettato per essere veloce e lo dimostra.

  1. Gestione delle dipendenze: nessun file di intestazione, devi solo guardare i pacchetti che sono importati direttamente (non devi preoccuparti di ciò che importano) quindi hai dipendenze lineari.
  2. Grammatica: la grammatica della lingua è semplice, quindi facilmente analizzabile. Sebbene il numero di funzionalità sia ridotto, quindi il codice del compilatore stesso è stretto (pochi percorsi).
  3. Nessun sovraccarico consentito: vedi un simbolo, sai a quale metodo si riferisce.
  4. È banalmente possibile compilare Go in parallelo perché ogni pacchetto può essere compilato in modo indipendente.

Nota che GO non è l'unica lingua con tali caratteristiche (i moduli sono la norma nelle lingue moderne), ma lo hanno fatto bene.


Il punto (4) non è del tutto vero. I moduli che dipendono l'uno dall'altro dovrebbero essere compilati in ordine di dipendenza per consentire l'inserimento e roba tra moduli diversi.
fuz,

1
@FUZxxl: Ciò riguarda solo la fase di ottimizzazione, tuttavia è possibile avere un perfetto parallelismo fino alla generazione IR back-end; è quindi interessata solo l'ottimizzazione tra moduli, che può essere eseguita nella fase di collegamento e il collegamento non è comunque parallelo. Naturalmente, se non si desidera duplicare il proprio lavoro (re-analisi), è meglio compilare in modo "reticolare": 1 / moduli senza dipendenza, 2 / moduli dipendenti solo da (1), 3 / moduli dipende solo da (1) e (2), ...
Matthieu M.

2
Che è perfettamente facile da fare usando le utility di base come un Makefile.
fuz,

12

Citando dal libro " The Go Programming Language " di Alan Donovan e Brian Kernighan:

La compilazione di Go è notevolmente più veloce della maggior parte degli altri linguaggi compilati, anche quando si costruisce da zero. Ci sono tre ragioni principali per la velocità del compilatore. Innanzitutto, tutte le importazioni devono essere esplicitamente elencate all'inizio di ciascun file sorgente, quindi il compilatore non deve leggere ed elaborare un intero file per determinarne le dipendenze. In secondo luogo, le dipendenze di un pacchetto formano un grafico aciclico diretto e poiché non ci sono cicli, i pacchetti possono essere compilati separatamente e forse in parallelo. Infine, il file oggetto per un pacchetto Go compilato registra le informazioni di esportazione non solo per il pacchetto stesso, ma anche per le sue dipendenze. Durante la compilazione di un pacchetto, il compilatore deve leggere un file oggetto per ogni importazione ma non deve guardare oltre questi file.


9

L'idea di base della compilazione è in realtà molto semplice. Un parser a discesa ricorsiva, in linea di principio, può funzionare a velocità legata I / O. La generazione del codice è sostanzialmente un processo molto semplice. Una tabella di simboli e un sistema di tipi di base non sono elementi che richiedono molti calcoli.

Tuttavia, non è difficile rallentare un compilatore.

Se esiste una fase di preprocessore, con direttive multilivello che includono direttive, definizioni di macro e compilazione condizionale, per quanto utili siano queste cose, non è difficile caricarlo. (Per un esempio, sto pensando ai file di intestazione di Windows e MFC.) Ecco perché sono necessarie intestazioni precompilate.

In termini di ottimizzazione del codice generato, non vi è alcun limite alla quantità di elaborazione che può essere aggiunta a quella fase.


7

Semplicemente (con le mie stesse parole), perché la sintassi è molto semplice (da analizzare e da analizzare)

Ad esempio, nessuna ereditarietà di tipo significa, non un'analisi problematica per scoprire se il nuovo tipo segue le regole imposte dal tipo di base.

Ad esempio in questo esempio di codice: "interfacce" il compilatore non va e controlla se il tipo previsto implementa l'interfaccia fornita durante l'analisi di quel tipo. Solo fino a quando non viene utilizzato (e SE viene utilizzato) viene eseguito il controllo.

Altro esempio, il compilatore ti dice se stai dichiarando una variabile e non la stai usando (o se dovresti contenere un valore di ritorno e non lo sei)

Non compila quanto segue:

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

Questo tipo di imposizioni e principi rendono il codice risultante più sicuro e il compilatore non deve eseguire ulteriori convalide che il programmatore può fare.

In generale, tutti questi dettagli facilitano l'analisi di una lingua, il che si traduce in compilazioni veloci.

Ancora una volta, con le mie stesse parole.


3

penso che Go sia stato progettato parallelamente alla creazione del compilatore, quindi erano i migliori amici dalla nascita. (IMO)


0
  • Go importa le dipendenze una volta per tutti i file, quindi il tempo di importazione non aumenta esponenzialmente con la dimensione del progetto.
  • Linguistica più semplice significa che interpretarli richiede meno elaborazione.

Cos'altro?

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.