Stavo pensando di più al linguaggio di programmazione che sto progettando. e mi stavo chiedendo, quali sono i modi in cui potrei minimizzare il suo tempo di compilazione?come ridurre al minimo il tempo di compilazione del linguaggio di programmazione?
risposta
Ecco un colpo ..
Usa compilazione incrementale se il toolchain lo supporta. (make, visual studio, ecc.).
Ad esempio, in GCC/make, se si hanno molti file da compilare, ma si effettuano solo modifiche in un file, viene compilato solo quel file.
A meno che non sia un file di intestazione. Spesso vale la pena di mettere un po 'di sforzo nel factoring dei file .h in modo che non cambino troppo spesso. Un progetto legacy memorizza inutilmente i dati in un file di intestazione utilizzato ovunque. Non ci sono guadagni di compilazione incrementale lì. – Edmund
Uh, penso che i file di intestazione non siano compilati .. Ad esempio, se si tenta di compilare un file di intestazione in visual C++, si ottiene un messaggio che dice "Nessuno strumento di compilazione associato a questa estensione." Comunque, è vero, ma solo per C/C++, forse .. – krebstar
Quali sono i modi in cui è possibile ridurre al minimo il tempo di compilazione?
- No compilazione (linguaggio interpretato)
- ritardata (just in time) di compilazione
- compilazione incrementale
- file di intestazione precompilata
Abbiamo fatto un test qui con VS6 alcuni anni fa, e il nostro grande progetto con un tempo di compilazione di 15 minuti in realtà ha impiegato circa 6 secondi * più * per compilare con l'intestazione precompilata file attivati. –
Ora che guardo questa risposta, ho problemi anche con gli altri proiettili. Il problema principale è che in realtà non riducono il tempo di compilazione, ma invece lo spostano in un altro posto. Questo * potrebbe * essere quello di cui ha bisogno, ma non è certo quello che ha chiesto. –
Utilizzando VS6 IMO non puoi permetterti di usare 'auto' per l'intestazione precompilata: devi invece dire esattamente quale file * .cpp li genera (ad esempio "stdafx.cpp") e quanti degli header sono precompilati. – ChrisW
dipende da quale lingua/piattaforma' ri programmando per. per lo sviluppo .NET, minimizza il numero di progetti che hai nella tua soluzione.
In passato si potevano ottenere notevoli aumenti di velocità impostando un'unità RAM e compilando lì. Non so se questo è ancora vero, però.
Ho menzionato anche questo nella mia risposta. Sono anche curioso di sapere se qualcuno lo fa ancora. –
In C++ è possibile utilizzare la compilazione distribuito con strumenti come Incredibuild
Eiffel ha avuto un'idea di diversi stati di surgelati, e ricompilando non significava necessariamente che tutta la classe è stato ricompilato.
Quanto puoi dividere i moduli compatibili e quanto ti preoccupi di tenerne traccia?
Nella maggior parte delle lingue (piuttosto bene tutto tranne C++), la compilazione di singole unità di compilazione è abbastanza veloce.
Binding/linking è spesso ciò che è lento - il linker deve fare riferimento all'intero programma anziché solo una singola unità.
C++ soffre come - a meno che non si usi l'idi di pImpl - richiede i dettagli di implementazione di ogni oggetto e tutte le funzioni inline per compilare il codice client.
Java (sorgente al bytecode) soffre perché la grammatica non distingue oggetti e classi - devi caricare la classe Foo per vedere se Foo.Bar.Baz è il campo Baz di oggetto a cui fa riferimento il campo statico Bar di la classe Foo, o un campo statico della classe Foo.Bar. È possibile apportare modifiche alla sorgente della classe Foo tra i due e non modificare la sorgente del codice client, ma è comunque necessario ricompilare il codice client, in quanto il bytecode distingue tra le due forme anche se la sintassi non . Bytecode Python AFAIK non distingue tra i due - i moduli sono veri membri dei loro genitori.
C++ e C soffrono se si includono più intestazioni del necessario, poiché il preprocessore deve elaborare ogni intestazione più volte e compilare il compilatore. Ridurre al minimo le dimensioni e la complessità dell'intestazione aiuta a suggerire una migliore modularità per migliorare i tempi di compilazione.Non è sempre possibile memorizzare nella cache la compilazione dell'intestazione, in quanto le definizioni presenti quando l'intestazione è sottoposta a elaborazione può alterarne la semantica e persino la sintassi.
C soffre se si utilizza molto il preprocessore, ma la compilazione effettiva è veloce; gran parte del codice C utilizza typedef struct _X* X_ptr
per nascondere l'implementazione meglio di C++: un'intestazione C può facilmente consistere in typedef e dichiarazioni di funzioni, offrendo una migliore incapsulamento.
Quindi suggerirei di fare in modo che la lingua nascondesse i dettagli di implementazione dal codice client, e se sei un linguaggio OO con entrambi i membri di istanza e gli spazi dei nomi, fai la sintassi per accedere ai due inequivocabili. Consenti moduli veri, quindi il codice cliente deve solo essere a conoscenza dell'interfaccia anziché dei dettagli di implementazione. Non consentire alle macro del preprocessore o ad altri meccanismi di variazione di alterare la semantica dei moduli di riferimento.
Uno semplice: assicurarsi che il compilatore possa sfruttare in modo nativo le CPU multi-core.
Il tuo problema principale oggi è I/O. La tua CPU è molte volte più veloce della memoria principale e la memoria è circa 1000 volte più veloce dell'accesso al disco rigido.
Quindi, a meno che non si eseguano ottimizzazioni estese al codice sorgente, la CPU impiegherà la maggior parte del tempo in attesa che i dati vengano letti o scritti.
Prova queste regole:
progettare il vostro compilatore di lavorare in diverse fasi, indipendenti. L'obiettivo è essere in grado di eseguire ogni fase in un thread diverso in modo da poter utilizzare CPU multi-core. Aiuterà anche a parallelizzare l'intero processo di compilazione (ad esempio, compila più di un file allo stesso tempo)
Consente inoltre di caricare molti file sorgente in anticipo e di preelaborarli in modo che il passaggio di compilazione effettivo funzioni più rapidamente.
Provare a consentire la compilazione dei file in modo indipendente. Ad esempio, creare un "pool di simboli mancante" per il progetto. I simboli mancanti non dovrebbero causare errori di compilazione in quanto tali. Se trovi un simbolo mancante da qualche parte, rimuovilo dalla piscina. Quando tutti i file sono stati compilati, verificare che il pool sia vuoto.
Creare una cache con informazioni importanti. Ad esempio: il file X utilizza i simboli del file Y. In questo modo, puoi saltare la compilazione del file Z (che non fa riferimento a nulla in Y) quando Y cambia. Se vuoi fare un ulteriore passo avanti, metti tutti i simboli che sono definiti ovunque in un pool. Se un file cambia in modo tale che i simboli vengono aggiunti/rimossi, saprai immediatamente quali file sono interessati (senza nemmeno aprirli).
Compilare in background. Avvia un processo del compilatore che controlla la directory del progetto per le modifiche e le compila non appena l'utente salva il file. In questo modo, dovrai compilare solo alcuni file ogni volta invece di tutto. A lungo termine, si compilerà molto di più, ma per l'utente, i tempi di rotazione saranno molto più brevi (= l'utente deve attendere che possa eseguire il risultato compilato dopo una modifica).
Utilizzare un compilatore "Just in time" (ad esempio, compila un file quando viene utilizzato, ad esempio in una dichiarazione di importazione). I progetti vengono quindi distribuiti in forma sorgente e compilati quando vengono eseguiti per la prima volta. Python fa questo. Per eseguire questa operazione, è possibile precompilare la libreria durante l'installazione del compilatore.
Non utilizzare i file di intestazione. Conservare tutte le informazioni in un unico luogo e generare i file di intestazione dalla fonte, se necessario.Forse mantenere i file di intestazione solo in memoria e non salvarli su disco.
Questo è buono, ma hai tralasciato la cosa più importante: I/O di file. –
@ david.pfx: l'I/O del file è un dato problema. Seguendo le regole si creerà un processo che dipende il meno dall'I/O. –
Come ho detto, la risposta ha molte buone idee, ma se non ti concentri esplicitamente sulla ricerca di modi per accelerare l'I/O, le prestazioni non seguiranno. Per dirla in modo diverso, se il compilatore termina l'I/O rilegato, nessuna delle tecniche intelligenti di threading o caching farà alcuna differenza. Penso che la tua risposta sarebbe migliorata considerando questo punto. –
- Fai la grammatica semplice e non ambiguo, e quindi veloce e facile da analizzare.
- Inserisci forti restrizioni sull'inclusione dei file.
- Consentire la compilazione senza informazioni complete ogniqualvolta possibile (ad esempio predeclaration in C e C++).
- Compilare una sola passata, se possibile.
- assicurarsi che tutto può essere compilato la prima volta si tenta di compilarlo. Per esempio. divieto di riferimenti futuri.
- Utilizzare una grammatica senza contesto in modo che sia possibile trovare l'albero di analisi corretto senza una tabella di simboli.
- Assicurarsi che la semantica possa essere dedotta dalla sintassi in modo da poter costruire direttamente l'AST corretto piuttosto che con un albero di analisi e una tabella dei simboli.
Quanto è serio un compilatore?
A meno che la sintassi non sia abbastanza complessa, il parser dovrebbe essere in grado di eseguire non più di 10-100 volte più lentamente rispetto all'indicizzazione tramite i caratteri del file di input.
Analogamente, la generazione del codice deve essere limitata dalla formattazione dell'output.
Non si dovrebbero riscontrare problemi di prestazioni a meno che non si stia facendo un compilatore grande e serio, in grado di gestire app mega-line con molti file di intestazione.
Quindi è necessario preoccuparsi di intestazioni precompilate, passaggi di ottimizzazione e collegamenti.
Ho implementato un compilatore da solo e ho finito per doverlo vedere una volta che le persone iniziarono a caricarlo in batch centinaia di file sorgente. Ero abbastanza sorpreso di quello che ho scoperto.
Si scopre che la cosa più importante che è possibile ottimizzare non è la grammatica. Neanche il tuo analizzatore lessicale o il tuo parser. Invece, la cosa più importante in termini di velocità è il codice che legge i file sorgente dal disco. I/O su disco sono lenti. Davvero lento. Puoi praticamente misurare la velocità del tuo compilatore per il numero di I/O del disco che esegue.
Così si scopre che la cosa migliore che si può fare per accelerare un compilatore è leggere l'intero file in memoria in un grande I/O, fare tutto il lexing, l'analisi, ecc. Dalla RAM, e poi scrivi il risultato su disco in un grande I/O.
Ho parlato con uno dei responsabili della gestione di Gnat (compilatore Ada di GCC) a proposito di questo, e mi ha detto che in realtà era solito mettere tutto ciò che poteva sui dischi RAM in modo che anche il suo file I/O fosse davvero solo RAM legge e scrive.
Stavo pensando di usare una named pipe invece di un file e altri modi per tenerlo in ram. Non riuscivo a pensare a un modo per migliorare il collegamento. Posso farti mandare per email myusername @ gmail.com ho un sacco di domande per te. –
Non vorrei passare attraverso sforzi erculean prima che tu abbia la possibilità di misurare effettivamente le cose. Ottimizzazione prematura e quant'altro. Basta essere sicuri di rendere il front-end al lexer abbastanza modulare da sostituire con una API di buffer in seguito. –
assolutamente corretto. Per la maggior parte delle lingue il tempo di I/O del file (input e output) domina assolutamente qualsiasi altra cosa tu faccia. Anche il lexer dà un contributo. Tutto il resto è un terzo lontano. –
Una cosa sorprendentemente mancante nelle risposte fino ad ora: farti fare una grammatica context free, ecc. Dai un'occhiata alle lingue disegnate da Wirth come Pascal & Modula-2. Non è necessario reimplementare Pascal, ma il design della grammatica è personalizzato per una rapida compilazione. Poi vedi se riesci a trovare qualche vecchio articolo sui trucchi che Anders ha implementato implementando Turbo Pascal. Suggerimento: guidato da tavola.
Non ho visto molto lavoro svolto per ridurre al minimo il tempo di compilazione. Ma alcune idee vengono in mente:
- Mantenere la grammatica semplice. La grammatica complicata aumenterà il tuo tempo di compilazione.
- Prova a utilizzare il parallelismo, utilizzando GPU multicore o CPU.
- Benchmark un moderno compilatore e scopri quali sono i colli di bottiglia e cosa puoi fare nel tuo compilatore/linguaggio per evitarli.
A meno che non si sta scrivendo un linguaggio altamente specializzata, compilare il tempo non è davvero un problema ..
Fai un sistema di generazione che non succhiare!
C'è una quantità enorme di programmi là fuori con forse 3 file sorgente che richiedono meno di un secondo per essere compilati, ma prima di arrivare a tanto si dovrebbe passare attraverso uno script di automake che impiega circa 2 minuti per controllare cose come il dimensione di un int
. E se vai a compilare qualcos'altro un minuto dopo, ti fa sedere quasi esattamente nella stessa serie di test.
Quindi, a meno che il compilatore è fare le cose terribili per l'utente, come la modifica della dimensione dei suoi int
s o cambiare le implementazioni di funzionalità di base tra le esecuzioni, basta scaricare queste informazioni in un file e far loro ottenere in un secondo posto di 2 minuti.
Ecco alcuni trucchi prestazioni che abbiamo imparato attraverso la misurazione della velocità di compilazione e ciò che colpisce è:
Scrivi un compilatore a due passaggi: caratteri a IR, IR al codice. (E 'più facile scrivere un compilatore -pass tre che va caratteri -> AST -> IR -> codice, ma non è il più veloce.)
Come corollario, non hanno un ottimizzatore; è difficile scrivere un veloce ottimizzatore.
Considera la generazione di un codice byte invece del codice macchina nativo. La macchina virtuale per Lua è un buon modello.
Provare un allocatore di registri a scansione lineare o il semplice allocatore di registro utilizzato da Fraser e Hanson in lcc.
In un semplice compilatore, l'analisi lessicale è spesso il maggior collo di bottiglia delle prestazioni. Se stai scrivendo codice C o C++, usa re2c. Se stai usando un'altra lingua (che troverai molto più piacevole), leggi l'articolo su re2c e applica le lezioni apprese.
Generare codice utilizzando il massimo munch o, eventualmente, iburg.
Sorprendentemente, l'assembler GNU è un collo di bottiglia in molti compilatori. Se puoi generare binari direttamente, fallo. Oppure dai un'occhiata allo New Jersey Machine-Code Toolkit.
Come indicato sopra, progettare la lingua per evitare qualsiasi cosa come
#include
. O non utilizzare file di interfaccia o precompilare i file di interfaccia.Questa tattica riduce drasticamente il burdern sul lexer, che come ho detto è spesso il collo di bottiglia più grande.
IR significa che cosa? sto cercando queste informazioni ora, l'analisi lessicale non è più un problema? Pensavo che l'IO fosse il problema principale (come nelle tue analisi leggi molti file, non l'effettiva complessità). La mia lingua ho intenzione di essere complessa. –
IR significa rappresentazione intermedia. In un semplice compilatore, il lexing è ancora costoso; il ciclo tocca ogni carattere dell'input. Si desidera * entrambi * mantenere le dimensioni di input totali in basso * e * per rendere efficiente il lexing. Una grammatica complessa è OK. –
lo sta ancora progettando .. avere un po 'di pazienza ..;) – p4bl0