Ist die funktionale Programmierung beim Multithreading schneller, weil ich Dinge anders schreibe oder weil Dinge anders kompiliert werden?


63

Ich tauche in die Welt der funktionalen Programmierung ein und lese überall, dass funktionale Sprachen für Multithreading- / Multicore-Programme besser sind. Ich verstehe, wie funktionale Sprachen eine Menge Dinge anders machen, wie Rekursion , Zufallszahlen usw. Aber ich kann nicht herausfinden, ob Multithreading in einer funktionalen Sprache schneller ist, weil es anders kompiliert ist oder weil ich es anders schreibe .

Ich habe zum Beispiel ein Programm in Java geschrieben, das ein bestimmtes Protokoll implementiert. In diesem Protokoll senden und empfangen die beiden Parteien Tausende von Nachrichten, verschlüsseln diese und senden sie immer wieder neu (und empfangen sie). Wie erwartet ist Multithreading der Schlüssel, wenn Sie in der Größenordnung von Tausenden arbeiten. In diesem Programm gibt es keine Sperren .

Wenn ich dasselbe Programm in Scala schreibe (das die JVM verwendet), ist diese Implementierung dann schneller? Wenn ja warum Liegt es am Schreibstil? Wenn es ist wegen des Schreibstiles, jetzt , dass Java Lambda - Ausdrücke enthält, kann nicht erreiche ich die gleichen Ergebnisse Java mit Lambda verwenden? Oder ist es schneller, weil Scala die Dinge anders kompiliert?


64
Afaik-Funktionsprogrammierung beschleunigt Multithreading nicht. Es macht Multithreading einfacher zu implementieren und sicherer, da es einige Merkmale der funktionalen Programmierung wie Unveränderlichkeit und Funktionen ohne Nebenwirkungen gibt, die diesbezüglich hilfreich sind.
Pieter B

7
Beachten Sie, dass 1) besser nicht wirklich definiert ist 2) es wird sicherlich nicht einfach als "schneller" definiert. Eine Sprache X, die das Milliardenfache des Codes für einen Leistungszuwachs von 0,1% gegenüber Y erfordert, ist für eine vernünftige Definition von besser nicht besser als Y.
Bakuriu

2
Wollten Sie nach "funktionaler Programmierung" oder "im funktionalen Stil geschriebenen Programmen" fragen? Oft führt eine schnellere Programmierung nicht zu einem schnelleren Programm.
Ben Voigt

1
Vergessen Sie nicht, dass es immer einen GC gibt, der im Hintergrund ausgeführt werden muss und mit Ihren Allokationsanforderungen Schritt hält ... und ich bin mir nicht sicher, ob er Multithread-fähig ist ...
Mehrdad,

4
Die einfachste Antwort lautet hier: Funktionale Programmierung ermöglicht das Schreiben von Programmen, bei denen weniger Probleme mit den Race-Bedingungen auftreten. Dies bedeutet jedoch nicht, dass Programme, die im imperativen Stil geschrieben wurden, langsamer sind.
Dawid Pura

Antworten:


97

Der Grund, warum Leute sagen, dass funktionale Sprachen für die Parallelverarbeitung besser sind, liegt in der Tatsache, dass sie normalerweise einen veränderlichen Zustand vermeiden. Der veränderbare Zustand ist die "Wurzel allen Übels" im Kontext der Parallelverarbeitung; Sie machen es wirklich einfach, auf Rennbedingungen zu stoßen, wenn sie von gleichzeitigen Prozessen geteilt werden. Die Lösung für die Race-Bedingungen beinhaltet dann, wie Sie bereits erwähnt haben, Sperr- und Synchronisationsmechanismen, die zu einem Laufzeit-Overhead führen, da die Prozesse aufeinander warten, bis sie die gemeinsam genutzte Ressource nutzen, und eine größere Designkomplexität, wie dies bei all diesen Konzepten der Fall ist tief in solchen Anwendungen verschachtelt.

Wenn Sie einen veränderlichen Zustand vermeiden, werden keine Synchronisations- und Sperrmechanismen mehr benötigt. Da funktionale Sprachen in der Regel einen veränderlichen Status vermeiden, sind sie für die parallele Verarbeitung von Natur aus effizienter und effektiver - Sie haben nicht den Laufzeitaufwand für gemeinsam genutzte Ressourcen und nicht die zusätzliche Designkomplexität, die normalerweise folgt.

Dies ist jedoch alles nebensächlich. Wenn Ihre Java-Lösung auch einen veränderlichen Status (der speziell zwischen Threads geteilt wird) vermeidet, würde die Konvertierung in eine funktionale Sprache wie Scala oder Clojure keine Vorteile in Bezug auf die gleichzeitige Effizienz bringen, da die ursprüngliche Lösung bereits frei von dem durch verursachten Overhead ist die Verriegelungs- und Synchronisationsmechanismen.

TL; DR: Wenn eine Lösung in Scala bei der Parallelverarbeitung effizienter ist als eine in Java, liegt dies nicht an der Art und Weise, wie der Code in der JVM kompiliert oder ausgeführt wird, sondern daran, dass die Java-Lösung den veränderlichen Status zwischen Threads teilt. Dies kann entweder zu Rennbedingungen führen oder den Synchronisationsaufwand erhöhen, um diese zu vermeiden.


2
Wenn nur ein Thread ein Datenelement ändert; Es ist keine besondere Pflege erforderlich. Nur wenn mehrere Threads die gleichen Daten ändern, müssen Sie besondere Sorgfalt walten lassen (Synchronisation, Transaktionsspeicher, Sperren usw.). Ein Beispiel hierfür ist der Stapel eines Threads, der ständig vom Funktionscode geändert wird, jedoch nicht von mehreren Threads geändert wird.
Brendan

31
Es genügt, wenn ein Thread die Daten mutiert, während andere lesen, dass Sie anfangen müssen, "besondere Sorgfalt" walten zu lassen.
Peter Green

10
@Brendan: Nein, wenn ein Thread Daten ändert, während andere Threads aus denselben Daten lesen, liegt eine Racebedingung vor. Besondere Sorgfalt ist erforderlich, auch wenn nur ein Thread geändert wird.
Cornstalks

3
Der veränderbare Zustand ist die "Wurzel allen Übels" im Kontext der Parallelverarbeitung => Wenn Sie Rust noch nicht angeschaut haben, rate ich Ihnen, einen Blick darauf zu werfen. Es schafft es, die Wandlungsfähigkeit sehr effizient zu ermöglichen, indem erkannt wird, dass das wahre Problem mit Aliasing gemischt wandelbar ist: Wenn Sie nur Aliasing oder nur Wandlungsfähigkeit haben, gibt es kein Problem.
Matthieu M.

2
@MatthieuM. Richtig, danke! Ich habe bearbeitet, um die Dinge in meiner Antwort klarer auszudrücken. Der veränderbare Zustand ist nur dann "die Wurzel allen Übels", wenn er zwischen gleichzeitigen Prozessen geteilt wird - etwas, das Rust mit seinen Besitzkontrollmechanismen vermeidet.
MichelHenrich

8

Art von beidem. Es ist schneller, weil es einfacher ist, Ihren Code so zu schreiben, dass er schneller kompiliert werden kann. Sie werden nicht unbedingt einen Geschwindigkeitsunterschied durch das Wechseln der Sprache erhalten, aber wenn Sie mit einer funktionalen Sprache begonnen hätten, hätten Sie das Multithreading wahrscheinlich mit viel weniger Programmieraufwand durchführen können . Entsprechend ist es für einen Programmierer viel einfacher, Threading-Fehler zu machen, die in einer imperativen Sprache Geschwindigkeit kosten, und es ist viel schwieriger, diese Fehler zu bemerken.

Der Grund dafür ist, dass zwingende Programmierer im Allgemeinen versuchen, den gesamten sperrenfreien Code mit Threads in eine möglichst kleine Box zu packen und ihn so schnell wie möglich wieder in ihre komfortable, wandelbare, synchrone Welt zurückzuführen. Die meisten Fehler, die Ihre Geschwindigkeit kosten, werden an dieser Grenzfläche gemacht. In einer funktionalen Programmiersprache müssen Sie sich nicht so viele Gedanken darüber machen, an dieser Grenze Fehler zu machen. Der Großteil Ihrer Telefonvorwahl befindet sich sozusagen "in der Box".


7

Funktionale Programmierung sorgt in der Regel nicht für schnellere Programme. Dies erleichtert die parallele und gleichzeitige Programmierung. Hierfür gibt es zwei Hauptschlüssel:

  1. Die Vermeidung eines veränderlichen Zustands verringert in der Regel die Anzahl der Fehler, die in einem Programm auftreten können, und insbesondere in einem gleichzeitig ablaufenden Programm.
  2. Das Vermeiden von Synchronisationsprimitiven mit gemeinsamem Speicher und auf Sperren basierenden Synchronisationsprimitiven zugunsten von Konzepten höherer Ebenen vereinfacht tendenziell die Synchronisation zwischen Codethreads.

Ein hervorragendes Beispiel für Punkt 2 ist, dass wir in Haskell klar zwischen deterministischer Parallelität und nicht deterministischer Parallelität unterscheiden . Es gibt keine bessere Erklärung, als Simon Marlows hervorragendes Buch Parallel and Concurrent Programming in Haskell zu zitieren (Zitate stammen aus Kapitel 1 ):

Ein paralleles Programm verwendet eine Vielzahl von Computerhardware (z. B. mehrere Prozessorkerne), um eine Berechnung schneller durchzuführen. Ziel ist es, die Antwort früher zu finden, indem verschiedene Teile der Berechnung an verschiedene Prozessoren delegiert werden, die zur gleichen Zeit ausgeführt werden.

Im Gegensatz dazu ist die Parallelität eine Programmstrukturierungstechnik, bei der es mehrere Steuerungsthreads gibt. Konzeptionell werden die Steuerthreads "zur gleichen Zeit" ausgeführt. Das heißt, der Benutzer sieht ihre Effekte ineinander verschachtelt. Ob sie tatsächlich zur gleichen Zeit ausgeführt werden oder nicht, ist ein Implementierungsdetail. Ein gleichzeitiges Programm kann auf einem einzelnen Prozessor durch verschachtelte Ausführung oder auf mehreren physischen Prozessoren ausgeführt werden.

Darüber hinaus erwähnt Marlow auch die Dimension des Determinismus :

Eine verwandte Unterscheidung besteht zwischen deterministischen und nicht deterministischen Programmiermodellen. Ein deterministisches Programmiermodell ist eines, bei dem jedes Programm nur ein Ergebnis liefern kann, wohingegen ein nicht deterministisches Programmiermodell Programme zulässt, die je nach Aspekt der Ausführung unterschiedliche Ergebnisse haben können. Gleichzeitige Programmiermodelle sind notwendigerweise nicht deterministisch, da sie mit externen Agenten interagieren müssen, die zu unvorhersehbaren Zeiten Ereignisse verursachen. Nichtdeterminismus hat jedoch einige bemerkenswerte Nachteile: Programme sind erheblich schwieriger zu testen und zu begründen.

Für die parallele Programmierung möchten wir möglichst deterministische Programmiermodelle verwenden. Da es nur darum geht, die Antwort schneller zu finden, möchten wir unser Programm nicht schwieriger machen, in diesem Prozess Fehler zu beheben. Deterministische Parallelprogrammierung ist das Beste aus beiden Welten: Das sequentielle Programm kann getestet, getestet und begründet werden, aber das Programm läuft schneller, wenn mehr Prozessoren hinzugefügt werden.

In Haskell sind die Parallelitäts- und Nebenläufigkeitsfunktionen auf diese Konzepte ausgelegt. Insbesondere, welche anderen Sprachen als ein Feature-Set zusammengefasst sind, teilt sich Haskell in zwei:

  • Deterministische Merkmale und Bibliotheken für Parallelität .
  • Nicht deterministische Funktionen und Bibliotheken für die gleichzeitige Verwendung .

Wenn Sie nur versuchen, eine reine, deterministische Berechnung zu beschleunigen, macht die deterministische Parallelität die Sache oft viel einfacher. Oft macht man einfach so etwas:

  1. Schreiben Sie eine Funktion, die eine Liste von Antworten erstellt, deren Berechnung teuer ist, die jedoch nicht sehr voneinander abhängen. Dies ist Haskell, also sind Listen faul - die Werte ihrer Elemente werden erst berechnet, wenn ein Verbraucher sie verlangt.
  2. Verwenden Sie die Strategies- Bibliothek, um die Elemente der Ergebnislisten Ihrer Funktion über mehrere Kerne hinweg parallel zu nutzen.

Ich habe das tatsächlich mit einem meiner Spielzeugprojektprogramme vor ein paar Wochen gemacht . Es war trivial, das Programm zu parallelisieren. Das Wichtigste, was ich tun musste, war tatsächlich, einen Code hinzuzufügen, der besagt, dass die Elemente dieser Liste parallel berechnet werden (Zeile 90), und ich bekam einen nahezu linearen Durchsatzschub einige meiner teureren Testfälle.

Ist mein Programm schneller als mit herkömmlichen sperrenbasierten Multithreading-Dienstprogrammen? Das bezweifle ich sehr. Das Ordentliche in meinem Fall war, so viel aus so wenig Geld herauszuholen - mein Code ist wahrscheinlich sehr suboptimal, aber weil es so einfach zu parallelisieren ist, habe ich mit viel weniger Aufwand eine große Beschleunigung erzielt, als ihn richtig zu profilieren und zu optimieren. und keine Gefahr von Rennbedingungen. Und das ist, so würde ich behaupten, die Hauptmethode, mit der Sie mit funktionaler Programmierung "schnellere" Programme schreiben können.


2

In Haskell ist eine Änderung buchstäblich unmöglich, ohne dass spezielle veränderbare Variablen über eine Änderungsbibliothek abgerufen werden. Stattdessen erstellen Funktionen die Variablen, die sie benötigen, gleichzeitig mit ihren Werten (die träge berechnet werden), und der Müll wird gesammelt, wenn er nicht mehr benötigt wird.

Selbst wenn Sie Änderungsvariablen benötigen, können Sie diese in der Regel sparsam und zusammen mit den nicht änderbaren Variablen verwenden. (Eine andere nette Sache in haskell ist STM, das Sperren durch atomare Operationen ersetzt, aber ich bin nicht sicher, ob dies nur für die funktionale Programmierung ist oder nicht.) Normalerweise muss nur ein Teil des Programms parallel gemacht werden, um die Dinge zu verbessern leistungsmäßig.

Dies macht die Parallelität in Haskell oftmals einfach, und es werden Anstrengungen unternommen, um sie automatisch zu machen. Bei einfachem Code können Parallelität und Logik sogar getrennt werden.

Aufgrund der Tatsache, dass die Auswertungsreihenfolge in Haskell keine Rolle spielt, erstellt der Compiler lediglich die auszuwertenden Warteschlangensachen und sendet sie an die verfügbaren Kerne, sodass Sie eine Reihe von "Threads" erstellen können, die dies nicht tun tatsächlich werden fäden erst nötig. Die Bewertungsreihenfolge, die keine Rolle spielt, ist charakteristisch für die Reinheit, die normalerweise eine funktionale Programmierung erfordert.

Weitere Lektüre
Parallelität in Haskell (HaskellWiki) Parallele und gleichzeitige Programmierung in Haskell
in "Real-World Haskell"
von Simon Marlow


7
grep java this_post. grep scala this_postund grep jvm this_postzurück keine Ergebnisse :)
Andres F.

4
Die Frage ist vage. Im Titel und im ersten Absatz werden Fragen zur funktionalen Programmierung im Allgemeinen gestellt , im zweiten und dritten Absatz werden Fragen zu Java und Scala im Besonderen gestellt . Das ist bedauerlich, zumal eine der Kernstärken von Scala gerade darin besteht, dass es sich nicht (nur) um eine funktionale Sprache handelt. Martin Odersky nennt es "postfunktional", andere nennen es "objektfunktional". Es gibt zwei verschiedene Definitionen des Begriffs "funktionale Programmierung". Einer ist "Programmieren mit erstklassigen Prozeduren" (die ursprüngliche Definition für LISP), der andere ist ...
Jörg W Mittag

2
"Programmierung mit referenziell transparenten, reinen, nebenwirkungsfreien Funktionen und unveränderlichen persistenten Daten" (eine viel strengere und auch neuere Interpretation). Diese Antwort spricht die zweite Interpretation an, was Sinn macht, weil a) die erste Interpretation völlig unabhängig von Parallelität und Nebenläufigkeit ist, b) die erste Interpretation im Grunde genommen bedeutungslos geworden ist, da mit Ausnahme von C fast jede Sprache auch nur in bescheidenem Umfang verbreitet verwendet wird Heute hat erstklassige Verfahren (einschließlich Java), und c) das OP fragt nach dem Unterschied zwischen Java und Scala, aber es gibt keine ...
Jörg W Mittag

2
zwischen den beiden in Bezug auf Definition # 1, nur Definition # 2.
Jörg W Mittag

Die Bewertungssache ist nicht ganz richtig, wie es hier geschrieben steht; Standardmäßig verwendet die Laufzeitumgebung kein Multithreading, und auch wenn Sie Multithreading aktivieren, müssen Sie der Laufzeitumgebung mitteilen, welche Dinge sie parallel auswerten soll.
Cubic
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.