Il miglior libro per rispondere alla tua domanda sarebbe probabilmente: Cooper e Torczon, "Engineering a Compiler", 2003. Se hai accesso a una biblioteca universitaria dovresti essere in grado di prenderne in prestito una copia.
In un compilatore di produzione come llvm o gcc i progettisti fanno ogni sforzo per mantenere tutti gli algoritmi sotto dove è la dimensione dell'input. Per alcune delle analisi per le fasi di "ottimizzazione", ciò significa che è necessario utilizzare l'euristica anziché produrre un codice veramente ottimale.nO(n2)n
Il lexer è una macchina a stati finiti, quindi nella dimensione dell'input (in caratteri) e produce un flusso di token che viene passato al parser.O ( n )O(n)O(n)
Per molti compilatori per molte lingue il parser è LALR (1) e quindi elabora il flusso di token nel tempo nel numero di token di input. Durante l'analisi devi in genere tenere traccia di una tabella dei simboli, ma, per molte lingue, può essere gestita con una pila di tabelle hash ("dizionari"). Ogni accesso al dizionario è , ma a volte potresti dover camminare nello stack per cercare un simbolo. La profondità della pila è dove è la profondità di annidamento degli ambiti. (Quindi nei linguaggi simili a C, quanti strati di parentesi graffe ci sono dentro.)O ( 1 ) O ( s ) sO(n)O(1)O(s)s
Quindi l'albero di analisi viene in genere "appiattito" in un diagramma di flusso di controllo. I nodi del grafico del flusso di controllo potrebbero essere istruzioni a 3 indirizzi (simili a un linguaggio di assemblaggio RISC) e la dimensione del grafico del flusso di controllo sarà tipicamente lineare rispetto alla dimensione dell'albero di analisi.
Quindi vengono in genere applicati una serie di passaggi per l'eliminazione della ridondanza (eliminazione della sottoespressione comune, movimento del codice invariante del loop, propagazione costante, ...). (Questo è spesso chiamato "ottimizzazione" sebbene raramente ci sia qualcosa di ottimale nel risultato, il vero obiettivo è migliorare il codice il più possibile entro i limiti di tempo e spazio che abbiamo posto sul compilatore.) Ogni fase di eliminazione della ridondanza sarà in genere richiedono prove di alcuni fatti relativi al grafico del flusso di controllo. Queste prove vengono in genere eseguite utilizzando l'analisi del flusso di dati . La maggior parte delle analisi del flusso di dati sono progettate in modo tale da convergere nei passaggi sul diagramma di flusso dove è (approssimativamente parlando) la profondità di annidamento del circuito e un passaggio sul diagramma di flusso richiede tempod O ( n ) nO(d)dO(n)dove è il numero di istruzioni a 3 indirizzi.n
Per ottimizzazioni più sofisticate potresti voler fare analisi più sofisticate. A questo punto inizi a imbatterti in compromessi. Volete che i vostri algoritmi di analisi impieghino molto meno diO(n2)tempo nella dimensione del diagramma di flusso dell'intero programma, ma ciò significa che è necessario fare a meno delle informazioni (e del programma che migliorano le trasformazioni) che potrebbero essere costose da dimostrare. Un classico esempio di questo è l'analisi alias, in cui per alcune coppie di scritture di memoria si desidera dimostrare che le due scritture non possono mai indirizzare la stessa posizione di memoria. (Potresti voler fare un'analisi alias per vedere se potresti spostare un'istruzione sopra l'altra.) Per ottenere informazioni accurate sugli alias potresti aver bisogno di analizzare ogni possibile percorso di controllo attraverso il programma, che è esponenziale nel numero di rami nel programma (e quindi esponenziale nel numero di nodi nel grafico del flusso di controllo).
Successivamente si entra nell'allocazione del registro. L'allocazione dei registri può essere definita come un problema di colorazione del grafico e la colorazione di un grafico con un numero minimo di colori è nota come NP-Hard. Quindi la maggior parte dei compilatori utilizza una sorta di avida euristica combinata con lo spargimento di registri con l'obiettivo di ridurre il numero di spargimenti di registro nel miglior modo possibile entro limiti di tempo ragionevoli.
Finalmente inizi a generare codice. La generazione del codice viene in genere eseguita come un blocco di base massimo in un momento in cui un blocco di base è un insieme di nodi del diagramma di flusso di controllo collegati in modo lineare con una singola entrata e una singola uscita. Questo può essere riformulato come un grafico che copre il problema in cui il grafico che si sta tentando di coprire è il grafico di dipendenza dell'insieme di istruzioni a 3 indirizzi nel blocco di base e si sta tentando di coprire con un insieme di grafici che rappresentano la macchina disponibile Istruzioni. Questo problema è esponenziale nella dimensione del blocco di base più grande (che potrebbe, in linea di principio, essere dello stesso ordine della dimensione dell'intero programma), quindi questo è di nuovo tipicamente fatto con l'euristica in cui solo un piccolo sottoinsieme delle possibili coperture sono esaminato.