Ottimizzazione della generazione del codice per le espressioni in un compilatore

6

Non so se questa domanda abbia una risposta semplice o no, ma devo solo chiedere.

Sto leggendo questo tutorial (non so se si tratta di una serie ben nota, ma si tratta di un nome è 'Costruiamo un compilatore!' di Jack Crenshaw). È una serie piacevole in cui impari gradualmente come costruire un compilatore (è una cosa che impara da fare, in realtà).

Già nel secondo capitolo, ho sentito il bisogno di ottimizzare alcune cose. Nella serie, vengono fatte alcune concessioni, per mantenere le cose semplici: non c'è differenza tra lo scanner lessicale (anche se credo che sia introdotto più avanti nella serie) e la generazione del codice: sono fatte in un colpo solo. Inoltre, l'autore fa molto affidamento sullo stack per creare il codice per gestire le espressioni. (Inoltre, l'output è codice assembly, non so se questo è normale per i compilatori, quindi ho pensato di parlarne ...)

Ad esempio, l'analisi dell'espressione 5 + (7 * 3 - 21/3) (e la memorizzazione del risultato in eax) risulta approssimativamente nel seguente codice (io uso x86 asm, al contrario della serie, quindi questo è il codice assembly che il mio compilatore ha dato):

    push 5   ; These lines is generated by a simple number parser
    push 7   ; This parser pushes its result to the stack
    push 3   ; (like most other parsing functions)

    pop eax  ; Code for multiplication
    pop ebx
    mul ebx
    push eax

    push 21  ; Result of parsing these numbers
    push 3

    pop ebx  ;  Division code
    pop eax
    div ebx
    push eax

    pop ebx  ; Subtraction code
    pop eax
    sub eax, ebx
    push eax

    pop eax  ; Addition code
    pop ebx
    add eax, ebx
    push eax

Come puoi vedere, il codice per gestire l'addizione, la sottrazione, la moltiplicazione, ecc. può rimanere lo stesso ogni volta che l'operazione viene utilizzata. Questo è molto conveniente, ma ovviamente il codice assembly generato è molto lento ed esoterico. Quando scrivi a mano, preferisci scrivere qualcosa come:

    mov eax, 21
    div 3
    mov ebx, eax
    mov eax, 7
    mul 3
    sub eax, ebx
    add 5

La mia domanda è se c'è un modo per ottimizzare l'output del compilatore. Certo che c'è, ma c'è un modo specifico per rendere il mio codice più simile al secondo, cioè: non usare lo stack così intensamente, ma affidarsi invece ai registri per passare i valori.

Io stesso ho pensato di analizzare l'intera espressione e rappresentarla internamente come una struttura ad albero (suppongo che sia il tipo di cosa che fa un parser lessicale) prima di generare il codice, ma non sono del tutto sicuro di come procedere. D'altra parte, c'è l'ottimizzazione dello spioncino e la possibilità di ottimizzare il codice assembler dopo che è stato generato, ma non ho idea di quanto sarà efficace. Generare codice migliore in primo luogo sembra logico e più fattibile per me.

So che ci sono molte tecniche disponibili per ottimizzare il mio codice (ad esempio, il folding costante, che ridurrebbe l'intera espressione a 19. Non ho deliberatamente usato questo tipo di ottimizzazione per migliorare il mio punto di vista), spero per una singola tecnica che è adatta per questo tipo di cose.

    
posta Ruben 30.09.2013 - 20:33
fonte

2 risposte

6

Comprendi che il tutorial di Jack Crenshaw riguarda le basi del design del compilatore. Non ha lo scopo di portarti fino alla qualità di produzione in un unico swell. Ci sono altri testi di base disponibili: il vari dragon libri sono famosi. Personalmente mi piace testo di Wirth : è quasi avvicinabile come quello di Crenshaw.

Per generare codice di qualità di produzione, devi fare molto di più. Il riferimento canonico qui è "Progettazione di un compilatore di ottimizzazione " di Wulf. Sorprendentemente, Amazon elenca diverse copie usate disponibili a prezzi ragionevoli.

Si noti inoltre che la generazione di un codice ottimale o persino ottimizzato per l'architettura x86 è molto simile all'allevamento di elefanti: ci vuole un sacco di tempo, un sacco di rumore, un sacco di sforzi, un sacco di muggiti, e c'è un vero rischio di venire schiacciato sotto i piedi.

    
risposta data 30.09.2013 - 22:41
fonte
5

"Costruiamo un compilatore" è una grande serie per insegnare i principi dell'analisi, ma il compilatore output non è il suo punto di forza. Il modo in cui i compilatori reali si occupano di questo è quello di separare il parser dal generatore di codice. Il parser produce un Abstract Syntax Tree ( esempio ), un albero di oggetti in memoria che rappresentano il significato del codice. L'AST viene passato a un generatore di codice, che produce output da esso, ma possono essere applicate ottimizzazioni, sia tra la creazione dell'AST e la generazione del codice o durante la fase di generazione del codice.

Ad esempio, il tuo generatore di codice potrebbe avere una o più variabili membro interne denominate "variabile corrente" che tiene traccia delle ultime variabili su cui è operato il codice, e fintanto che il codice continua a funzionare sullo stesso variabili, non ha bisogno di scrivere i pop e le spinte.

Un esempio di ottimizzazione prima della generazione del codice sarebbe se l'albero contiene un nodo per un'operazione aritmetica, e entrambi gli operandi sono valori costanti, ( 3 * 5 ) il compilatore potrebbe eseguire il calcolo in fase di compilazione e quindi sostituire il nodo con un nodo con valore costante ( 15 ) invece di scrivere codice per eseguire il calcolo.

    
risposta data 30.09.2013 - 22:53
fonte

Leggi altre domande sui tag