La programmazione funzionale è più veloce nel multithreading perché scrivo cose in modo diverso o perché le cose sono compilate in modo diverso?


63

Mi sto tuffando nel mondo della programmazione funzionale e continuo a leggere ovunque che i linguaggi funzionali sono migliori per i programmi multithreading / multicore. Capisco come i linguaggi funzionali fanno molte cose in modo diverso, come la ricorsione , i numeri casuali ecc. Ma non riesco a capire se il multithreading è più veloce in un linguaggio funzionale perché è compilato in modo diverso o perché lo scrivo in modo diverso.

Ad esempio, ho scritto un programma in Java che implementa un determinato protocollo. In questo protocollo le due parti si inviano e si ricevono reciprocamente migliaia di messaggi, crittografano tali messaggi e li rispediscono (e li ricevono) ancora e ancora. Come previsto, il multithreading è fondamentale quando si tratta di una scala di migliaia. In questo programma non è previsto alcun blocco .

Se scrivo lo stesso programma in Scala (che utilizza la JVM), questa implementazione sarà più veloce? Se si, perché? È a causa dello stile di scrittura? Se è a causa dello stile di scrittura, ora che Java include espressioni lambda, non potrei ottenere gli stessi risultati usando Java con lambda? O è più veloce perché Scala compilerà le cose in modo diverso?


64
La programmazione funzionale di Afaik non rende il multithreading più veloce. Rende il multithreading più facile da implementare e più sicuro perché ci sono alcune caratteristiche della programmazione funzionale come l'immutabilità e le funzioni che non hanno effetti collaterali che aiutano in questo senso.
Pieter B,

7
Si noti che 1) meglio non è realmente definito 2) sicuramente non è definito semplicemente "più veloce". Un linguaggio X che richiede un miliardo di volte la dimensione del codice per un guadagno delle prestazioni dello 0,1% rispetto a Y non è migliore di Y per una definizione ragionevole di migliore.
Bakuriu,

2
Intendevi chiedere "programmazione funzionale" o "programmi scritti in stile funzionale"? Spesso una programmazione più veloce non produce un programma più veloce.
Ben Voigt,

1
Non dimenticare che c'è sempre un GC che deve funzionare in background e tenere il passo con le tue richieste di allocazione ... e non sono sicuro che sia multithread ...
Mehrdad,

4
La risposta più semplice qui è: la programmazione funzionale consente di scrivere programmi che considererebbero meno problemi di condizioni di gara, tuttavia ciò non significa che i programmi scritti in stile imperativo saranno più lenti.
Dawid Pura,

Risposte:


97

Il motivo per cui le persone dicono che i linguaggi funzionali sono migliori per l'elaborazione parallela è dovuto al fatto che di solito evitano lo stato mutabile. Lo stato mutevole è la "radice di tutti i mali" nel contesto dell'elaborazione parallela; rendono davvero facile imbattersi in condizioni di gara quando sono condivisi tra processi concorrenti. La soluzione alle condizioni di gara implica quindi meccanismi di blocco e sincronizzazione, come hai detto, che causano sovraccarico di runtime, poiché i processi si aspettano l'un l'altro per utilizzare la risorsa condivisa e una maggiore complessità di progettazione, poiché tutti questi concetti tendono ad essere profondamente annidato all'interno di tali applicazioni.

Quando si evita lo stato mutabile, scompare la necessità di meccanismi di sincronizzazione e blocco. Poiché i linguaggi funzionali generalmente evitano lo stato mutabile, sono naturalmente più efficienti ed efficaci per l'elaborazione parallela: non si avrà il sovraccarico di runtime delle risorse condivise e non si avrà la complessità di progettazione aggiuntiva che di solito segue.

Tuttavia, questo è tutto fortuito. Se la tua soluzione in Java evita anche lo stato mutabile (specificamente condiviso tra i thread), convertirlo in un linguaggio funzionale come Scala o Clojure non produrrebbe alcun vantaggio in termini di efficienza concorrente, perché la soluzione originale è già libera dal sovraccarico causato da i meccanismi di blocco e sincronizzazione.

TL; DR: se una soluzione in Scala è più efficiente nell'elaborazione parallela di una in Java, non è a causa del modo in cui il codice viene compilato o eseguito attraverso la JVM, ma piuttosto perché la soluzione Java condivide lo stato mutabile tra i thread, causando condizioni di gara o aggiungendo il sovraccarico della sincronizzazione per evitarle.


2
Se solo un thread modifica un dato; non è necessaria alcuna cura speciale. È solo quando più thread possono modificare gli stessi dati che è necessario un tipo di cura speciale (sincronizzazione, memoria transazionale, blocco, qualunque cosa). Un esempio di questo è lo stack di un thread, che è costantemente mutato dal codice funzionale ma non modificato da più thread.
Brendan,

31
Avere un thread che muta i dati mentre altri lo leggono è sufficiente che tu debba iniziare a fare "attenzione speciale".
Peter Green,

10
@Brendan: No, se un thread modifica i dati mentre altri thread leggono da quegli stessi dati, allora hai una condizione di competizione. È necessario prestare particolare attenzione anche se viene modificato solo un thread.
Cornstalks,

3
Lo stato mutevole è la "radice di tutti i mali" nel contesto dell'elaborazione parallela => se non hai ancora guardato Rust, ti consiglio di sbirciarlo. Riesce a consentire la mutabilità in modo molto efficiente rendendosi conto che il vero problema è mutabile mescolato con aliasing: se hai solo aliasing o hai solo mutabilità, non c'è problema.
Matthieu M.,

2
@MatthieuM. Bene, grazie! Ho modificato per esprimere le cose più chiaramente nella mia risposta. Lo stato mutevole è "la radice di tutti i mali" solo quando è condiviso tra processi concorrenti - qualcosa che Rust evita con i suoi meccanismi di controllo della proprietà.
MichelHenrich,

8

Sorta di entrambi. È più veloce perché è più facile scrivere il codice in un modo che sia più facile da compilare più velocemente. Non avrai necessariamente una differenza di velocità cambiando le lingue, ma se avessi iniziato con un linguaggio funzionale, probabilmente avresti potuto fare il multithreading con un impegno molto minore da parte del programmatore . Sulla stessa linea, è molto più facile per un programmatore fare errori di threading che costerà velocità in un linguaggio imperativo e molto più difficile notare quegli errori.

Il motivo è che i programmatori imperativi generalmente cercano di mettere tutto il codice thread-free privo di blocchi in una casella il più piccola possibile e di scappare il prima possibile nel loro comodo mondo sincrono mutevole. La maggior parte degli errori che ti costano la velocità sono fatti su quell'interfaccia di confine. In un linguaggio di programmazione funzionale, non devi preoccuparti tanto di fare errori su quel confine. La maggior parte del codice chiamante è anche "dentro la scatola", per così dire.


7

La programmazione funzionale non consente programmi più veloci, come regola generale. Ciò che rende è una programmazione parallela e simultanea più semplice . Ci sono due chiavi principali per questo:

  1. Evitare lo stato mutevole tende a ridurre il numero di cose che possono andare storte in un programma, e ancora di più in un programma concorrente.
  2. Evitare primitive di sincronizzazione basate su memoria condivisa e lock a favore di concetti di livello superiore tende a semplificare la sincronizzazione tra thread di codice.

Un ottimo esempio di punto # 2 è che in Haskell abbiamo una chiara distinzione tra il parallelismo deterministico contro la concorrenza non deterministica . Non c'è spiegazione migliore di citare l'eccellente libro di Simon Marlow Parallel and Concurrent Programming in Haskell (le citazioni sono dal capitolo 1 ):

Un programma parallelo è uno che utilizza una molteplicità di hardware computazionale (ad esempio, diversi core del processore) per eseguire un calcolo più rapidamente. L'obiettivo è quello di arrivare alla risposta prima, delegando parti diverse del calcolo a processori diversi che eseguono contemporaneamente.

Al contrario, la concorrenza è una tecnica di strutturazione del programma in cui esistono più thread di controllo. Concettualmente, i thread di controllo vengono eseguiti "allo stesso tempo"; cioè, l'utente vede i loro effetti interlacciati. Se eseguono effettivamente contemporaneamente o meno è un dettaglio di implementazione; un programma simultaneo può essere eseguito su un singolo processore attraverso l'esecuzione interfogliata o su più processori fisici.

Oltre a ciò, Marlow menziona anche la dimensione del determinismo :

Una distinzione correlata è tra i modelli di programmazione deterministica e non deterministica . Un modello di programmazione deterministica è quello in cui ciascun programma può dare un solo risultato, mentre un modello di programmazione non deterministico ammette programmi che possono avere risultati diversi, a seconda di alcuni aspetti dell'esecuzione. I modelli di programmazione simultanea sono necessariamente non deterministici perché devono interagire con agenti esterni che causano eventi in tempi imprevedibili. Il non determinismo presenta alcuni notevoli svantaggi, tuttavia: i programmi diventano significativamente più difficili da testare e ragionare.

Per la programmazione parallela, vorremmo utilizzare modelli di programmazione deterministica, se possibile. Poiché l'obiettivo è solo quello di arrivare alla risposta più rapidamente, preferiremmo non rendere più difficile il debug del nostro programma nel processo. La programmazione parallela deterministica è il migliore dei due mondi: test, debug e ragionamento possono essere eseguiti sul programma sequenziale, ma il programma funziona più velocemente con l'aggiunta di più processori.

In Haskell le caratteristiche di parallelismo e concorrenza sono progettate attorno a questi concetti. In particolare, quali altre lingue raggruppano insieme come un set di funzionalità, Haskell si divide in due:

  • Funzionalità e librerie deterministiche per il parallelismo .
  • Funzionalità e librerie non deterministiche per la concorrenza .

Se stai solo cercando di accelerare un calcolo puro e deterministico, avere un parallelismo deterministico spesso rende le cose molto più facili. Spesso fai semplicemente una cosa del genere:

  1. Scrivi una funzione che produce un elenco di risposte, ognuna delle quali è costosa da calcolare ma non dipende molto l'una dall'altra. Questo è Haskell, quindi gli elenchi sono pigri: i valori dei loro elementi non vengono effettivamente calcolati fino a quando un consumatore non li richiede.
  2. Utilizzare la libreria Strategie per utilizzare gli elementi dell'elenco dei risultati della funzione in parallelo su più core.

In realtà l'ho fatto con uno dei miei programmi di progetti di giocattoli poche settimane fa . Parallelizzare il programma era banale: la cosa chiave che dovevo fare era, in effetti, aggiungere un po 'di codice che dicesse "calcola gli elementi di questo elenco in parallelo" (linea 90), e ho ottenuto un incremento della velocità quasi lineare in alcuni dei miei casi di test più costosi.

Il mio programma è più veloce di se avessi utilizzato le utility di multithreading basate su lock convenzionali? Ne dubito molto. La cosa bella nel mio caso è stata quella di ottenere così tanto botto da così poco dollaro: il mio codice è probabilmente molto subottimale, ma poiché è così facile da parallelizzare ho ottenuto una grande accelerazione con molto meno sforzo rispetto alla profilazione e all'ottimizzazione, e nessun rischio di condizioni di gara. E questo, direi, è il modo principale in cui la programmazione funzionale consente di scrivere programmi "più veloci".


2

In Haskell, la modifica è letteralmente impossibile senza ottenere speciali variabili modificabili attraverso una libreria di modifiche. Al contrario, le funzioni creano le variabili di cui hanno bisogno contemporaneamente ai loro valori (che sono calcolati pigramente) e raccolgono i rifiuti quando non sono più necessari.

Anche quando hai bisogno di variabili di modifica, di solito puoi ottenere usando con parsimonia, e insieme alle variabili non modificabili. (Un'altra cosa carina in haskell è STM, che sostituisce i blocchi con operazioni atomiche, ma non sono sicuro che questo sia solo per la programmazione funzionale o meno.) Di solito, solo una parte del programma dovrà essere resa parallela per migliorare le cose prestazioni-saggio.

Questo rende il parallelismo in Haskell facile per la maggior parte del tempo, e in effetti sono in corso sforzi per renderlo automatico. Per un codice semplice, il parallelismo e la logica possono persino essere separati.

Inoltre, a causa del fatto che l'ordine di valutazione non ha importanza in Haskell, il compilatore crea semplicemente una coda di elementi che devono essere valutati e li invia a tutti i core disponibili, in modo da poter creare un gruppo di "thread" che non in realtà diventano discussioni fino a quando necessario. L'ordine di valutazione che non conta è caratteristico della purezza, che di solito richiede una programmazione funzionale.

Ulteriore lettura del
parallelismo in Haskell (HaskellWiki)
Programmazione simultanea e multicore in "Real-World Haskell"
Programmazione parallela e concorrente in Haskell di Simon Marlow


7
grep java this_post. grep scala this_poste grep jvm this_postnon restituisce risultati :)
Andres F.

4
La domanda è vaga. Nel titolo e nel primo paragrafo, si chiede della programmazione funzionale in generale , nel secondo e terzo paragrafo, si chiede di Java e Scala in particolare . Che è un peccato, soprattutto perché uno dei principali punti di forza di Scala è proprio il fatto che si tratta non è (solo) un linguaggio funzionale. Martin Odersky lo chiama "post-funzionale", altri lo chiamano "oggetto funzionale". Esistono due diverse definizioni del termine "programmazione funzionale". Uno è "programmazione con procedure di prima classe" (la definizione originale applicata a LISP), l'altro è ...
Jörg W Mittag,

2
"programmazione con funzioni referenzialmente trasparenti, pure, prive di effetti collaterali e dati persistenti immutabili" (un'interpretazione molto più rigorosa e anche più recente). Questa risposta si rivolge alla seconda interpretazione, il che ha senso, perché a) la prima interpretazione è totalmente estranea al parallelismo e alla concorrenza, b) la prima interpretazione è diventata sostanzialmente insignificante poiché, ad eccezione di C, quasi tutte le lingue in un uso anche modestamente diffuso oggi ha procedure di prima classe (incluso Java) ec) l'OP chiede la differenza tra Java e Scala, ma non c'è ...
Jörg W Mittag,

2
tra i due per quanto riguarda la definizione n. 1, solo la definizione n. 2.
Jörg W Mittag,

La cosa di valutazione non è del tutto vera come è scritto qui; Per impostazione predefinita, il runtime non utilizza affatto il multithreading e IIRC anche se abiliti il ​​multithreading devi comunque dire al runtime quali elementi dovrebbe valutare in parallelo.
Cubico
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.