Wie kann eine Programmiersprache schneller sein, als eine andere?

11 Antworten

Das liegt an der unterschiedlichen Art und Weise, wie der Code ausgeführt wird und ob und wie der Code beim Ausführen optimiert wird.

Das schnellste ist ohne Frage Assembler, da das direkt in die einzelnen CPU-Befehle übersetzt wird. Es gibt also nichts mehr dazwischen, nichts was bremst, nur die Befehle, die der Programmierer geschrieben hat und die CPU führt sie einfach nur stumpf aus.

Python ist (standardmäßig) eine Skriptsprache, das heißt, die Laufzeitumgebung liest dieses Skript ein, interpretiert jeden Befehl nacheinander, prüft ihn ggf. auf Korrektheit und führt dann die tatsächlichen CPU-Befehle dahinter aus - und jeder dieser Zwischenschritte muss natürlich auch von der CPU verarbeitet werden. Das heißt, hinter jedem noch so kleinem Befehl liegt deutlich mehr Arbeit für die CPU, als man im ersten Moment erwarten würde.

C wird kompiliert, das heißt, ein Compiler liest den Code, interpretiert ihn, überprüft ihn ggf. und - nein, er führt ihn nicht aus - generiert auf der Basis direkt die CPU-Befehle. Das läuft etwas komplexer ab, um die vielen Vereinfachungen anbieten zu können, sind einige Zwischenschritte nötig, doch entscheidend ist: Am Ende kommen die CPU-Befehle raus. C++ ist das Gleiche, nur erweitert, der Compiler hat also mehr zu tun, aber am Ende kommen wieder CPU-Befehle raus.
Dennoch wird C++ nie die Performance eines nativ in Assembler geschrieben Programms erreichen können, da für z.B. die Objektorientierung einiges an Handling drum herum notwendig ist. Das erleichtert dem Programmierer die Arbeit, kann aber nicht mehr vollständig entfernt werden, zumindest nicht automatisch vom Compiler. Hier kann aber viel optimiert werden, wenn der Programmierer sich auskennt, kann er vieles davon umgehen und, wenn notwendig, ebenfalls sehr performanten Code schreiben. Das ist auch der Grund, warum performancekritische Programme gerne in C/C++ geschrieben werden: Es ist ein guter Mittelweg zwischen Optimierbarkeit und Einfachheit.

Es gibt aber auch viele Zwischenwege.
C# wird erst kompiliert, am Ende kommen dann keine CPU-Befehle raus, sondern ein Zwischencode, eine an Assembler angelehnte objektorientierte Programmiersprache namens CIL. Die wird dann von der Laufzeitumgebung gelesen, geprüft, etc., aber - und da liegt der Unterschied zu Java - nicht ausgeführt, sondern wieder kompiliert, da kommen dann die CPU-Befehle raus.
Diese ganzen Zwischenschritte mögen unnötig klingen, sind sie aber ganz und gar nicht. Die CPU-Befehle hängen direkt von der CPU ab, deshalb laufen so kompilierte Programme nicht auf jeder CPU, nicht mal in jedem Betriebssystem, die Anforderungen sind also ein großer Punkt. Der Zwischencode von Java und C# interessiert sich nicht dafür, der ist überall gleich, erst die Laufzeitumgebung muss sich darum kümmern und die ist für jedes Programm gleich. Die kann dann den Zwischencode lesen und ggf. die optimalen CPU-Befehle auswählen, je nachdem, auf welcher Hardware es läuft.
Der Unterschied von Java ist deshalb relevant, da Java nach dem Kompilieren (standardmäßig) wieder wie eine Skriptsprache arbeitet, der eigene Zwischencode wird immer gelesen und ausgeführt. C# kompiliert und speichert ihn im RAM, sodass danach immer direkt die CPU-Befehle ausgeführt werden können.

Und dann gibt's noch viele Zwischenvarianten, z.B. bieten C/C++ und Delphi die Möglichkeit, direkt Assembler im "normalen" Code zu verwenden, so können die Entwickler besonders genau optimieren, müssen aber auch genau aufpassen.

Alles Weitere hängt von den Laufzeitumgebungen und ihrer Performance ab und wie gut die Compiler oder PreProzessoren (die formen den Code vor dem Kompilieren nochmal um) optimieren. Oder die Optimierungen der verschiedenen Frameworks, z.B. liefert Microsoft für ihr .NET eine gewaltige Performance-Optimierung nach der Anderen, die vielleicht gar nicht so groß wirken (z.B. String-Verarbeitung), in der Masse aber große Auswirkungen haben, da die Details quasi überall direkt oder indirekt verwendet werden. Oder es werden (ähnlich Delphi und ihr Assembler) Möglichkeiten eingebaut, im Ausnahmefall an den ganzen Vereinfachungen vorbei zu optimieren (z.B. kann C# im Gegensatz zu Java auch mit Pointern arbeiten), wieder mit Vorteilen, aber meist auch vielen Nachteilen.

Es gibt noch einige Punkte mehr, z.B. ist bei C# und Java der Garbage Collector ein großes Thema, der muss ja den RAM aufräumen und das kostet Zeit, in der das Programm deutlich ausgebremst wird. Man kann also viel Performance rausholen, wenn der GC optimiert wird, oder der Programmierer dem GC Arbeit erspart, indem er die unnötig erzeugte Objekte spart oder z.B. Array-Caches nutzt. Solche Optimierungen können dir aber auch schwer in den Rücken fallen, da die vermeintliche Optimierung nur wenig Zeit spart, dadurch aber an anderer Stelle mehr Zeit verloren geht. Außerdem ist so optimierter Code meist deutlich komplizierter, was wiederum Raum für Fehler oder Performance-Probleme bietet.

Und dann gibt's noch viele Alternativen, die für die selbe Sprache unterschiedliche Wege gehen, z.B. gibt's viele C++-Compiler, die unterschiedlich optimiert sind und Python gibt's auch nicht nur als reine Skriptsprache und bei C# kann ich einstellen, wann er was kompilieren soll, zur Laufzeit, beim Start, beim Kompilieren, nur ein Teil, etc.

Woher ich das weiß:Berufserfahrung
alfredo153  31.08.2021, 09:02
Der Unterschied von Java ist deshalb relevant, da Java nach dem Kompilieren (standardmäßig) wieder wie eine Skriptsprache arbeitet, der eigene Zwischencode wird immer gelesen und ausgeführt.

Das ist wohl ein Missverständnis. Schon 1999 - bevor es überhaupt die CLR gab - hatte die JVM ihren Just-in-Time-Compiler ("HotSpot"). Der Bytecode wird seitdem keineswegs "immer gelesen und ausgeführt", sondern je nach Bedarf kompiliert - das kann vor der Ausführung sein oder erst nach einer gewissen Anzahl von Wiederholungen, und dann wiederum mit verschiedenen Instrumentierungs- und Optimierungsgraden ("tiered compilation").

Der kleine Unterschied zur CLR ist, dass das zur Laufzeit adaptiv entschieden wird ("mixed mode"), während die CLR traditionell immer vor Ausführung kompiliert. Auch Code, der nur ein einziges Mal ausgeführt wird.

0
Palladin007  31.08.2021, 10:34
@alfredo153

Dann habe ich das wohl falsch gelesen, danke für die Korrektur.
Leider weiß ich die Quelle nicht mehr, sonst würde ich sie hier verlinken.

0
Wie kann eine Programmiersprache schneller sein, als eine andere?

Eine Sprache ist umso schneller, je maschinennäher sie ist. Am schnellsten sind Maschinensprachen, gefolgt von Assemblersprachen, gefolgt von Compilersprachen wie C++, gefolgt von Interpretersprachen wie Python.

Vergleich das mit Deinen Sprachkenntnissen. Selbst wenn Du gut englisch sprichst, wirst Du als nicht Muttersprachler englische Anweisungen erst in Gedanken ins Deutsche übersetzen, bevor Du das Gesagte verstehst.

Diese Übersetzung von englisch nach deutsch kostet Zeit. In diesem Beispiel ist Englisch die "Interpretersprache" (vgl. Python) und Deutsch die Sprache, die Du nicht erst übersetzen musst (wie C++).

Noch schneller würdest Du reagieren, wenn es sich um einen Reflex handelt. Das wäre dann das Äquivalent zur Assemblersprache. Und ein direkter elektrischer Reiz Diener Nerven wäre das Äquivalent zur Maschinensprache.

Alex


Wenn Du maschinennah programmierst, ist der Code quasi schon in alle möglichen Schritte zerlegt, die die CPU direkt, mit möglichst wenig Zwischenschritten ausführen kann.

Am nächsten dran an Einsen und Nullen ist wohl Assembler.

Hier mal ein Beispiel von Wikipedia:

In Assembler gibst Du "Hallo Welt" so aus:

 ASSUME  CS:CODE, DS:DATA        ;- dem Assembler die Zuordnung der Segmentregister zu den Segmenten mitteilen

DATA    SEGMENT                 ;Beginn des Datensegments
Meldung db  "Hallo Welt"        ;- Zeichenkette „Hallo Welt“
        db  13, 10              ;- Neue Zeile
        db  "$"                 ;- Zeichen, das INT 21h, Unterfunktion 09h als Zeichenkettenende verwendet
DATA    ENDS                    ;Ende des Datensegments

CODE    SEGMENT                 ;Beginn des Codesegments
Anfang:                         ;- Einsprung-Label fuer den Anfang des Programms
        mov ax, DATA            ;- Adresse des Datensegments in das Register „AX“ laden
        mov ds, ax              ;  In das Segmentregister „DS“ uebertragen (das DS-Register kann nicht direkt mit einer Konstante beschrieben werden)
        mov dx, OFFSET Meldung  ;- die zum Datensegment relative Adresse des Textes in das „DX“ Datenregister laden
                                ;  die vollstaendige Adresse von „Meldung“ befindet sich nun im Registerpaar DS:DX
        mov ah, 09h             ;- die Unterfunktion 9 des Betriebssysteminterrupts 21h auswaehlen
        int 21h                 ;- den Betriebssysteminterrupt 21h aufrufen (hier erfolgt die Ausgabe des Textes am Schirm)
        mov ax, 4C00h           ;- die Unterfunktion 4Ch (Programmbeendigung) des Betriebssysteminterrupts 21h festlegen
        int 21h                 ;- diesen Befehl ausfuehren, damit wird die Kontrolle wieder an das Betriebssystem zurueckgegeben
CODE    ENDS                    ;Ende des Codesegments

END     Anfang                  ;- dem Assembler- und Linkprogramm den Programm-Einsprunglabel mitteilen
                                ;- dadurch erhaelt der Befehlszaehler beim Aufruf des Programmes diesen Wert

In Pascal erreichst Du die Ausgabe von "Hallo Welt" wie folgt.

Damit die CPU das umsetzen kann, sind natürlich eine Menge Zwischenschritte
nötig und das kostet Zeit, ist für den Menschen aber leichter zu lesen und natürlich auch zu erstellen:

program Hallo(output);
begin
  writeln('Hallo Welt')
end.
Woher ich das weiß:eigene Erfahrung

Das liegt letztlich an zwei Dingen, die nicht unabhängig voneinander sind.

  1. Design der Sprache.
  2. Design des Laufzeitsystems.

Manche Sprachen sind besonders dafür geeignet, in fixen Code übersetzbar zu sein. Der Code wird einmal kompiliert, zur Zeit der Ausführung muss nicht mehr irgendwas hinter den Kulissen entschieden werden - es wird einfach stupid ausgeführt. Das klassische Beispiel sind Assembler und C ("Assembler mit Zuckerguss", sagt Niklaus Wirth). Damit kann man sehr schnelle Programme schreiben (wenn man's kann), weil man sozusagen die volle Kontrolle darüber hat, was auf der CPU passiert. Aber man kann auch viel falsch machen und muss sehr viel "zu Fuß" arbeiten.

Auf dem anderen Ende des Spektrums gibt es Sprachen, die sehr dynamisch sind und dem Programmierer viel Arbeit abnehmen: da kann sich jederzeit alles ändern und das Laufzeitsystem macht viele Dinge automatisch im Hintergrund (etwa Speicherverwaltung). Du kannst sogar noch zur Laufzeit das Programm selbst ändern, Code nachladen, usw. Das wären vor allem jene, die man gern als "Skriptsprachen" bezeichnet: Ruby, Python, etc. und andere dynamische Sprachen wie Lisp. Diese Sprachen müssen zwangsläufig zur Laufzeit viel tun, das CPU kostet. Fast immer werden sie interpretiert, nicht kompiliert.

In der Praxis liegen die meisten irgendwo dazwischen. Bereits C++ hat eine gewisse Dynamik zur Laufzeit (virtuelle Methoden, RTTI, ...), die ein bisschen Performance kosten kann, wenn man sie verwendet. Viele moderne Sprachen verwenden einen kompilierten Zwischencode (etwa Java auf der JVM, C# auf der CLR) und bringen Speicherverwaltung mit. Python macht das auch, aber transparent. Andere Sprachen versuchen den Spagat durch besonders schlaue Compiler, die ein Laufzeitsystem möglichst ersetzen sollen (etwa Rust).

Grundsätzlich ist Geschwindigkeit nicht unbedingt eine Eigenschaft der Sprache sondern mehr der Ausführung der Programme welcher mit dieser Sprache geschrieben wurden.

Je nach Design der Sprache kann aber die Sprache in vollem Umfang nicht in jeder Umgebung ausgeführt werden.

C++ wird zB direkt in Maschinenbefehle kompiliert und erreicht daher die höchste Geschwindigkeit. Hier gibt es höchstens Unterschiede im verwendeten Compiler. Je nach Compiler kann dieser nämlich mehr oder auch weniger optimieren Maschinencode erzeugen, auch wenn man dem Compiler mit einem guten Quellcode unter die Arme greifen kann.

Python hat nun einige Sprachkonstrukte welche sich in direkterweise nicht unbedingt in Maschinensprache all zu gut abbilden lassen. Python ist nämlich als sogenannte Skriptsprache Designed, also als eine Sprache deren Quellcode von einem Programm gelesen wird und die einzelnen Instruktionen dann von diesem Programm interpretiert werden und erst dieses Programm steuert die Hardware. Dadurch erlaubt man viel mehr Abstraktion und zB auch das Dynamische lesen und ausführen von Python Code. Ein Pythonprogramm kann sich also mühelos während der Ausführung selbst umschreiben, das ist mit kompilierten Sprachen nur umständlich möglich.

C# und Java gehen dabei einen Mittelweg. Die erzeugen sogenannten Bytecode, den man sich vorstellen kann wie Maschinencode für einen virtuellen Prozessor. Da der Virtuelle Prozessor einfach mehr Dinge abbilden kann als der reale lassen sich so auch viele Dinge erreichen die in einer kompilierten Sprache nur umständlich sind. Der Virtuelle Prozessor nutzt dabei einen sogenannten JIT Compiler welcher den Bytecode zur Ausführung direkt in Maschinencode umschreibt welcher an die Hardware angepasst ist. Dadurch sind diese Sprachen zwischen Python und C++ angesiedelt.

Allgemein lassen sich aber alle Sprachen zumindest in einem Subset kompilieren. So gibt es zB für ein Subset von Python Compiler und dessen Code erreicht die Geschwindigkeit von C++