Compilatori di qualità LISP / Scheme per competere con C / C ++


8

In teoria, è possibile avere un compilatore Lisp / Scheme in grado di produrre codice in grado di competere con il C compilato, diciamo con un margine del 15-25%?

Nei miei test, ho scoperto che l'attuale raccolto di compilatori (Bigloo, SBCL, Gambit, Chicken, ecc.) È 20-50 volte più lento del codice C equivalente .

L' unico valore anomalo è il compilatore Stalin . Per i programmi semplici, produce file binari equivalenti a C. Tuttavia, ciò che ritengo sospetto è che nessuno degli altri progetti (Bigloo, Chicken, Clozure, ecc.) Abbia tentato di implementare qualsiasi trucco utilizzato da Stalin ("ottimizzazione dell'intero programma", eccetera).

Sono un grande fan di LISP dalla metà degli anni '90 e mi piacerebbe portarlo a bordo in modo che il mio team possa avviare progetti in metà del tempo normalmente impiegando C / C ++ /. NET / etc, ma ... le prestazioni i problemi sono un enorme ostacolo.

Mi chiedo se la mancanza di compilatori LISP di qualità sia dovuta al fatto che non sono stati investiti tempo e denaro seri nell'argomento O se questo semplicemente non è un compito fattibile dato lo stato attuale della tecnologia del compilatore ??


1
Credo che hai provato i compilatori Common Lisp con (declare (optimize ...)), (declare (<type> <var))e (the <type> <expr>)nelle funzioni? Altrimenti non è certo un paragone equo :)
Jakub Lédl

1
Penso che cs.stackexchange.com/questions/842/… risponda a questa domanda.
Kyle Jones,

@KyleJones Lo fa? La mia ipotesi è che con le ottimizzazioni massime, Common Lisp può ottenere entro il margine specificato dall'OP, se non più vicino.
Jakub Lédl,

Cambiare semplicemente il linguaggio di programmazione non farà mai in modo che il tuo team produca quattro volte più codice corretto nello stesso tempo. Ciò che gli studi hanno dimostrato è che i programmatori esperti nella lingua presentano approssimativamente lo stesso numero di righe di codice per unità di tempo per la complessità del problema fisso, indipendentemente dalla lingua. Quindi non otterrai nulla a meno che nella tua area problematica i programmi LISP siano molto più brevi. Un'altra cosa da considerare è che devi far conoscere le persone a LISP per lo sviluppo e la manutenzione. E quelli sono lontani nel mezzo.
vonbrand,

1
Mi sembra che questa domanda richieda più esperienza da programmatore che una risposta scientifica. Pertanto, questo potrebbe essere il sito sbagliato per la domanda.
Raffaello

Risposte:


7

Come notato nei commenti, non è molto esatto affermare che "l'attuale raccolto di compilatori (Bigloo, SBCL, Gambit, Chicken, ecc.) È 20-50 volte più lento del codice C equivalente", senza qualificare come hai testato e cosa hai testato.

Per il mio uso, trovo che per molte cose Gambit e Chicken Scheme sono abbastanza vicini all'equivalente codice 'C' in velocità, con l'attuale versione di Gambit (4.7.3) generalmente più veloce di Chicken (4.9.0.1) ma oltre pre -optimizingil codice di output 'C' (facendo ipotesi sul numero di registri disponibili - presuppone x686 - e forzando l'uso della memoria dello stack per eventuali requisiti di memoria extra, quali decisioni dovrebbero essere lasciate al compilatore 'C' come fa Chicken, il che spesso elimina il requisito per registri extra e combina le fasi di elaborazione) in modo da evitare che il compilatore "C" esegua le proprie ottimizzazioni, portando a piccoli loop molto stretti fino a circa due volte più lenti degli stessi piccoli loop stretti in "C" (o Chicken ); Chicken definisce semplicemente tutte le variabili locali di una determinata funzione che ritiene opportune (utilizzate principalmente immutabilmente) e quindi dipende dal compilatore per ottimizzare la maggior parte di quelle assenti. Il pollo non

EDIT_ADD: ho fatto ulteriori ricerche sulle versioni di Chicken and Gambit-C Scheme e ho trovato quanto segue:

  1. Chicken è più veloce di Gambit per piccoli loop stretti per il motivo sopra (dipende più dal compilatore 'C' per le ottimizzazioni senza fare altrettanto da sé e quindi sfrutta meglio i registri extra x86-64), ma anche perché non include un controllo di manutenzione dello stack "POLL" all'interno dei loop, mentre Gambit include il controllo "POLL" all'interno del loop. Anche quando questo non viene attivato (il solito caso), saranno necessari diversi cicli di clock della CPU per determinare che non è necessario nulla (circa 6 cicli). Un futuro compilatore più intelligente potrebbe vedere che non è necessario eseguire un controllo dello stack quando si è all'interno di un ciclo stretto e non si eseguono operazioni di costruzione dello stack, eseguendolo appena prima o dopo il ciclo e risparmiando questo tempo.

  2. Le macro "C" di Gambit fanno troppo lavoro, come detto, nel definire con precisione come devono essere eseguite le operazioni, in particolare includendo operazioni con dimensioni dello stack fisse, e queste sono probabilmente più difficili da ottimizzare per il compilatore "C"; l'uso più efficace dei registri potrebbe ridurre il tempo di loop stretto di forse 4 cicli, che in combinazione con quanto sopra lo metterebbe nel campo da baseball di Chicken.

  3. Né l'ottimizzazione "lettura / modifica / scrittura" dell'output, per esempio, operazioni vettoriali che sono state modificate sul posto e non producono codice in modo che il compilatore lo faccia. Un backend più intelligente come LLVM quando utilizzato con Haskell fa questo genere di cose. Ciò ridurrebbe l'uso del registro di uno e il tempo di esecuzione utilizzando solo una singola istruzione anziché una lettura separata, il calcolo della modifica e una scrittura nella stessa posizione; questo diventerebbe solo un calcolo della modifica (diciamo un po 'o), quindi una modifica in lettura (| =) scrivi una singola istruzione. Questo potrebbe rendere la velocità più veloce di un ciclo o giù di lì

  4. Entrambi sono digitati dinamicamente e processano i bit "tag" dei dati come parte dei loro dati. Né sono abbastanza intelligenti per i loop stretti da ridurre i tag, eseguire il ciclo "senza tag", quindi aggiungere nuovamente i tag per qualsiasi risultato dal ciclo, né producono codice in cui il compilatore 'C' può vedere per fare questo sebbene combina queste operazioni per alcuni casi. L'ottimizzazione qui potrebbe ridurre i tempi di ciclo di un paio di cicli della CPU o giù di lì, a seconda del ciclo.

  5. I loop 'C' molto stretti possono richiedere circa 3,5 cicli di clock della CPU per loop su una CPU veloce che non ha la velocità di accesso alla cache di memoria (come AMD Bulldozer, che è circa due volte più lento); lo stesso loop in Chicken attualmente richiede circa 6 cicli e Gambit richiede circa 16,9 cicli. Con tutte le ottimizzazioni di cui sopra, non c'è motivo per cui queste implementazioni di Scheme non possano farlo, tuttavia è necessario un po 'di lavoro:

  6. Nel caso di Gambit, il lavoro più duro potrebbe essere il miglioramento dell'analisi del flusso per riconoscere quando non è necessario inserire test "POLL" (vale a dire, questo potrebbe essere guidato dagli interrupt, sebbene il compilatore consenta di disattivare gli interrupt per alcuni usi? ); il miglioramento più semplice sarebbe implementare semplicemente un migliore utilizzo dei registri (es. riconoscere meglio i registri x86-64 anziché l'architettura x686 predefinita). Per entrambi, una migliore analisi del flusso per riconoscere che possono "deselezionare" i dati, in particolare "fixnum", "flonum" e vettori, quindi queste operazioni non sono necessarie all'interno di circuiti stretti e combinando le istruzioni di lettura / modifica / scrittura. Entrambi questi scopi potrebbero essere raggiunti usando un backend migliore come LLVM (non una quantità banale di lavoro, ma entrambi sono già in parte lì).

Conclusione: Chicken è attualmente circa il 50% più lento di 'C' sulla CPU più veloce (non il mio Bulldozer, dove ha circa la stessa velocità a causa della limitazione della cache del codice 'C') e Gambit è circa il 400% più lento (solo circa 125% più lento sul mio lento Bulldozer). Tuttavia, i futuri miglioramenti ai compilatori potrebbero ridurlo in modo che il codice non sia più lento di "C" o entro il margine specificato dall'OP.

Un linguaggio più complesso come Haskell, quando si utilizza il back-end LLVM, prestando particolare attenzione all'uso rigoroso (non un problema con lo schema che è sempre desideroso per impostazione predefinita) e utilizzando strutture dati appropriate (array unboxed ST anziché elenchi per loop stretti; in qualche modo applicabile allo Schema usando i vettori), gira alla stessa velocità di 'C' se il back-end LLVM viene usato con ottimizzazioni complete. Se può farlo, lo stesso può fare Scheme con i miglioramenti del compilatore sopra indicati.

DI NUOVO, non c'è modo che una di queste sia da 20 a 50 volte più lenta se usata con flag di ottimizzazione adeguati. END_EDIT_ADD

Naturalmente, tutti i benchmark vengono invalidati se non si utilizzano le impostazioni di ottimizzazione appropriate per ognuno come si farebbe in un ambiente di produzione ...

Penserei che il compilatore commerciale Chez Scheme sarebbe in campo per quanto riguarda la produzione di output ad alte prestazioni come Gambit e Chicken, poiché essendo commerciale ha sicuramente un "serio tempo e denaro investito in esso".

L'unico modo in cui riesco a far funzionare Gambit o Chicken con una velocità che va da "20 a 50 volte più lenta di" C "è di non utilizzare alcuna impostazione di ottimizzazione, nel qual caso spesso non funzionano molto più velocemente di quanto interpretato nelle loro REPL - 10 volte più lento rispetto all'uso corretto di tali impostazioni.

È possibile che l'OP non abbia testato usando queste impostazioni correttamente?

Se l'OP si preoccupa di chiarire le sue procedure di test, modificherò volentieri questa risposta per mostrare che almeno Gambit e Chicken non devono essere così lenti.


Questo è con i controlli del tipo di runtime disabilitati? Non vorrei farlo nella produzione, poiché rende sfruttabili i bug che in precedenza non lo erano.
Demi,

@Demetri, con la maggior parte dei compilatori di Scheme come Gambit-C, Chicken o Bigloo, si ottiene una velocità tre volte superiore per molti benchmark disabilitando tutti i controlli di runtime, ma il codice non è ancora "20-50 volte più lento" come affermato dalla domanda del PO. In effetti, molti di questi controlli possono essere disabilitati in modo sicuro nel codice di produzione dopo aver verificato la presenza di problemi nel debug senza rischi scrivendo il codice in modo tale che tali controlli siano integrati nel codice solo quando necessario.
GordonBood

@Demetri Posso attestare che lo schema del pollo si trova nel campo di baseball di 1,5-2,5 volte più lentamente di C per il codice di riferimento, se compilato con ottimizzazioni. Ma sì, è orribilmente lento se compilato senza ottimizzazioni. FWIW Ottengo i migliori risultati dall'uso di fixnum-ops, unboxed-object e consenti la compilazione di blocchi, ovvero una migliore analisi statica che porta a migliori ottimizzazioni.
Morten Jensen,

Sono più preoccupato per i controlli di sicurezza in fase di esecuzione - Non vorrei che un bug che non si presentasse nei test fosse un buffer overflow sfruttabile. Ovviamente si potrebbero attivare ottimizzazioni.
Demi,

@Demetri comprensibile. La mia esperienza è che l'overhead dei controlli di runtime dipende molto dal tipo di codice che scrivi. A volte il mio overhead è più di 10 volte quello di correre senza controlli nei miei test.
Morten Jensen,
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.