21

Ho iniziato ad aggiungere chiusure (lambda) alla mia lingua che utilizza LLVM come back-end. Li ho implementati per casi semplici in cui possono essere sempre in linea, ad esempio il codice per la definizione della chiusura stessa non ha bisogno di essere generato, in quanto è sottolineato dove usato.Come implementare in modo efficiente chiusure in LLVM IR?

Ma come generare il codice per una chiusura nel caso in cui la chiusura non sia sempre in linea (ad esempio, viene passata ad un'altra funzione che non è in linea). Preferibilmente, i siti di chiamata non dovrebbero preoccuparsi se sono passati funzioni o chiusure regolari e li chiamerebbero come normali funzioni.

Potrei generare una funzione con un nome sintetico, ma dovrebbe prendere l'ambiente di riferimento come argomento aggiuntivo e quella funzione non può essere passata a un'altra funzione che non conosce l'argomento extra necessario.

Ho pensato ad una possibile soluzione utilizzando gli intrinseci del trampolino di LLVM, che "asportano" un singolo parametro da una funzione, restituendo un puntatore a una funzione trampolino che prende un parametro in meno. In questo caso, se la funzione generata per la chiusura prendesse l'ambiente di riferimento come primo parametro, potrei asportarlo e recuperare una funzione che richiede esattamente quanti parametri come la chiusura dichiara effettivamente. Questo suono è fattibile? Efficiente? Ci sono soluzioni migliori?

Codice esempio:

def applyFunctionTo(value: Int, f: (Int) -> Int) = f(value) 

def main() = { 
    val m := 4; 
    val n := 5; 
    val lambda := { (x: Int) => x + m + n }; 
    applyFunctionTo(3, lambda) 
} 

Ora, lascia immaginare che questo non sarebbe ottenere inline a def main() = 3 + 4 + 5, e che applyFunctionTo sarebbe eventualmente essere compilato separatamente, e non possiamo cambiare il sito di chiamata lì. Con trampolino, immagino che il codice generato sarebbe qualcosa di simile (espresso in pseudocodice, * significa puntatore):

def main$lambda$1(env: {m: Int, n: Int}*, x: Int) = x + env.m + env.n 
def main() = { 
    m = 4 
    n = 5 
    env* = allocate-space-for {Int, Int} 
    env = {m, n} 
    tramp* = create-trampoline-for(main$lambda$1*, env*) 
    return applyFunctionTo(3, tramp*) 
    // release memory for env and trampoline if the lambda didn't escape 
} 

Vi sembra giusto?

+0

Non esiste alcuna differenza tra l'implementazione di chiusure e l'implementazione di oggetti con metodi virtuali. –

+0

È possibile che tu abbia ragione, tuttavia, la lingua non avrà metodi virtuali (ancora). Almeno avrà chiusure e molte altre cose prima. Potrei aggiungere alcune funzionalità in un ordine stupido perché lo sto facendo solo per scopi di apprendimento, soprattutto. Spero solo che alla fine arrivi qualcosa di utile. –

+0

Intendevo che non c'è motivo di inventare qualcosa di nuovo per le chiusure: puoi semplicemente fare la stessa cosa che, come dire, un compilatore C++ sta già facendo.È probabile che sia già la cosa più efficiente da fare. –

risposta

7

Suoni fattibili ed efficienti.

Il modo alternativo, che non ha bisogno di trampolini, è definire il tipo di chiusura come una coppia di puntatore e puntatore a funzione dell'ambiente ovvero puntatore dello stack. Nella convenzione di chiamata C gli argomenti extra vengono ignorati, quindi se si fornisce l'ambiente come ultimo argomento è possibile utilizzare anche (function_ptr, null) come callback per la funzione normale.

+0

Per ora proverò i trampolini, ma sembra che l'alternativa di passare le funzioni come una coppia di puntatori potrebbe effettivamente essere migliore nel caso in cui la maggior parte delle funzioni passate nei programmi siano o chiusure che in realtà hanno variabili libere o metodi di oggetti (dove 'questo' può essere considerato una variabile libera). Non sono sicuro di come esca esattamente la lingua, ma potrei considerare di passare a quella rappresentazione in seguito. –

1

Un'idea stupida sarebbe che per ogni chiusura si genera una struttura locale del thread per contenere i dati richiesti (potrebbe essere solo un puntatore a una struttura locale o più puntatori).

Il creatore della chiusura è responsabile dell'impostazione delle variabili TLS e del "salvataggio" dello stato (per consentire la chiamata ricorsiva).

L'utente chiama quindi la funzione normalmente, viene eseguita e utilizza l'ambiente.

Dopo la chiamata, il creatore della chiusura "ripristina" i valori originali nelle variabili TLS.