Warum können Hochsprachen scheinbar nie in Bezug auf die Geschwindigkeit Niedrigsprachen erreichen? Beispiele für Hochsprachen wären Python, Haskell und Java. Niedrigsprachige Sprachen wären schwieriger zu definieren, aber sagen wir C. Vergleiche finden sich überall im Internet und alle sind sich einig, dass C viel schneller ist, manchmal um den Faktor 10 oder mehr.
Was verursacht so große Leistungsunterschiede und warum können Hochsprachen nicht aufholen?
Anfangs glaubte ich, dass dies alles die Schuld der Compiler ist und dass sich die Dinge in Zukunft verbessern werden, aber einige der beliebtesten höheren Sprachen gibt es schon seit Jahrzehnten und sie bleiben in Bezug auf die Geschwindigkeit immer noch zurück. Können sie nicht einfach zu einem C-ähnlichen Syntaxbaum kompilieren und dann die gleichen Verfahren befolgen, mit denen der Maschinencode generiert wird? Oder hat es vielleicht etwas mit der Syntax selbst zu tun?
Beispiele:
Antworten:
Einige Mythen entlarven
Es gibt keine schnelle Sprache. Eine Sprache kann im Allgemeinen schnellen Code erzeugen, aber verschiedene Sprachen zeichnen sich durch unterschiedliche Benchmarks aus. Wir können Sprachen anhand eines bestimmten Satzes fehlerhafter Benchmarks bewerten, Sprachen jedoch nicht im luftleeren Raum.
C-Code ist in der Regel schneller, weil Benutzer, die jeden Zentimeter Leistung benötigen, C verwenden. Eine Statistik, dass C "um den Faktor 10" schneller ist, ist möglicherweise ungenau, da es den Benutzern von Python möglicherweise weniger wichtig ist über Geschwindigkeit und schrieb keinen optimalen Python-Code. Wir sehen dies insbesondere bei Sprachen wie Haskell. Wenn Sie sich wirklich anstrengen , können Sie Haskell schreiben, das mit C vergleichbar ist. Aber die meisten Leute brauchen diese Leistung nicht, deshalb haben wir eine Reihe fehlerhafter Vergleiche.
Manchmal ist es die Unsicherheit, nicht die Abstraktion, die C schnell macht. Das Fehlen von Array-Grenzen und Null-Zeiger-Überprüfungen spart Zeit und hat im Laufe der Jahre zahlreiche Sicherheitslücken verursacht.
Sprachen sind nicht schnell, Implementierungen sind schnell. Viele abstrakte Sprachen beginnen langsam, weil Geschwindigkeit nicht ihr Ziel ist, sondern werden schneller, wenn immer mehr Optimierungen hinzugefügt werden.
Der Kompromiss zwischen Abstraktion und Geschwindigkeit ist möglicherweise ungenau. Ich würde einen besseren Vergleich vorschlagen:
Einfachheit, Geschwindigkeit, Abstraktion: Wählen Sie zwei.
Wenn wir identische Algorithmen in verschiedenen Sprachen ausführen, stellt sich die Frage nach der Geschwindigkeit auf das Problem: "Wie viel müssen wir zur Laufzeit tun, damit dies funktioniert?"
In einer sehr abstrakten Sprache , die einfach ist , wie Python oder JavaScript, da wir die Darstellung von Daten erst zur Laufzeit kennen, kommt es zur Laufzeit zu vielen Zeiger-De-Referenzierungen und dynamischen Überprüfungen, die langsam sind.
Ebenso müssen viele Überprüfungen durchgeführt werden, um sicherzustellen, dass Sie Ihren Computer nicht beschädigen. Wenn Sie eine Funktion in Python aufrufen, muss sichergestellt werden, dass das aufgerufene Objekt tatsächlich eine Funktion ist, da Sie sonst möglicherweise zufälligen Code ausführen, der schreckliche Dinge tut.
Schließlich haben die meisten abstrakten Sprachen Overhead durch die Speicherbereinigung. Die meisten Programmierer sind sich einig, dass es schmerzhaft ist, die Lebensdauer des dynamisch zugewiesenen Speichers zu verfolgen, und sie möchten lieber, dass ein Garbage Collector dies zur Laufzeit für sie erledigt. Dies braucht Zeit, die ein C-Programm nicht für GC ausgeben muss.
Zusammenfassung Bedeutet nicht langsam
Es gibt jedoch Sprachen, die sowohl abstrakt als auch schnell sind. Das dominanteste in dieser Ära ist Rust. Durch die Einführung eines Ausleihprüfers und eines ausgeklügelten Typsystems wird abstrakter Code ermöglicht und Informationen zur Kompilierungszeit verwendet, um den Arbeitsaufwand zur Laufzeit (z. B. Speicherbereinigung) zu reduzieren.
Jede statisch typisierte Sprache spart uns Zeit, indem sie die Anzahl der Laufzeitprüfungen reduziert, und führt zu Komplexität, da wir zur Kompilierungszeit einen Typechecker benötigen. Wenn wir eine Sprache haben, die codiert, ob ein Wert in seinem Typsystem null sein kann, können wir mit Nullzeigerprüfungen zur Kompilierungszeit Zeit sparen.
quelle
Hier sind einige wichtige Ideen dazu.
Ein einfacher Vergleich / eine Fallstudie, um Wrt-Abstraktion gegen Geschwindigkeit zu machen, ist Java gegen C ++. Java wurde entwickelt, um einige der untergeordneten Aspekte von C ++ wie die Speicherverwaltung zu abstrahieren. In den frühen Tagen (um die Erfindung der Sprache Mitte der 1990er Jahre) war die Java-Müllerkennung nicht sehr schnell, aber nach einigen Jahrzehnten der Forschung sind die Müllsammler extrem fein abgestimmt / schnell / optimiert, so dass die Müllsammler als Performance Drain auf Java. siehe auch diese Überschrift von 1998: Leistungstests zeigen Java so schnell wie C ++ / javaworld
Programmiersprachen und ihre lange Entwicklung haben eine inhärente "pyramidenförmige / hierarchische Struktur" als eine Art transzendentes Entwurfsmuster. An der Spitze der Pyramide befindet sich etwas, das andere untere Abschnitte der Pyramide steuert. Mit anderen Worten, Bausteine bestehen aus Bausteinen. Dies ist auch in der API-Struktur zu sehen. In diesem Sinne führt eine größere Abstraktion immer zu einer neuen Komponente an der Spitze der Pyramide, die andere Komponenten steuert. In gewissem Sinne ist es nicht so sehr so, dass alle Sprachen gleich sind, sondern dass Sprachen Routinen in anderen Sprachen erfordern. Beispielsweise rufen viele Skriptsprachen (Python / Ruby) häufig C- oder C ++ - Bibliotheken auf. Numerische oder Matrix-Routinen sind ein typisches Beispiel dafür. Es gibt also höhere und niedrigere Sprachen, und die höheren Sprachen nennen sozusagen niedrigere Sprachen. In diesem Sinne ist die Messung der Relativgeschwindigkeit nicht wirklich vergleichbar.
Man könnte sagen, dass ständig neue Sprachen erfunden werden, um zu versuchen, den Kompromiss zwischen Abstraktion und Geschwindigkeit zu optimieren, dh das wichtigste Designziel. Vielleicht ist es nicht so sehr so, dass eine größere Abstraktion immer die Geschwindigkeit opfert, sondern dass bei neueren Designs immer ein besseres Gleichgewicht angestrebt wird. Beispielsweise wurde Google Go in vielerlei Hinsicht speziell im Hinblick auf den Kompromiss optimiert, um gleichzeitig sowohl hochrangig als auch performant zu sein. siehe zB Google Go: Warum die Programmiersprache von Google mit Java in der Enterprise / Techworld mithalten kann
quelle
Ich denke gerne über Leistung nach, "wo der Gummi auf die Straße trifft". Der Computer führt Anweisungen aus, keine Abstraktionen.
Was ich sehen möchte, ist Folgendes: Wird jede Anweisung, die ausgeführt wird, "verdient", indem sie wesentlich zum Endergebnis beiträgt? Als zu einfaches Beispiel sollten Sie einen Eintrag in einer Tabelle mit 1024 Einträgen nachschlagen. Dies ist ein 10-Bit-Problem, da das Programm 10 Bit "lernen" muss, bevor es die Antwort kennt. Wenn es sich bei dem Algorithmus um eine binäre Suche handelt, trägt jede Iteration 1 Informationsbit bei, da die Unsicherheit um den Faktor 2 verringert wird. Daher sind 10 Iterationen erforderlich, eine für jedes Bit.
Die lineare Suche ist dagegen zunächst sehr ineffizient, da die ersten Iterationen die Unsicherheit um einen sehr kleinen Faktor verringern. Sie lernen also nicht viel für den Aufwand.
OK, wenn der Compiler dem Benutzer erlauben kann, gute Anweisungen auf eine als "abstrakt" bezeichnete Weise zu verpacken, ist das in Ordnung.
quelle
Die Abstraktion reduziert naturgemäß die Übermittlung von Informationen sowohl an den Programmierer als auch an die unteren Ebenen des Systems (Compiler, Bibliotheken und Laufzeitsystem). Dies ermöglicht es den unteren Schichten im Allgemeinen zugunsten der Abstraktion anzunehmen, dass der Programmierer sich nicht mit einem nicht spezifizierten Verhalten befasst, was eine größere Flexibilität bei der Bereitstellung des spezifizierten Verhaltens bietet .
Ein Beispiel für einen potenziellen Nutzen dieses Aspekts "egal" ist das Datenlayout. In C (niedrige Abstraktion) ist der Compiler bei der Optimierung des Datenlayouts stärker eingeschränkt. Selbst wenn der Compiler (z. B. durch Profilinformationen) erkennen könnte, dass Optimierungen zur Vermeidung von Hot / Cold oder zur Vermeidung falscher Freigabe von Vorteil sind, wird im Allgemeinen verhindert, dass solche Optimierungen angewendet werden. (Es gibt eine gewisse Freiheit, "als ob" anzugeben, dh die Spezifikation abstrakter zu behandeln, aber das Ableiten aller möglichen Nebenwirkungen belastet den Compiler zusätzlich.)
Eine abstraktere Spezifikation ist auch robuster gegen Änderungen bei Kompromissen und Verwendungen. Die unteren Schichten sind weniger gezwungen, das Programm für neue Systemeigenschaften oder neue Verwendungen neu zu optimieren. Eine konkretere Spezifikation muss entweder von einem Programmierer neu geschrieben werden, oder die unteren Schichten müssen zusätzliche Anstrengungen unternehmen, um ein "als ob" -Verhalten zu gewährleisten.
Der leistungsbeeinträchtigende Aspekt der Informationsverstecke ist "kann nicht ausdrücken", was die unteren Ebenen normalerweise als "weiß nicht" behandeln. Dies bedeutet, dass die unteren Schichten Informationen, die für die Optimierung nützlich sind, von anderen Mitteln wie der typischen allgemeinen Verwendung, der gezielten Verwendung oder spezifischen Profilinformationen unterscheiden müssen.
Die Auswirkungen des Versteckens von Informationen wirken auch in die andere Richtung. Der Programmierer kann produktiver sein, indem er nicht jedes Detail berücksichtigen und spezifizieren muss, aber der Programmierer verfügt möglicherweise über weniger Informationen über die Auswirkungen von Entwurfsentscheidungen auf höherer Ebene.
Wenn Code hingegen spezifischer (weniger abstrakt) ist, können die unteren Schichten des Systems einfacher das tun, was ihnen gesagt wird, als ihnen gesagt wird. Wenn der Code für die gezielte Verwendung gut geschrieben ist, passt er gut zur gezielten Verwendung. Eine weniger abstrakte Sprache (oder ein Programmierparadigma) ermöglicht es dem Programmierer , die Implementierung durch detailliertes Design und durch die Verwendung von Informationen zu optimieren, die in einer bestimmten Sprache nicht einfach an die unteren Schichten übermittelt werden können.
Wie bereits erwähnt, sind weniger abstrakte Sprachen (oder Programmiertechniken) attraktiv, wenn zusätzliche Fähigkeiten und Anstrengungen des Programmierers zu lohnenden Ergebnissen führen können. Wenn mehr Aufwand und Fähigkeiten des Programmierers angewendet werden, sind die Ergebnisse in der Regel besser. Darüber hinaus wird ein Sprachsystem, das weniger für leistungskritische Anwendungen verwendet wird (stattdessen wird der Entwicklungsaufwand oder die Zuverlässigkeit betont - bei Grenzprüfungen und der Speicherbereinigung geht es nicht nur um die Produktivität des Programmierers, sondern auch um die Korrektheit. Durch die Verringerung der mentalen Belastung des Programmierers durch Abstraktion kann die Zuverlässigkeit verbessert werden.) wird weniger Druck haben, um die Leistung zu verbessern.
Die Spezifität widerspricht auch dem Prinzip, sich nicht zu wiederholen, da die Optimierung normalerweise durch Anpassen des Codes an eine bestimmte Verwendung möglich ist. Dies hat offensichtliche Auswirkungen auf die Zuverlässigkeit und den Programmieraufwand.
Die von einer Sprache bereitgestellten Abstraktionen können auch unerwünschte oder unnötige Arbeiten enthalten, ohne dass eine weniger schwere Abstraktion gewählt werden kann. Während unnötige Arbeit manchmal von den unteren Schichten entdeckt und entfernt werden kann (z. B. können Grenzprüfungen aus dem Körper einer Schleife extrahiert und in einigen Fällen vollständig entfernt werden), erfordert die Feststellung, dass dies eine gültige Optimierung ist, mehr "Geschick und Aufwand" durch der Compiler.
Das Alter und die Popularität der Sprache sind ebenfalls bemerkenswerte Faktoren sowohl für die Verfügbarkeit qualifizierter Programmierer als auch für die Qualität der unteren Schichten des Systems (einschließlich ausgereifter Bibliotheken und Codebeispiele).
Ein weiterer Konfliktfaktor bei solchen Vergleichen ist der etwas orthogonale Unterschied zwischen der Kompilierung vor der Zeit und der Justierung in der Zeit. Während die Just-in-Time-Kompilierung Profilinformationen (die sich nicht auf den Programmierer verlassen müssen, um Profilläufe bereitzustellen) und die systemspezifische Optimierung (eine vorzeitige Kompilierung kann auf eine breitere Kompatibilität abzielen) leichter nutzen kann, wird der Aufwand für aggressive Optimierung als berücksichtigt Teil der Laufzeitleistung. JIT-Ergebnisse können zwischengespeichert werden, wodurch der Overhead für häufig verwendeten Code reduziert wird. (Die Alternative der binären Neuoptimierung kann einige Vorteile der JIT-Kompilierung bieten, aber herkömmliche binäre Verteilungsformate lassen die meisten Quellcodeinformationen fallen und zwingen das System möglicherweise dazu, zu versuchen, die Absicht einer bestimmten Implementierung zu erkennen.)
(Niedrigere Abstraktionssprachen bevorzugen aufgrund ihrer Betonung der Programmiererkontrolle die Verwendung der Kompilierung vor der Zeit. Die Kompilierung zur Installationszeit wird möglicherweise toleriert, obwohl die Auswahl der Implementierung zur Verbindungszeit eine bessere Kontrolle der Programmierer ermöglichen würde. Die JIT-Kompilierung opfert eine signifikante Kontrolle. )
Es gibt auch das Problem der Benchmarking-Methodik. Gleiche Anstrengungen / Fähigkeiten können effektiv nicht festgestellt werden, aber selbst wenn dies erreicht werden könnte, würden die Sprachziele die Ergebnisse beeinflussen. Wenn eine geringe maximale Programmierzeit erforderlich wäre, könnte ein Programm für eine weniger abstrakte Sprache im Vergleich zu einem einfachen idiomatischen Ausdruck in einer abstrakteren Sprache möglicherweise nicht vollständig geschrieben werden. Wenn eine hohe maximale Programmierzeit / -aufwand zulässig wäre, hätten Sprachen mit geringerer Abstraktion einen Vorteil. Benchmarks, die Best-Effort-Ergebnisse liefern, wären natürlich zugunsten weniger abstrakter Sprachen voreingenommen.
Es ist manchmal möglich, in einer Sprache weniger idiomatisch zu programmieren, um die Vorteile anderer Programmierparadigmen zu nutzen, aber selbst wenn die Ausdruckskraft verfügbar ist, sind die Kompromisse dafür möglicherweise nicht günstig.
quelle