Compilatori come Javac rilevano automaticamente le funzioni pure e le parallelizzano?


12

Le funzioni pure sono conosciute per facilitare la parellizzazione. Cosa c'è nella programmazione funzionale che lo rende intrinsecamente adattato all'esecuzione parallela?

Compilatori come Javac sono abbastanza intelligenti da rilevare quando un metodo è una funzione pura? Si possono sempre implementare classi che implementano interfacce funzionali come Function , ma hanno effetti collaterali.


7
La domanda non è solo se il compilatore può sapere se una funzione è pura, ma anche se può pianificare in modo intelligente l'esecuzione parallela di funzioni pure. Non è sufficiente sparare un nuovo thread per ognuno: questo è inefficiente. GHC (Haskell) si occupa di questo usando la pigrizia e i "fili verdi"; Sarei onestamente sorpreso se ci provasse un linguaggio impuro, data l'ulteriore difficoltà di assicurarsi che i thread puri fossero programmati correttamente rispetto al thread impuro principale.
Ryan Reich,

@RyanReich, ci sono vantaggi in termini di prestazioni nell'uso della programmazione funzionale in un linguaggio funzionale impuro come Java? i vantaggi della programmazione funzionale sono puramente funzionali come la modularità?
Naveen,

@RyanReich GHC affronta il problema facendo annotare il programmatore quando vogliono il parallelismo. La purezza implica che queste annotazioni non cambiano mai la semantica, solo le prestazioni. (Esistono anche meccanismi di concorrenza che possono dare origine al parallelismo, ma questo è un diverso bollitore di pesci.)
Derek Elkins lasciò SE

@Naveen Ci sono altri vantaggi nelle funzioni pure per quanto riguarda l'ottimizzazione oltre al parallelismo come il maggiore codice di riordino della libertà, la memoizzazione e l'eliminazione della sottoespressione comune. Potrei sbagliarmi, ma dubito che javac tenti di rilevare la purezza, poiché è probabilmente abbastanza raro nel codice idiomatico e un po 'difficile per tutti tranne i casi più banali. Ad esempio, devi sapere che non ci saranno NullPointerExceptions. I vantaggi delle ottimizzazioni basate su questo sono probabilmente anche relativamente piccoli per le tipiche applicazioni Java.
Derek Elkins lasciò SE il

6
javac è il compilatore java, che accetta il codice sorgente java e genera file di classe del codice byte java. È piuttosto limitato su ciò che può (e dovrebbe) fare. Non ha la libertà o i meccanismi sottostanti necessari per introdurre il parallelismo nel file di classe del codice byte.
Erik Eidt,

Risposte:


33

sono compilatori come Javac abbastanza intelligenti da rilevare quando un metodo è una funzione pura.

Non è una questione di "abbastanza intelligente". Questo si chiama Analisi della Purezza ed è dimostrabilmente impossibile nel caso generale: equivale a risolvere il problema dell'arresto.

Ora, naturalmente, gli ottimizzatori fanno continuamente cose impossibili, "dimostrabilmente impossibili nel caso generale" non significa che non funzioni mai, significa solo che non possono funzionare in tutti i casi. Quindi, ci sono infatti algoritmi per verificare se una funzione è pura o no, è solo che il più delle volte il risultato sarà "Non lo so", il che significa che per motivi di sicurezza e correttezza, devi assumere che questa particolare funzione potrebbe essere impura.

E anche nei casi in cui si fa il lavoro, gli algoritmi sono complessi e costosi.

Quindi, questo è il problema n. 1: funziona solo per casi speciali .

Problema n. 2: librerie . Affinché una funzione sia pura, può sempre e solo chiamare funzioni pure (e quelle funzioni possono solo chiamare funzioni pure, e così via e così via). Javac ovviamente conosce solo Java e conosce solo il codice che può vedere. Quindi, se la tua funzione chiama una funzione in un'altra unità di compilazione, non puoi sapere se è pura o no. Se chiama una funzione scritta in un'altra lingua, non puoi saperlo. Se chiama una funzione in una libreria che potrebbe non essere ancora installata, non puoi saperlo. E così via.

Funziona solo quando si ha l'analisi dell'intero programma, quando l'intero programma è scritto nella stessa lingua e tutto viene compilato in una volta sola. Non puoi usare nessuna biblioteca.

Problema n. 3: programmazione . Una volta che hai capito quali parti sono pure, devi ancora programmarle per separare i thread. O no. L'avvio e l'arresto dei thread è molto costoso (specialmente in Java). Anche se si mantiene un pool di thread e non si avvia o si interrompe, anche il cambio di contesto del thread è costoso. È necessario essere sicuri che il calcolo verrà eseguito in modo significativamente più lungo del tempo necessario per la pianificazione e il cambio di contesto, altrimenti si perderanno prestazioni, non si ottiene.

Come probabilmente hai già indovinato, capire quanto tempo impiegherà un calcolo è dimostrabilmente impossibile nel caso generale (non possiamo nemmeno capire se ci vorrà un tempo finito, figuriamoci quanto tempo) e difficile e costoso anche in il caso speciale.

A parte: Javac e ottimizzazioni . Nota che la maggior parte delle implementazioni di javac in realtà non esegue molte ottimizzazioni. L'implementazione di Oracle di javac, ad esempio, si basa sul motore di esecuzione sottostante per eseguire ottimizzazioni . Questo porta ad un altro insieme di problemi: diciamo, javac ha deciso che una particolare funzione è pura ed è abbastanza costosa, e quindi la compila per essere eseguita su un thread diverso. Quindi, arriva l'ottimizzatore della piattaforma (ad esempio, il compilatore JIT HotSpot C2) e ottimizza l'intera funzione. Ora, hai un thread vuoto che non fa nulla. Oppure, immagina, ancora una volta, javac decide di pianificare una funzione su un thread diverso e l'ottimizzatore della piattaforma potrebbe farlo ottimizzarlo completamente, tranne per il fatto che non può eseguire l'allineamento oltre i limiti del thread, quindi una funzione che potrebbe essere ottimizzata completamente è ora inutilmente eseguita.

Quindi, fare una cosa del genere ha senso solo se si dispone di un singolo compilatore che fa la maggior parte delle ottimizzazioni in una volta sola, in modo che il compilatore sia a conoscenza e possa sfruttare tutte le diverse ottimizzazioni a diversi livelli e le loro interazioni tra loro.

Si noti che, ad esempio, il compilatore JIT HotSpot C2 esegue effettivamente una vettorializzazione automatica, che è anche una forma di parallelizzazione automatica.


Bene, a seconda della definizione di "funzione pura", può essere consentito l'uso di funzioni impure nell'implementazione.
Deduplicatore

@Deduplicator Beh, a seconda della tua definizione di definition, l'uso di un disparate definitiondi purityè probabilmente oscuro
cat

1
Il tuo problema n. 2 è per lo più invalidato dal fatto che praticamente tutte le ottimizzazioni vengono eseguite da JIT (ovviamente lo conosci, ma lo ignori). Allo stesso modo il problema n. 3 viene parzialmente invalidato poiché JIT si basa fortemente sulle statistiche raccolte dall'interprete. In particolare, non sono d'accordo con "Non è possibile utilizzare alcuna libreria" in quanto vi è la de-ottimizzazione per il salvataggio. Sono d'accordo che la complessità aggiunta sarebbe un problema.
maaartinus,

2
@maaartinus: Inoltre, solo la fine della mia risposta è specifica di javac. In particolare mi faccio menzione, ad esempio, che "Questo funziona solo, quando si ha l'analisi dell'intero programma, quando l'intero programma è scritto nella stessa lingua, e tutto viene compilato in una volta in un colpo solo." Questo è ovviamente vero per C2: si occupa solo di una lingua (bytecode JVM) e ha accesso all'intero programma in una sola volta.
Jörg W Mittag,

1
@ JörgWMittag So che l'OP chiede di javac, ma scommetto che stanno supponendo che javac sia responsabile dell'ottimizzazione. E che quasi non sanno che c'è C2. Non sto dicendo, la tua risposta è negativa. È solo che lasciare a javac qualsiasi ottimizzazione (tranne che per le banalità come l'uso StringBuilder) è un non-senso, quindi lo scarterei e presumo semplicemente, l'OP scrive javac ma significa Hotspot. Il tuo problema n. 2 è una buona ragione per non ottimizzare nulla in javac.
maaartinus,

5

La risposta votata non è riuscita a notare una cosa. La comunicazione sincrona tra i thread è estremamente costosa. Se la funzione è in grado di essere eseguita ad una velocità di molti milioni di chiamate al secondo, in realtà ti fa più male parallelizzarla piuttosto che lasciarla così com'è.

La forma più veloce di comunicazione inter-thread sincrona, utilizzando loop occupati con variabili atomiche, è purtroppo inefficiente dal punto di vista energetico. Se devi ricorrere all'utilizzo delle variabili di condizione per risparmiare energia, le prestazioni della comunicazione inter-thread ne risentono.

Pertanto, il compilatore non deve solo determinare se una funzione è pura, ma dovrebbe anche stimare il tempo di esecuzione della funzione per vedere se la parallelizzazione è una vincita netta. Inoltre, dovrebbe scegliere tra loop occupati usando variabili atomiche o variabili di condizione. E avrebbe bisogno di creare discussioni dietro la schiena.

Se si creano dinamicamente i thread, è persino più lento dell'uso delle variabili di condizione. Quindi, il compilatore dovrebbe impostare un certo numero di thread già in esecuzione.

Quindi, la risposta alla tua domanda è no , i compilatori non sono abbastanza "intelligenti" per parallelizzare automaticamente le funzioni pure soprattutto nel mondo Java. Sono intelligenti non parallelizzandoli automaticamente!


5
" Sono intelligenti non auto-parallelizzandoli! " : Questo va troppo lontano. Mentre è vero che la parallelizzazione in ogni punto possibile solo per il suo interesse sarebbe generalmente inefficiente, un compilatore intelligente identificherebbe una pratica strategia di parallelizzazione. Penso che la maggior parte delle persone lo capisca, quindi quando parliamo di auto-parallelizzazione, intendiamo auto-pratica-parallelizzazione.
Nat

@Nat: ridicolmente troppo difficile. Ciò richiederebbe l'identificazione di funzioni pure sulla scala di runtime di 100s di millisecondi e l'aspettativa del compilatore di avere un'idea del runtime di loop che non hanno costanti nelle loro iterazioni (e i casi che desideri non lo fanno) è sciocco.
Giosuè,

Sono d'accordo - il commento di @ Nat implica che la parallelizzazione non significa necessariamente più thread, il che è vero. La JIT potrebbe, ad esempio, incorporare più chiamate a una funzione pura e in alcuni casi intercalare le istruzioni della CPU. Ad esempio, se entrambe le chiamate del metodo recuperano una costante, potrebbe essere recuperata una volta e conservata in un registro della CPU per entrambe le istanze del metodo da utilizzare. Le CPU moderne sono bestie con numerosi registri di uso generale e istruzioni specializzate che possono essere molto utili quando si ottimizza il codice.

1
@Joshua: Molto più semplice per un compilatore JIT. Il compilatore JIT può anche capire che una funzione potrebbe non essere pura, ma finora nessuna chiamata ha invocato un comportamento impuro.
gnasher729,

Sono d'accordo con @Joshua. Ho un algoritmo difficile da parallelizzare al lavoro. Ho provato a parallelizzarlo manualmente, anche facendo alcune approssimazioni semplificanti (e quindi modificando l'algoritmo), e ho fallito miseramente ogni volta. Anche un programma che dice se è possibile parallelizzare qualcosa è estremamente difficile, anche se sarebbe molto più semplice che in realtà parallelizzarlo. Ricorda che stiamo parlando di linguaggi di programmazione completi di Turing.
juhist,
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.