Vergleich von Programmierparadigmen - Comparison of programming paradigms

Dieser Artikel versucht, die verschiedenen Ähnlichkeiten und Unterschiede zwischen den verschiedenen Programmierparadigmen als Zusammenfassung sowohl in grafischer als auch in tabellarischer Form mit Links zu den separaten Diskussionen zu diesen Ähnlichkeiten und Unterschieden in bestehenden Wikipedia-Artikeln darzustellen.

Hauptparadigmenansätze

Beim Programmieren gibt es zwei Hauptansätze:

Die folgenden werden weithin als die wichtigsten Programmierparadigmen angesehen, wie man bei der Messung der Popularität von Programmiersprachen sieht :

Die folgenden sind gängige Arten der Programmierung, die mit verschiedenen Paradigmen implementiert werden können:

Die Subroutinen, die OOP-Methoden implementieren, können letztendlich in einem zwingenden, funktionalen oder prozeduralen Stil codiert sein, der den Zustand im Namen des aufrufenden Programms direkt ändern kann oder nicht . Es gibt zwangsläufig einige Überschneidungen zwischen den Paradigmen, aber die Hauptmerkmale oder erkennbaren Unterschiede sind in dieser Tabelle zusammengefasst:

Paradigma Beschreibung Hauptmerkmale Zugehörige(s) Paradigma(s) Kritik Beispiele
Imperativ Programme als Anweisungen , die den berechneten Zustand direkt ändern ( Datenfelder ) Direkte Zuweisungen , gemeinsame Datenstrukturen , globale Variablen Edsger W. Dijkstra , Michael A. Jackson C , C++ , Java , Kotlin , PHP , Python , Ruby
Strukturiert Ein Stil der imperativen Programmierung mit logischerer Programmstruktur Struktogramme , Einrückung , keine oder eingeschränkte Verwendung von goto- Anweisungen Imperativ C , C++ , Java , Kotlin , Pascal , PHP , Python
Verfahrensweise Abgeleitet aus der strukturierten Programmierung, basierend auf dem Konzept der modularen Programmierung oder dem Prozeduraufruf Lokale Variablen , Sequenz, Auswahl, Iteration und Modularisierung Strukturiert, zwingend C , C++ , Lisp , PHP , Python
Funktional Behandelt Berechnungen als Auswertung mathematischer Funktionen, wobei Zustands- und veränderliche Daten vermieden werden Lambda-Kalkül , Kompositionalität , Formel , Rekursion , referentielle Transparenz , keine Nebenwirkungen Deklarativ C++ , C# , Clojure , CoffeeScript , Elixir , Erlang , F# , Haskell , Java (seit Version 8), Kotlin , Lisp , Python , R , Ruby , Scala , SequenceL , Standard ML , JavaScript , Elm
Ereignisgesteuert einschließlich zeitgesteuert Der Kontrollfluss wird hauptsächlich durch Ereignisse wie Mausklicks oder Interrupts einschließlich Timer bestimmt Hauptschleife , Eventhandler, asynchrone Prozesse Prozedural, Datenfluss JavaScript , ActionScript , Visual Basic , Elm
Objektorientierten Leckereien Datenfelder als Objekte durch vordefinierte manipulierten Methoden nur Objekte , Methoden , Message Passing , Information Hiding , Datenabstraktion , Kapselung , Polymorphismus , Vererbung , Serialisierung - Marshalling Verfahrensweise Wikipedia , andere Common Lisp , C++ , C# , Eiffel , Java , Kotlin , PHP , Python , Ruby , Scala , JavaScript
Deklarativ Definiert die Programmlogik, aber keinen detaillierten Kontrollfluss Sprachen der vierten Generation , Tabellenkalkulationen , Berichtsprogrammgeneratoren SQL , reguläre Ausdrücke , Prolog , OWL , SPARQL , Datalog , XSLT
Automatenbasierte Programmierung Behandelt Programme als Modell eines endlichen Automaten oder eines anderen formalen Automaten Zustand Enumeration , Regelgröße , Zustandsänderungen, Isomorphismus , Zustandsübergangstabelle Imperativ, ereignisgesteuert Abstrakte State-Machine-Sprache

Unterschiede in der Terminologie

Trotz mehrerer (Typen von) Programmierparadigmen parallel bestehenden (mit manchmal scheinbar widersprüchlichen Definitionen), viele der zugrunde liegenden fundamentalen Komponenten bleiben mehr oder weniger die gleichen ( Konstanten , Variablen , Datenfelder , Subroutinen , Anrufe etc.) und müssen irgendwie damit zwangsläufig sein in jedes einzelne Paradigma mit gleichermaßen ähnlichen Attributen oder Funktionen integriert. Die obige Tabelle ist nicht als Leitfaden für genaue Ähnlichkeiten gedacht, sondern eher als Index dafür, wo nach weiteren Informationen zu suchen ist, basierend auf der unterschiedlichen Benennung dieser Entitäten innerhalb jedes Paradigmas. Weitere erschwerende Dinge sind nicht standardisierte Implementierungen jedes Paradigmas in vielen Programmiersprachen , insbesondere in Sprachen, die mehrere Paradigmen mit jeweils eigenem Jargon unterstützen .

Sprachunterstützung

Syntaktischer Zucker ist die Versüßung der Programmfunktionalität durch Einführung von Sprachmerkmalen, die eine bestimmte Verwendung erleichtern, auch wenn das Endergebnis ohne sie erreicht werden könnte. Ein Beispiel für syntaktischen Zucker können wohl die Klassen sein, die in objektorientierten Programmiersprachen verwendet werden. Die imperative Sprache C kann objektorientierte Programmierung über ihre Funktionen von Funktionszeigern , Typumwandlungen und Strukturen unterstützen. Sprachen wie C++ zielen jedoch darauf ab, die objektorientierte Programmierung bequemer zu machen, indem sie eine für diesen Codierungsstil spezifische Syntax einführen. Darüber hinaus betont die spezialisierte Syntax den objektorientierten Ansatz. Ebenso könnten Funktionen und Schleifensyntax in C (und anderen prozeduralen und strukturierten Programmiersprachen) als syntaktischer Zucker betrachtet werden. Die Assemblersprache kann prozedurale oder strukturierte Programmierung über ihre Einrichtungen zum Modifizieren von Registerwerten und Verzweigen der Ausführung in Abhängigkeit vom Programmzustand unterstützen. Sprachen wie C haben jedoch eine für diese Codierungsstile spezifische Syntax eingeführt, um die prozedurale und strukturierte Programmierung bequemer zu machen. Features der Sprache C# (C Sharp), wie Eigenschaften und Schnittstellen, ermöglichen ebenfalls keine neuen Funktionen, sondern sollen gute Programmierpraktiken prominenter und natürlicher machen.

Manche Programmierer halten diese Funktionen für unwichtig oder sogar leichtfertig. Alan Perlis zum Beispiel witzelte einmal in einem Verweis auf durch Klammern getrennte Sprachen , dass „syntaktischer Zucker Krebs des Semikolons verursacht “ (siehe Epigramme zur Programmierung ).

Eine Erweiterung davon ist die syntaktische Saccharin oder grundlose Syntax, die die Programmierung nicht einfacher macht.

Leistungsvergleich

Insgesamt Länge Befehlsweg nur ein Programm in einem imperativen Stil codiert, keine Subroutinen verwenden, wäre die niedrigste Anzahl haben. Die binäre Größe eines solchen Programms kann jedoch größer sein als das gleiche Programm, das unter Verwendung von Subroutinen codiert wurde (wie bei der funktionalen und prozeduralen Programmierung) und würde auf mehr nicht-lokale physikalische Befehle verweisen , die Cache-Fehltreffer und den Befehlsabruf- Overhead in modernen Prozessoren erhöhen können .

Die Paradigmen, die Subroutinen ausgiebig verwenden (einschließlich funktionaler, prozeduraler und objektorientierter) und keine signifikante Inline-Erweiterung (Inlining, über Compiler-Optimierungen ) verwenden, werden folglich einen größeren Anteil der Gesamtressourcen für die Subroutinen-Verknüpfungen verwenden. Objektorientierte Programme , die nicht absichtlich alter tun Programmzustand direkt, anstatt mit Mutatormethoden (oder Setter ) diese Zustandsänderungen verkapseln, wird als direkte Folge haben Aufwand mehr. Dies liegt daran, dass die Nachrichtenweitergabe im Wesentlichen ein Subroutinenaufruf ist, jedoch mit drei zusätzlichen Overheads: dynamische Speicherzuweisung , Parameterkopieren und dynamischer Versand . Das Abrufen von Speicher aus dem Heap und das Kopieren von Parametern für die Nachrichtenweitergabe können erhebliche Ressourcen erfordern, die weit über die für die Zustandsänderung benötigten hinausgehen. Accessoren (oder Getter ), die lediglich die Werte privater Membervariablen zurückgeben, hängen auch von ähnlichen Subroutinen für die Nachrichtenweitergabe ab, anstatt eine direktere Zuweisung (oder einen Vergleich) zu verwenden, was die Gesamtpfadlänge erhöht.

Verwalteter Code

Bei Programmen, die in einer Umgebung mit verwaltetem Code wie .NET Framework ausgeführt werden , wirken sich viele Probleme auf die Leistung aus, die durch das Programmiersprachenparadigma und die verschiedenen verwendeten Sprachfeatures erheblich beeinträchtigt werden.

Pseudocode-Beispiele, die verschiedene Paradigmen vergleichen

Ein Pseudocode- Vergleich von imperativen, prozeduralen und objektorientierten Ansätzen, die verwendet werden, um die Fläche eines Kreises (πr²) zu berechnen, unter der Annahme, dass kein Subroutinen- Inlining , keine Makro- Präprozessoren , Registerarithmetik und jede Anweisung 'Schritt' als nur eine Anweisung gewichtet wird – als a grobes Maß für die Befehlspfadlänge – wird unten dargestellt. Der die Zustandsänderung konzeptionell durchführende Anweisungsschritt ist jeweils fett hervorgehoben. Die zur Berechnung der Kreisfläche verwendeten arithmetischen Operationen sind in allen drei Paradigmen gleich, mit dem Unterschied, dass das prozedurale und das objektorientierte Paradigma diese Operationen in einen Unterroutinenaufruf einschließen, der die Berechnung allgemein und wiederverwendbar macht. Der gleiche Effekt könnte in einem rein imperativen Programm mit einem Makropräprozessor nur auf Kosten einer erhöhten Programmgröße (nur an jeder Makroaufrufstelle) ohne entsprechende anteilige Laufzeitkosten (proportional zu n Aufrufen – die innerhalb eines innere Schleife zum Beispiel). Umgekehrt könnte das Inlining von Subroutinen durch einen Compiler prozedurale Programme auf eine Größe reduzieren, die dem rein zwingenden Code ähnlich ist. Bei objektorientierten Programmen müssen jedoch selbst mit Inlining Nachrichten (aus Kopien der Argumente) für die Verarbeitung durch die objektorientierten Methoden erstellt werden. Der Overhead von Aufrufen, ob virtuell oder anderweitig, wird nicht durch die Änderung des Kontrollflusses dominiert – sondern durch die umgebenden Aufrufkonventionskosten , wie Prolog- und Epilog- Code, Stack-Setup und Argumentübergabe (siehe hier für realistischere Befehlspfadlänge, Stack und andere Kosten im Zusammenhang mit Anrufen auf einer x86- Plattform). Siehe auch hier für eine Folienpräsentation von Eric S. Roberts ("The Allocation of Memory to Variables", Kapitel 7) – die die Verwendung von Stapel- und Heap-Speicher beim Summieren von drei rationalen Zahlen in der objektorientierten Sprache Java veranschaulicht .

Imperativ Verfahrensweise Objektorientierten
 load r;                      1
 r2 = r * r;                  2
 result = r2 * "3.142";       3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.... storage .............
result variable
constant "3.142"
area proc(r2,res):
   push stack                                 5
   load r2;                                   6
   r3 = r2 * r2;                              7
   res = r3 * "3.142";                        8
   pop stack                                  9
   return;                                   10
...............................................
main proc:
   load r;                                    1
   call area(r,result);
    +load p = address of parameter list;      2
    +load v = address of subroutine 'area';   3
    +goto v with return;                      4
.
.
.
.
.... storage .............
result variable
constant "3.142"
parameter list variable
function pointer (==>area)
stack storage
circle.area method(r2):
   push stack                                 7
   load r2;                                   8
   r3 = r2 * r2;                              9
   res = r3 * "3.142";                       10
   pop stack                                 11
   return(res);                           12,13
...............................................
main proc:
   load r;                                    1
   result = circle.area(r);
      +allocate heap storage;                 2
      +copy r to message;                     3
      +load p = address of message;           4
      +load v = addr. of method 'circle.area' 5
      +goto v with return;                    6
.
.
.... storage .............
result variable (assumed pre-allocated)
immutable variable "3.142" (final)
(heap) message variable for circle method call
vtable(==>area)
stack storage

Die Vorteile der prozeduralen Abstraktion und des objektorientierten Polymorphismus werden durch ein kleines Beispiel wie das obige schlecht veranschaulicht. Dieses Beispiel soll hauptsächlich einige intrinsische Leistungsunterschiede veranschaulichen, nicht Abstraktion oder Codewiederverwendung.

Unterprogramm, Methodenaufruf-Overhead

Das Vorhandensein einer (aufgerufenen) Unterroutine in einem Programm trägt unabhängig vom Paradigma nichts zur Funktionalität des Programms bei, kann jedoch erheblich zur Strukturierung und Allgemeingültigkeit des Programms beitragen, was das Schreiben, Ändern und Erweitern erheblich erleichtert. Das Ausmaß , in der verschiedenen Paradigmen verwenden Subroutinen (und den daraus folgenden Speicherbedarf) beeinflusst die Gesamtleistung des gesamten Algorithmus, obwohl als Guy Steele in einem 1977 Papier wiesen darauf hin, eine gut konzipierte Programmiersprache Implementierung kann sehr niedrig Gemeinkosten für prozedurale Abstraktion hat (beklagt jedoch in den meisten Implementierungen, dass dies in der Praxis selten erreicht wird - "in dieser Hinsicht eher gedankenlos oder nachlässig"). Im selben Artikel argumentiert Steele auch für Automaten-basierte Programmierung (unter Verwendung von Prozeduraufrufen mit Tail-Rekursion ) und kommt zu dem Schluss, dass "wir Prozeduraufrufe respektieren sollten" (weil sie mächtig sind), schlug jedoch vor, sie "sparsam zu verwenden". "

In der Häufigkeit von Unterprogrammaufrufen:

  • Bei der prozeduralen Programmierung wird die Granularität des Codes weitgehend durch die Anzahl der diskreten Prozeduren oder Module bestimmt .
  • Bei der funktionalen Programmierung sind häufige Aufrufe von Bibliotheksunterprogrammen üblich, können jedoch häufig vom optimierenden Compiler inline eingebunden werden
  • Bei der objektorientierten Programmierung wird die Anzahl der aufgerufenen Methodenaufrufe auch teilweise durch die Granularität der Datenstrukturen bestimmt und kann daher viele Nur-Lese- Zugriffe auf Low-Level-Objekte umfassen, die gekapselt und somit in keiner anderen, direkteren, Weg. Da eine erhöhte Granularität eine Voraussetzung für eine stärkere Wiederverwendung von Code ist , geht die Tendenz zu feinkörnigen Datenstrukturen und einer entsprechenden Zunahme der Anzahl diskreter Objekte (und ihrer Methoden) und folglich der Aufrufe von Unterprogrammen. Von der Schaffung von Gottesobjekten wird aktiv abgeraten. Konstruktoren tragen ebenfalls zur Zählung bei, da sie auch Unterroutinenaufrufe sind (es sei denn, sie sind inline). Leistungsprobleme, die durch übermäßige Granularität verursacht werden, werden möglicherweise erst sichtbar, wenn die Skalierbarkeit ein Problem wird.
  • Bei anderen Paradigmen, bei denen eine Mischung der obigen Paradigmen verwendet werden kann, ist die Verwendung der Unterroutine weniger vorhersehbar.

Zuweisung von dynamischem Speicher für Nachrichten- und Objektspeicher

Einzigartig beinhaltet das objektorientierte Paradigma eine dynamische Speicherzuweisung aus dem Heap-Speicher sowohl für die Objekterzeugung als auch für die Nachrichtenweitergabe. Ein Benchmark von 1994 - "Speicherzuweisungskosten in großen C- und C++-Programmen", der von der Digital Equipment Corporation mit einer Vielzahl von Software unter Verwendung eines Profilierungstools auf Befehlsebene durchgeführt wurde, maß, wie viele Befehle pro dynamischer Speicherzuweisung erforderlich waren. Die Ergebnisse zeigten, dass die niedrigste absolute Anzahl von ausgeführten Anweisungen im Durchschnitt bei etwa 50 lag, andere jedoch bis zu 611 haben daher einen hohen Overhead". Das 1996 erschienene IBM Paper "Scalability of Dynamic Storage Allocation Algorithms" von Arun Iyengar von IBM demonstriert verschiedene dynamische Speicheralgorithmen und ihre jeweiligen Instruktionszahlen. Sogar der empfohlene MFLF-I-Algorithmus (HS Stone, RC 9674) zeigt Befehlszahlen in einem Bereich zwischen 200 und 400. Das obige Pseudocode-Beispiel enthält keine realistische Schätzung dieser Speicherzuweisungspfadlänge oder des damit verbundenen Speicherpräfix-Overheads und des anschließenden zugehörigen Mülls Inkassogemeinkosten. Ein Open-Source-Software- Mikrozuordner des Spieleentwicklers John W. Ratcliff besteht aus fast 1.000 Zeilen Code, was stark darauf hindeutet, dass die Heap-Zuweisung eine nicht triviale Aufgabe ist .

Dynamisch verteilte Nachrichtenaufrufe im Vergleich zu direkten Prozeduraufruf-Overheads

In ihrem Abstract „ Optimization of Object-Oriented Programs Using Static Class Hierarchy Analysis “ behaupten Jeffrey Dean, David Grove und Craig Chambers vom Department of Computer Science and Engineering der University of Washington , dass „Heavy use of inheritance and dynamically -gebundene Nachrichten machen Code wahrscheinlich erweiterbarer und wiederverwendbarer, verursachen aber auch einen erheblichen Leistungsaufwand im Vergleich zu einem äquivalenten, aber nicht erweiterbaren Programm, das nicht objektorientiert geschrieben wurde , sind die Leistungskosten der zusätzlichen Flexibilität, die durch die Verwendung eines stark objektorientierten Stils bereitgestellt wird, akzeptabel.In anderen Bereichen, wie grundlegenden Datenstrukturbibliotheken, numerischen Computing-Paketen, Rendering-Bibliotheken und spurgesteuerten Simulationsframeworks, sind die Kosten für Message Passing kann zu groß sein und den Programmierer zwingen, objektorientierte Programmierung an den „Hotspots“ seiner Anwendung zu vermeiden."

Serialisieren von Objekten

Die Serialisierung verursacht bei der Übergabe von Objekten von einem System an ein anderes einen großen Aufwand , insbesondere wenn die Übertragung in menschenlesbaren Formaten wie Extensible Markup Language ( XML ) und JavaScript Object Notation ( JSON ) erfolgt. Dies steht im Gegensatz zu kompakten Binärformaten für nicht objektorientierte Daten. Sowohl die Codierung als auch die Decodierung des Datenwerts des Objekts und seiner Attribute sind am Serialisierungsprozess beteiligt, der auch das Bewusstsein für komplexe Probleme wie Vererbung, Kapselung und Datenverbergung umfasst.

Paralleles Rechnen

Carnegie-Mellon University Professor Robert Harper März 2011 schrieb: „ In diesem Semester Dan Licata und ich sind Co-Lehre einen neuen Kurs auf der funktionalen Programmierung für das erste Jahr prospektive CS Majors ... Objektorientierte Programmierung ist vollständig aus dem Einführungs Lehrplan beseitigt , da es von Natur aus sowohl antimodular als auch antiparallel ist und daher für ein modernes CS-Curriculum ungeeignet ist. Für Studierende, die studieren möchten, wird ein neuer Kurs über objektorientierte Designmethodik auf der zweiten Stufe angeboten. dieses Thema."

Siehe auch

Verweise

Weiterlesen

Externe Links