Was stützt die Behauptung, dass C ++ schneller sein kann als eine JVM oder CLR mit JIT? [geschlossen]

119

Ein wiederkehrendes Thema zu SE, das mir in vielen Fragen aufgefallen ist, ist das anhaltende Argument, dass C ++ schneller und / oder effizienter ist als übergeordnete Sprachen wie Java. Das Gegenargument ist, dass moderne JVM oder CLR dank JIT und so weiter für eine wachsende Anzahl von Aufgaben genauso effizient sein können und dass C ++ nur dann effizienter ist, wenn Sie wissen, was Sie tun und warum Sie die Dinge auf eine bestimmte Weise tun wird Leistungssteigerungen verdienen. Das liegt auf der Hand und ist durchaus sinnvoll.

Ich möchte eine grundlegende Erklärung wissen (wenn es so etwas gibt ...), warum und wie bestimmte Aufgaben in C ++ schneller sind als in der JVM oder CLR? Liegt es einfach daran, dass C ++ in Maschinencode kompiliert wird, während die JVM oder CLR zur Laufzeit noch den Verarbeitungsaufwand für die JIT-Kompilierung haben?

Wenn ich versuche, das Thema zu recherchieren, finde ich nur die gleichen Argumente, die ich oben skizziert habe, ohne detaillierte Informationen darüber zu haben, wie C ++ genau für Hochleistungscomputer verwendet werden kann.

Anonym
quelle
Die Leistung hängt auch von der Komplexität des Programms ab.
Pandu
23
Ich füge hinzu: "C ++ ist nur dann effizienter, wenn Sie wissen, was Sie tun und warum eine bestimmte Vorgehensweise Leistungssteigerungen verdient." Mit den Worten, es ist nicht nur eine Frage des Wissens, es ist eine Frage der Entwicklerzeit. Es ist nicht immer effizient, die Optimierung zu maximieren. Aus diesem Grund gibt es (unter anderem) höhere Programmiersprachen wie Java und Python, um die Zeit zu verkürzen, die ein Programmierer für die Ausführung einer bestimmten Aufgabe auf Kosten einer hochgradig abgestimmten Optimierung benötigt.
Joel Cornett
4
@ Joel Cornett: Ich stimme vollkommen zu. In Java bin ich definitiv produktiver als in C ++ und ich denke nur an C ++, wenn ich wirklich schnellen Code schreiben muss. Andererseits habe ich gesehen, dass schlecht geschriebener C ++ - Code sehr langsam ist: C ++ ist für unerfahrene Programmierer weniger nützlich.
Giorgio
3
Jede Kompilierungsausgabe, die von einer JIT erstellt werden kann, kann von C ++ erstellt werden. Code, der von C ++ erstellt werden kann, muss jedoch nicht unbedingt von einer JIT erstellt werden. Daher sind die Fähigkeiten und Leistungsmerkmale von C ++ eine Obermenge derjenigen einer höheren Sprache. QED
tylerl
1
@Doval Technisch gesehen stimmt das, aber in der Regel können Sie die möglichen Laufzeitfaktoren, die sich auf die Leistung eines Programms auswirken, auf der einen Seite zählen. In der Regel ohne Verwendung von mehr als zwei Fingern. Im schlimmsten Fall versenden Sie also mehrere Binärdateien. Es stellt sich jedoch heraus, dass Sie dies nicht einmal tun müssen, da die potenzielle Beschleunigung vernachlässigbar ist, weshalb sich niemand darum kümmert.
Tyler

Antworten:

200

Es geht nur um die Erinnerung (nicht um die JIT). Der JIT-Vorteil gegenüber C besteht hauptsächlich darin, virtuelle oder nicht virtuelle Anrufe durch Inlining zu optimieren, woran der CPU-BTB bereits hart arbeitet.

In modernen Computern ist der Zugriff auf den Arbeitsspeicher sehr langsam (im Vergleich zu allem, was die CPU tut), was bedeutet, dass Anwendungen, die den Cache so oft wie möglich nutzen (was einfacher ist, wenn weniger Arbeitsspeicher verwendet wird), bis zu hundertmal schneller sein können als solche nicht. Es gibt viele Möglichkeiten, wie Java mehr Speicher als C ++ verwendet und es schwieriger macht, Anwendungen zu schreiben, die den Cache voll ausnutzen:

  • Es gibt einen Speicheroverhead von mindestens 8 Bytes für jedes Objekt, und die Verwendung von Objekten anstelle von Grundelementen ist an vielen Stellen erforderlich oder bevorzugt (nämlich bei den Standardauflistungen).
  • Zeichenfolgen bestehen aus zwei Objekten und haben einen Overhead von 38 Byte
  • UTF-16 wird intern verwendet, was bedeutet, dass jedes ASCII-Zeichen zwei Bytes anstelle von einem benötigt (die Oracle JVM hat kürzlich eine Optimierung eingeführt, um dies für reine ASCII-Zeichenfolgen zu vermeiden).
  • Es gibt keinen aggregierten Referenztyp (dh Strukturen), und es gibt wiederum keine Arrays von aggregierten Referenztypen. Ein Java-Objekt oder ein Array von Java-Objekten weist im Vergleich zu C-Strukturen und Arrays eine sehr schlechte L1 / L2-Cache-Lokalität auf.
  • Java-Generics verwenden die Typlöschung, die im Vergleich zur Typinstanziierung eine schlechte Cache-Lokalität aufweist.
  • Die Objektzuordnung ist undurchsichtig und muss für jedes Objekt separat vorgenommen werden. Daher ist es für eine Anwendung nicht möglich, die Daten bewusst cachefreundlich anzuordnen und sie dennoch als strukturierte Daten zu behandeln.

Einige andere Faktoren, die sich auf den Arbeitsspeicher beziehen, jedoch nicht auf den Cache:

  • Es gibt keine Stapelzuweisung, daher müssen sich alle nicht primitiven Daten, mit denen Sie arbeiten, auf dem Heap befinden und die Garbage Collection durchlaufen (einige aktuelle JITs weisen Stapel in bestimmten Fällen hinter den Kulissen zu).
  • Da es keine aggregierten Referenztypen gibt, erfolgt keine Stapelübergabe von aggregierten Referenztypen. (Denken Sie an eine effiziente Übergabe von Vektorargumenten.)
  • Garbage Collection kann den Inhalt des L1 / L2-Caches beeinträchtigen, und GC Stop-the-World-Pausen beeinträchtigen die Interaktivität.
  • Das Konvertieren zwischen Datentypen erfordert immer das Kopieren. Sie können keinen Zeiger auf eine Reihe von Bytes nehmen, die Sie von einem Socket erhalten haben, und sie als Float interpretieren.

Einige dieser Dinge sind Kompromisse (manuelle Speicherverwaltung ist es für die meisten Menschen wert, viel Leistung aufzugeben ), andere sind wahrscheinlich das Ergebnis des Versuchs, Java einfach zu halten, und andere sind Designfehler (wenn auch möglicherweise nur im Nachhinein) (UTF-16 war nämlich eine Kodierung mit fester Länge, als Java erstellt wurde, was die Entscheidung, es auszuwählen, viel verständlicher macht).

Es ist erwähnenswert, dass viele dieser Kompromisse für Java / JVM sehr unterschiedlich sind als für C # / CIL. Die .NET-CIL enthält Strukturen vom Referenztyp, Stapelzuweisung / -übergabe, gepackte Arrays von Strukturen und typinstanziierte Generika.

Michael Borgwardt
quelle
37
+1 - Insgesamt ist dies eine gute Antwort. Ich bin mir jedoch nicht sicher, ob der Aufzählungspunkt "Es gibt keine Stapelzuweisung" völlig korrekt ist. Java-JITs führen häufig eine Escape-Analyse durch, um die Stapelzuweisung zu ermöglichen. Vielleicht sollten Sie sagen, dass der Programmierer aufgrund der Java-Sprache nicht entscheiden kann, wann ein Objekt im Vergleich zur Heap-Zuweisung gestapelt wird. Wenn ein Garbage Collector der Generation (den alle modernen JVMs verwenden) verwendet wird, bedeutet "Heap Allocation" außerdem eine völlig andere Sache (mit völlig anderen Leistungsmerkmalen) als in einer C ++ - Umgebung.
Daniel Pryden
5
Ich würde denken, dass es zwei andere Dinge gibt, aber ich arbeite meistens mit Sachen auf einer viel höheren Ebene, also sag mir, ob ich falsch liege. Sie können C ++ nicht wirklich schreiben, ohne ein allgemeineres Bewusstsein dafür zu entwickeln, was im Arbeitsspeicher tatsächlich vor sich geht und wie Maschinencode tatsächlich funktioniert, während Skriptsprachen oder Sprachen für virtuelle Maschinen all diese Dinge Ihrer Aufmerksamkeit entziehen. Sie können außerdem die Funktionsweise von Dingen präziser steuern, während Sie sich in einer VM oder einer interpretierten Sprache darauf verlassen können, welche Kernbibliotheksautoren für ein zu spezifisches Szenario optimiert haben.
Erik Reppen
18
+1. Eine weitere Sache, die ich hinzufügen möchte (aber nicht bereit bin, eine neue Antwort zu übermitteln): Die Array-Indizierung in Java beinhaltet immer die Überprüfung der Grenzen. Bei C und C ++ ist dies nicht der Fall.
Riwalk
7
Es ist erwähnenswert, dass die Heap-Zuweisung in Java (aufgrund von internen Pools und Dingen) erheblich schneller ist als in einer naiven Version mit C ++. Die Speicherzuweisung in C ++ kann jedoch erheblich besser sein, wenn Sie wissen, was Sie tun.
Brendan Long
10
@BrendanLong, true .. aber nur, wenn der Speicher sauber ist - sobald eine App eine Weile ausgeführt wird, wird die Speicherzuweisung aufgrund der Notwendigkeit einer GC langsamer, was die Dinge dramatisch verlangsamt, da Speicher freigegeben, Finalisierer ausgeführt und dann kompakt. Es ist ein Kompromiss, der Benchmarks zugute kommt, aber (meiner Meinung nach) die Apps insgesamt verlangsamt.
gbjbaanb
67

Liegt es einfach daran, dass C ++ in Assembly- / Maschinencode kompiliert wird, während Java / C # zur Laufzeit noch den Verarbeitungsaufwand für die JIT-Kompilierung hat?

Teilweise, aber im Allgemeinen, unter der Annahme eines absolut fantastischen JIT-Compilers auf dem neuesten Stand der Technik, kann richtiger C ++ - Code aus ZWEI Hauptgründen immer noch eine bessere Leistung als Java-Code erbringen:

1) C ++ Vorlagen bieten bessere Möglichkeiten für das Schreiben von Code , das sowohl allgemeine und effizient . Templates bieten dem C ++ - Programmierer eine sehr nützliche Abstraktion mit ZERO-Laufzeitaufwand. (Templates sind im Prinzip Enten-Typisierungen zur Kompilierungszeit.) Im Gegensatz dazu erhalten Sie mit Java-Generika im Grunde genommen nur virtuelle Funktionen. Virtuelle Funktionen haben immer einen Laufzeit-Overhead und können im Allgemeinen nicht eingebunden werden.

In den meisten Sprachen, einschließlich Java, C # und sogar C, können Sie zwischen Effizienz und Allgemeingültigkeit / Abstraktion wählen. C ++ - Vorlagen bieten Ihnen beides (auf Kosten längerer Kompilierzeiten).

2) Die Tatsache, dass der C ++ - Standard nicht viel über das binäre Layout eines kompilierten C ++ - Programms zu sagen hat, gibt C ++ - Compilern viel mehr Spielraum als einem Java-Compiler und ermöglicht bessere Optimierungen (was manchmal zu größeren Schwierigkeiten beim Debuggen führt). ) Tatsächlich erzwingt die Natur der Java-Sprachspezifikation in bestimmten Bereichen eine Leistungsbeeinträchtigung. Sie können beispielsweise kein zusammenhängendes Array von Objekten in Java haben. Sie können nur ein zusammenhängendes Array von Objektzeigern haben(Referenzen), was bedeutet, dass das Iterieren über ein Array in Java immer die Kosten für die Indirektion verursacht. Die Wertesemantik von C ++ ermöglicht jedoch zusammenhängende Arrays. Ein weiterer Unterschied ist die Tatsache, dass C ++ das Zuweisen von Objekten auf dem Stapel ermöglicht, während Java dies nicht zulässt. In der Praxis sind die Zuweisungskosten häufig nahe Null, da die meisten C ++ - Programme dazu neigen, Objekte auf dem Stapel zuzuweisen.

Ein Bereich, in dem C ++ möglicherweise hinter Java zurückbleibt, ist eine Situation, in der viele kleine Objekte auf dem Heap zugeordnet werden müssen. In diesem Fall führt das Garbage Collection-System von Java wahrscheinlich zu einer besseren Leistung als Standard newund deletein C ++, da die Java-GC die Freigabe von Massendaten ermöglicht. Wiederum kann ein C ++ - Programmierer dies durch die Verwendung eines Speicherpools oder eines Plattenzuordners ausgleichen, wohingegen ein Java-Programmierer keinen Rückgriff hat, wenn er mit einem Speicherzuordnungsmuster konfrontiert wird, für das die Java-Laufzeit nicht optimiert ist.

Weitere Informationen zu diesem Thema finden Sie in dieser hervorragenden Antwort .

Charles Salvia
quelle
6
Gute Antwort, aber ein kleiner Punkt: "C ++ - Vorlagen bieten Ihnen beides (auf Kosten längerer Kompilierzeiten.)" Ich würde auch auf Kosten größerer Programme hinzufügen. Könnte nicht immer ein Problem sein, aber wenn für mobile Geräte entwickelt, kann es definitiv sein.
Leo
9
@luiscubal: Nein, in dieser Hinsicht sind C # -Generiken sehr Java-artig (da derselbe "generische" Codepfad verwendet wird, unabhängig davon, welche Typen durchlaufen werden). Der Trick bei C ++ - Vorlagen besteht darin, dass der Code einmal für instanziiert wird Jeder Typ, auf den es angewendet wird. Es handelt sich also std::vector<int>um ein dynamisches Array, das nur für Ints entwickelt wurde, und der Compiler kann es entsprechend optimieren. AC # List<int>ist immer noch nur ein List.
Jalf
12
@jalf C # List<int>verwendet eine int[], nicht Object[]wie Java. Siehe stackoverflow.com/questions/116988/…
Luiscubal
5
@ Luiscubal: Ihre Terminologie ist unklar. Die JIT handelt nicht zu der Zeit, die ich als "Kompilierungszeit" betrachte. Sie haben natürlich Recht, wenn man einen hinreichend cleveren und aggressiven JIT-Compiler voraussetzt, sind den Möglichkeiten praktisch keine Grenzen gesetzt. C ++ erfordert dieses Verhalten. Mit C ++ - Vorlagen kann der Programmierer außerdem explizite Spezialisierungen angeben und gegebenenfalls zusätzliche explizite Optimierungen vornehmen. C # hat dafür kein Äquivalent. Zum Beispiel könnte ich in C ++ definieren, vector<N>wo für den speziellen Fall vector<4>meiner
handcodierten
5
@Leo: Vor 15 Jahren war es ein Problem, Code in Vorlagen aufzublähen. Aufgrund der umfangreichen Templatisierung und Inlining-Funktionen sowie der Fähigkeiten, die Compiler erworben haben (wie das Falten identischer Instanzen), wird heutzutage viel Code durch Vorlagen kleiner .
sbi
46

Was die anderen Antworten (bisher 6) vergessen zu haben scheinen zu erwähnen, aber was ich für sehr wichtig halte, um darauf zu antworten, ist eine der sehr grundlegenden Design-Philosophien von C ++, die Stroustrup vom ersten Tag an formuliert und angewendet hat:

Sie zahlen nicht für das, was Sie nicht nutzen.

Es gibt einige andere wichtige zugrunde liegende Designprinzipien, die C ++ stark geprägt haben (so dass Sie nicht zu einem bestimmten Paradigma gezwungen werden sollten), aber Sie zahlen nicht für das, was Sie nicht verwenden, und das ist genau dort, wo es am wichtigsten ist.


In seinem Buch The Design and Evolution of C ++ (normalerweise als [D & E] bezeichnet) beschreibt Stroustrup, welche Bedürfnisse er hatte, die ihn überhaupt zu C ++ veranlassten. In meinen eigenen Worten: Für seine Doktorarbeit (etwas mit Netzwerksimulationen zu tun, IIRC) implementierte er ein System in SIMULA, das ihm sehr gut gefiel, weil die Sprache es ihm sehr gut ermöglichte, seine Gedanken direkt im Code auszudrücken. Das resultierende Programm lief jedoch viel zu langsam, und um einen Abschluss zu bekommen, schrieb er das Ding in BCPL, einem Vorgänger von C. Den Code in BCPL zu schreiben, den er als Schmerz beschreibt, aber das resultierende Programm war schnell genug, um zu liefern Ergebnisse, die es ihm ermöglichten, seine Promotion zu beenden.

Danach wünschte er sich eine Sprache, mit der reale Probleme so direkt wie möglich in Code übersetzt werden können, der Code aber auch sehr effizient sein kann.
Daraufhin schuf er das, was später zu C ++ wurde.


Das oben angeführte Ziel ist also nicht nur eines von mehreren grundlegenden Konstruktionsprinzipien, es liegt auch sehr nahe am Sinn und Zweck von C ++. Und es ist fast überall in der Sprache zu finden: Funktionen sind nur virtualdann verfügbar, wenn Sie dies möchten (da das Aufrufen von virtuellen Funktionen mit einem geringen Aufwand verbunden ist). PODs werden nur dann automatisch initialisiert, wenn Sie dies ausdrücklich anfordern. Ausnahmen kosten Sie nur dann Leistung, wenn Sie dies tatsächlich tun werfen sie (während es ein explizites Designziel war, die Einrichtung / Bereinigung von Stackframes sehr billig zu machen), kein GC läuft, wann immer es sich anfühlt, etc.

C ++ hat sich ausdrücklich dafür entschieden, Ihnen einige Vorteile zu ersparen ("muss ich diese Methode hier virtuell machen?"), Um die Leistung zu verbessern ("nein, das tue ich nicht, und jetzt kann der Compiler dies tun inlineund das Heck aus dem heraus optimieren Ganzes! "), und es überrascht nicht, dass dies tatsächlich zu Leistungsgewinnen im Vergleich zu Sprachen führte, die praktischer sind.

sbi
quelle
4
Sie zahlen nicht für das, was Sie nicht nutzen. => und dann fügten sie RTTI hinzu :(
Matthieu M.
11
@Matthieu: Obwohl ich Ihr Gefühl verstehe, kann ich nicht anders, als zu bemerken, dass auch das mit Sorgfalt in Bezug auf die Leistung hinzugefügt wurde. RTTI ist so festgelegt, dass es mithilfe virtueller Tabellen implementiert werden kann und daher nur einen geringen Overhead verursacht, wenn Sie es nicht verwenden. Wenn Sie keinen Polymorphismus verwenden, entstehen keinerlei Kosten. Vermisse ich etwas?
sbi
9
@ Matthieu: Natürlich gibt es Grund. Aber ist dieser Grund vernünftig? Soweit ich sehen kann, sind die "Kosten von RTTI", wenn sie nicht verwendet werden, ein zusätzlicher Zeiger in der virtuellen Tabelle jeder polymorphen Klasse, der auf ein statisch irgendwo zugewiesenes RTTI-Objekt zeigt. Wenn Sie den Chip nicht in meinem Toaster programmieren möchten, wie könnte dies jemals relevant sein?
sbi
4
@Aaronaught: Ich weiß nicht, was ich darauf antworten soll. Haben Sie meine Antwort wirklich einfach verworfen, weil sie auf die zugrunde liegende Philosophie hinweist, die Stroustrup et al dazu veranlasst hat, Funktionen so hinzuzufügen, dass Leistung möglich ist, anstatt diese Methoden und Funktionen einzeln aufzulisten?
sbi
9
@Aaronaught: Du hast mein Mitgefühl.
sbi
29

Kennen Sie das Google Research Paper zu diesem Thema?

Aus dem Fazit:

Wir stellen fest, dass C ++ in Bezug auf die Leistung mit großem Abstand gewinnt. Es waren jedoch auch die umfangreichsten Optimierungsanstrengungen erforderlich, von denen viele auf einem Niveau durchgeführt wurden, das dem durchschnittlichen Programmierer nicht zur Verfügung stand.

Dies ist zumindest teilweise eine Erklärung im Sinne von "weil C ++ - Compiler in der realen Welt durch empirische Maßnahmen schneller Code produzieren als Java-Compiler".

Doc Brown
quelle
4
Neben den Unterschieden bei der Speicher- und Cache-Nutzung ist der Umfang der durchgeführten Optimierung einer der wichtigsten. Vergleichen Sie, wie viele Optimierungen GCC / LLVM (und wahrscheinlich Visual C ++ / ICC) im Vergleich zum Java HotSpot-Compiler durchführen: viel mehr, insbesondere in Bezug auf Schleifen, die Beseitigung redundanter Verzweigungen und die Zuweisung von Registern. JIT-Compiler haben normalerweise keine Zeit für diese aggressiven Optimierungen, obwohl sie dachten, sie könnten mit den verfügbaren Laufzeitinformationen besser implementiert werden.
Gratian Lup
2
@GratianLup: Ich frage mich, ob das mit LTO (noch) stimmt.
Deduplizierer
2
@GratianLup: Lassen Sie uns nicht Profil-geführte Optimierung für C ++ vergessen ...
Deduplicator
23

Dies ist kein Duplikat Ihrer Fragen, aber die akzeptierte Antwort beantwortet die meisten Ihrer Fragen: Eine moderne Überprüfung von Java

Um zusammenzufassen:

Grundsätzlich schreibt die Semantik von Java vor, dass es eine langsamere Sprache als C ++ ist.

Je nachdem, mit welcher anderen Sprache Sie C ++ vergleichen, erhalten Sie möglicherweise die gleiche Antwort oder nicht.

In C ++ haben Sie:

  • Fähigkeit zu intelligentem Inlining,
  • Generische Codegenerierung mit starker Lokalität (Vorlagen)
  • so klein und kompakt wie möglich Daten
  • Möglichkeiten, um Indirektionen zu vermeiden
  • vorhersehbares Gedächtnisverhalten
  • Compileroptimierungen nur durch Verwendung von High-Level-Abstraktionen (Templates) möglich

Dies sind die Merkmale oder Nebenwirkungen der Sprachdefinition, die sie theoretisch speicher- und geschwindigkeitstechnisch effizienter machen als jede andere Sprache, die:

  • Verwenden Sie die Indirektion massiv ("Alles ist eine verwaltete Referenz- / Zeigersprache"): Indirektion bedeutet, dass die CPU in den Speicher springen muss, um die erforderlichen Daten zu erhalten. Dies erhöht die CPU-Cache-Fehler, was eine Verlangsamung der Verarbeitung bedeutet. C verwendet auch Indirektionen a viel, auch wenn es kleine Daten als C ++ haben kann;
  • Generieren Sie große Objekte, auf die die Mitglieder indirekt zugreifen: Dies ist eine Folge der Standardeinstellung von Verweisen. Mitglieder sind Zeiger. Wenn Sie also ein Mitglied erhalten, erhalten Sie möglicherweise keine Daten in der Nähe des Kerns des übergeordneten Objekts, was wiederum Cache-Fehler auslöst.
  • Verwenden Sie einen Garbarge Collector: Dies macht die Vorhersagbarkeit der Leistung (von Entwurf her) nur unmöglich.

Das aggressive Inlining des Compilers in C ++ reduziert oder eliminiert viele Indirektionen. Die Fähigkeit, kleine Mengen von kompakten Daten zu generieren, macht den Cache benutzerfreundlich, wenn Sie diese Daten nicht über den gesamten Speicher verteilen, sondern zusammen packen (beides ist möglich, C ++ lässt Sie nur wählen). RAII macht das C ++ - Speicherverhalten vorhersehbar und beseitigt viele Probleme bei Echtzeit- oder Halb-Echtzeitsimulationen, die hohe Geschwindigkeit erfordern. Lokalitätsprobleme lassen sich im Allgemeinen so zusammenfassen: Je kleiner das Programm / die Daten, desto schneller die Ausführung. C ++ bietet verschiedene Möglichkeiten, um sicherzustellen, dass Ihre Daten dort sind, wo Sie sie haben möchten (in einem Pool, einem Array oder was auch immer) und dass sie kompakt sind.

Offensichtlich gibt es andere Sprachen, die das Gleiche tun können, aber sie sind weniger beliebt, da sie nicht so viele Abstraktionswerkzeuge wie C ++ bieten und daher in vielen Fällen weniger nützlich sind.

Klaim
quelle
7

Es geht hauptsächlich um Speicher (wie Michael Borgwardt sagte) mit ein bisschen JIT-Ineffizienz.

Eine Sache, die nicht erwähnt wird, ist der Cache - um den Cache vollständig zu nutzen, müssen Ihre Daten zusammenhängend angeordnet sein (dh alle zusammen). Mit einem GC-System wird nun Speicher auf dem GC-Heap zugewiesen, was sehr schnell geht. Wenn jedoch Speicher belegt wird, wird der GC regelmäßig aktiv und entfernt nicht mehr benötigte Blöcke und komprimiert die verbleibenden Blöcke. Abgesehen von der offensichtlichen Verlangsamung beim Verschieben der verwendeten Blöcke bedeutet dies, dass die von Ihnen verwendeten Daten möglicherweise nicht zusammengehalten werden. Wenn Sie ein Array mit 1000 Elementen haben, werden diese über den gesamten Heap verteilt, es sei denn, Sie haben sie alle auf einmal zugewiesen (und dann ihren Inhalt aktualisiert, anstatt sie zu löschen und neue zu erstellen, die am Ende des Heaps erstellt werden). Dies erfordert mehrere Speichertreffer, um sie alle in den CPU-Cache zu lesen. Die AC / C ++ - App wird höchstwahrscheinlich den Speicher für diese Elemente reservieren und dann die Blöcke mit den Daten aktualisieren. (Okay, es gibt Datenstrukturen wie eine Liste, die sich eher wie die GC-Speicherzuweisungen verhalten, aber die Leute wissen, dass diese langsamer als Vektoren sind.)

Sie können dies in Betrieb sehen, indem Sie einfach alle StringBuilder-Objekte durch String ersetzen ... Stringbuilder arbeiten, indem sie Speicher vorab zuweisen und füllen. Dies ist ein bekannter Leistungstrick für Java / .NET-Systeme.

Vergessen Sie nicht, dass das Paradigma "Löschen alter und Zuweisen neuer Kopien" in Java / C # sehr häufig verwendet wird, nur weil den Leuten gesagt wird, dass die Speicherzuweisungen aufgrund der GC sehr schnell sind und das Streuspeichermodell daher überall verwendet wird ( mit ausnahme von stringbuildern natürlich), daher verschwenden all deine bibliotheken viel speicher und verbrauchen viel speicher, wovon keine den vorteil der zusammenhängend- keit hat. Beschuldige den Hype um GC dafür - sie sagten dir, dass die Erinnerung frei sei, lol.

Der GC selbst ist offensichtlich ein weiterer Perfektionstreffer - wenn er ausgeführt wird, muss er nicht nur durch den Heap fegen, sondern auch alle nicht verwendeten Blöcke freigeben und dann alle Finalisierer ausführen (obwohl dies früher separat durchgeführt wurde) nächstes Mal mit angehaltener App) (Ich weiß nicht, ob es noch so ein Volltreffer ist, aber alle Dokumente, die ich lese, verwenden nur Finalisierer, wenn es wirklich nötig ist) und dann muss es diese Blöcke in Position bringen, damit der Haufen ist komprimiert, und aktualisieren Sie den Verweis auf die neue Position des Blocks. Sie sehen, es ist viel Arbeit!

Perf-Treffer für C ++ - Speicher sind auf die Speicherzuweisung zurückzuführen. Wenn Sie einen neuen Block benötigen, müssen Sie den Heap nach dem nächsten freien Speicherplatz durchsuchen, der groß genug ist. Bei einem stark fragmentierten Heap ist dies bei weitem nicht so schnell wie bei einem GC "Ordnen Sie am Ende einfach einen weiteren Block zu", aber ich denke, dies ist nicht so langsam wie die gesamte Arbeit, die die GC-Komprimierung leistet, und kann durch die Verwendung mehrerer Blockheaps mit fester Größe (auch als Speicherpools bezeichnet) verringert werden.

Es gibt noch mehr ... wie das Laden von Assemblys aus dem GAC, das eine Sicherheitsüberprüfung erfordert, Prüfpfade (schalten Sie sxstrace ein und schauen Sie sich nur an, was gerade los ist !) Und allgemeine andere Überentwicklungen, die bei Java / .net viel beliebter zu sein scheinen als C / C ++.

gbjbaanb
quelle
2
Viele Dinge, die Sie schreiben, gelten nicht für moderne Müllsammler der Generation.
Michael Borgwardt
3
@MichaelBorgwardt wie? Ich sage "der GC läuft regelmäßig" und "er verdichtet den Haufen". Der Rest meiner Antwort bezieht sich darauf, wie Anwendungsdatenstrukturen Speicher verwenden.
gbjbaanb
6

"Liegt es einfach daran, dass C ++ in Assembly- / Maschinencode kompiliert wird, während Java / C # zur Laufzeit noch den Verarbeitungsaufwand für die JIT-Kompilierung hat?" Grundsätzlich ja!

Kurz gesagt, Java hat mehr Overhead als nur die JIT-Kompilierung. Zum Beispiel erledigt es viel mehr Überprüfungen für Sie (so erledigt es Dinge wie ArrayIndexOutOfBoundsExceptionsund NullPointerExceptions). Der Müllsammler ist ein weiterer erheblicher Aufwand.

Es ist ein ziemlich detaillierter Vergleich hier .

vaughandroid
quelle
2

Beachten Sie, dass im Folgenden nur der Unterschied zwischen nativer und JIT-Kompilierung verglichen wird und die Besonderheiten einer bestimmten Sprache oder eines bestimmten Frameworks nicht behandelt werden. Es kann legitime Gründe dafür geben, eine bestimmte Plattform darüber hinaus zu wählen.

Wenn wir behaupten, dass nativer Code schneller ist, sprechen wir über den typischen Anwendungsfall von nativ kompiliertem Code im Vergleich zu JIT-kompiliertem Code, bei dem die typische Verwendung einer JIT-kompilierten Anwendung vom Benutzer ausgeführt werden soll, mit sofortigen Ergebnissen (z. B. Nr warte zuerst auf den Compiler). In diesem Fall glaube ich nicht, dass irgendjemand behaupten kann, dass mit JIT kompilierter Code mit nativem Code übereinstimmen oder ihn übertreffen kann.

Nehmen wir an, wir haben ein Programm in einer Sprache X geschrieben und können es mit einem nativen Compiler und erneut mit einem JIT-Compiler kompilieren. Jeder Arbeitsablauf hat die gleichen Phasen, die verallgemeinert werden können als (Code -> Zwischendarstellung -> Maschinencode -> Ausführung). Der große Unterschied zwischen zwei ist, welche Stufen vom Benutzer und welche vom Programmierer gesehen werden. Bei der nativen Kompilierung sieht der Programmierer bis auf die Ausführungsstufe alles, aber bei der JIT-Lösung sieht der Benutzer neben der Ausführung auch die Kompilierung des Maschinencodes.

Die Behauptung, dass A schneller als B ist, bezieht sich auf die Zeit, die das Programm benötigt, um ausgeführt zu werden, wie es vom Benutzer gesehen wird . Wenn wir davon ausgehen, dass beide Codeteile in der Ausführungsphase identisch ausgeführt werden, müssen wir davon ausgehen, dass der JIT-Workflow für den Benutzer langsamer ist, da er auch den Zeitpunkt T der Kompilierung zum Maschinencode sehen muss, bei dem T> 0 ist Damit der JIT-Arbeitsablauf den nativen Arbeitsablauf für den Benutzer ausführen kann, muss die Ausführungszeit des Codes verringert werden, sodass die Ausführung + Kompilierung zum Maschinencode niedriger ist als nur die Ausführungsphase des nativen Arbeitsablaufs. Dies bedeutet, dass wir den Code in der JIT-Kompilierung besser optimieren müssen als in der nativen Kompilierung.

Dies ist jedoch ziemlich undurchführbar, da zur Durchführung der notwendigen Optimierungen zur Beschleunigung der Ausführung mehr Zeit für die Kompilierung des Maschinencodes aufgewendet werden muss und somit jede Zeit, die wir durch den optimierten Code einsparen, verloren geht Wir fügen es der Zusammenstellung hinzu. Mit anderen Worten, die "Langsamkeit" einer JIT-basierten Lösung beruht nicht nur auf der zusätzlichen Zeit für die JIT-Kompilierung, sondern der durch diese Kompilierung erzeugte Code ist langsamer als eine native Lösung.

Ich werde ein Beispiel verwenden: Registerzuordnung. Da der Speicherzugriff einige tausend Mal langsamer ist als der Registerzugriff, möchten wir nach Möglichkeit Register verwenden und haben so wenig Speicherzugriff wie möglich, aber wir haben eine begrenzte Anzahl von Registern, und wir müssen den Status in den Speicher verschieben, wenn wir ihn benötigen ein Register. Wenn wir einen Registerzuweisungsalgorithmus verwenden, dessen Berechnung 200 ms dauert, und dadurch 2 ms Ausführungszeit einsparen, wird die Zeit für einen JIT-Compiler nicht optimal genutzt. Lösungen wie Chaitins Algorithmus, mit dem sich hochoptimierter Code erzeugen lässt, sind ungeeignet.

Die Rolle des JIT-Compilers besteht darin, die beste Balance zwischen Kompilierungszeit und Qualität des produzierten Codes zu finden, wobei jedoch die schnelle Kompilierungszeit eine große Rolle spielt, da Sie den Benutzer nicht warten lassen möchten. Die Leistung des ausgeführten Codes ist im JIT-Fall langsamer, da der native Compiler bei der Optimierung des Codes nicht an die Zeit gebunden ist und daher die besten Algorithmen verwenden kann. Die Möglichkeit, dass die Gesamtkompilierung + Ausführung für einen JIT-Compiler nur die Ausführungszeit für nativ kompilierten Code überschreiten kann, ist effektiv 0.

Unsere VMs beschränken sich jedoch nicht nur auf die JIT-Kompilierung. Sie verwenden zeitnahe Kompilierungstechniken, Caching, Hot Swapping und adaptive Optimierungen. Ändern wir also unsere Behauptung, dass die Leistung dem Benutzer entspricht, und beschränken Sie sie auf die Zeit, die für die Ausführung des Programms benötigt wird (vorausgesetzt, wir haben AOT kompiliert). Wir können den ausführenden Code effektiv dem nativen Compiler (oder besser?) Gleichsetzen. Ein großer Vorteil für VMs ist, dass sie möglicherweise Code mit einer besseren Qualität als ein nativer Compiler produzieren können, da sie Zugriff auf mehr Informationen haben - die des laufenden Prozesses, beispielsweise wie oft eine bestimmte Funktion ausgeführt werden kann. Die VM kann dann über Hot-Swapping adaptive Optimierungen auf den wichtigsten Code anwenden.

Es gibt jedoch ein Problem mit diesem Argument - es wird davon ausgegangen, dass profilgesteuerte Optimierung und dergleichen nur für VMs gilt, was jedoch nicht zutrifft. Wir können es auch auf die native Kompilierung anwenden - indem wir unsere Anwendung mit aktivierter Profilerstellung kompilieren, die Informationen aufzeichnen und dann die Anwendung mit diesem Profil neu kompilieren. Es ist wahrscheinlich auch erwähnenswert, dass Code-Hot-Swapping nicht nur von einem JIT-Compiler ausgeführt werden kann, sondern auch von systemeigenem Code - obwohl die JIT-basierten Lösungen dafür leichter verfügbar sind und den Entwickler erheblich entlasten. Die große Frage ist also: Kann uns eine VM einige Informationen bieten, die die native Kompilierung nicht kann, was die Leistung unseres Codes steigern kann?

Ich kann es selbst nicht sehen. Wir können die meisten Techniken einer typischen VM auch auf systemeigenen Code anwenden - obwohl der Prozess aufwändiger ist. Ebenso können wir Optimierungen eines nativen Compilers auf eine VM zurück anwenden, die AOT-Kompilierung oder adaptive Optimierungen verwendet. Die Realität ist, dass der Unterschied zwischen nativ ausgeführtem Code und dem, der in einer VM ausgeführt wird, nicht so groß ist, wie wir angenommen haben. Sie führen letztendlich zum gleichen Ergebnis, verfolgen jedoch einen anderen Ansatz, um dorthin zu gelangen. Die VM verwendet einen iterativen Ansatz, um optimierten Code zu erstellen, den der native Compiler von Anfang an erwartet (und der durch einen iterativen Ansatz verbessert werden kann).

Ein C ++ - Programmierer argumentiert möglicherweise, dass er die Optimierungen von Anfang an benötigt, und sollte nicht darauf warten, dass eine VM herausfindet, wie sie dies tun soll, wenn überhaupt. Dies ist bei unserer aktuellen Technologie wahrscheinlich ein gültiger Punkt, da der aktuelle Grad an Optimierungen in unseren VMs unter dem liegt, den native Compiler bieten können. Dies ist jedoch möglicherweise nicht immer der Fall, wenn sich die AOT-Lösungen in unseren VMs verbessern usw.

Mark H
quelle
0

Dieser Artikel ist eine Zusammenfassung einer Reihe von Blog-Beiträgen, die versuchen, die Geschwindigkeit von c ++ mit der von c # zu vergleichen, und die Probleme, die Sie in beiden Sprachen lösen müssen, um leistungsstarken Code zu erhalten. Die Zusammenfassung lautet: "Ihre Bibliothek ist weitaus wichtiger als alles andere, aber wenn Sie in C ++ sind, können Sie das überwinden." oder "moderne Sprachen haben bessere Bibliotheken und erzielen so mit geringerem Aufwand schnellere Ergebnisse", abhängig von Ihrer philosophischen Neigung.

Jeff Gates
quelle
0

Ich denke, dass die eigentliche Frage hier nicht lautet: "Was ist schneller?" aber "welches hat das beste potenzial für mehr leistung?" Unter diesen Gesichtspunkten setzt sich C ++ klar durch - es ist zu nativem Code kompiliert, es gibt kein JITting, es ist eine niedrigere Abstraktionsebene usw.

Das ist noch lange nicht alles.

Da C ++ kompiliert wird, müssen alle Compileroptimierungen zur Kompilierungszeit durchgeführt werden, und Compileroptimierungen, die für einen Computer geeignet sind, können für einen anderen Computer völlig falsch sein. Es ist auch der Fall, dass globale Compiler-Optimierungen bestimmte Algorithmen oder Codemuster anderen vorziehen können und werden.

Andererseits wird ein JITted-Programm zur JIT-Zeit optimiert, sodass es einige Tricks abrufen kann, die ein vorkompiliertes Programm nicht kann, und sehr spezifische Optimierungen für den Computer vornehmen kann, auf dem es tatsächlich ausgeführt wird, und für den Code, auf dem es tatsächlich ausgeführt wird. Sobald Sie den anfänglichen Overhead der JIT überwunden haben, besteht in einigen Fällen die Möglichkeit, dass Sie schneller sind.

In beiden Fällen wird eine vernünftige Implementierung des Algorithmus und andere Fälle, in denen der Programmierer nicht dumm ist, wahrscheinlich weitaus bedeutendere Faktoren sein - zum Beispiel ist es durchaus möglich, in C ++ einen vollständig hirntoten String-Code zu schreiben, der von Even umhüllt wird eine interpretierte Skriptsprache.

Maximus Minimus
quelle
3
"Compiler-Optimierungen, die für eine Maschine geeignet sind, können für eine andere völlig falsch sein" Nun, das ist nicht wirklich die Schuld an der Sprache. Wirklich leistungskritischer Code kann für jeden Computer, auf dem er ausgeführt wird, separat kompiliert werden. Dies ist ein Kinderspiel, wenn Sie lokal aus source ( -march=native) kompilieren . - "Es ist eine niedrigere Abstraktionsebene" ist nicht wirklich wahr. C ++ verwendet genau so übergeordnete Abstraktionen wie Java (oder sogar höhere: funktionale Programmierung? Template-Metaprogrammierung?), Implementiert jedoch die Abstraktionen weniger "sauber" als Java.
Abfahrt
"Wirklich leistungskritischer Code kann für jeden Computer, auf dem er ausgeführt wird, separat kompiliert werden. Dies ist ein Kinderspiel, wenn Sie lokal aus dem Quellcode kompilieren."
Maximus Minimus
Nicht unbedingt der Endbenutzer, sondern nur die Person, die für die Installation des Programms verantwortlich ist. Auf dem Desktop und mobile Geräte, dass in der Regel ist der Endbenutzer, aber diese sind nicht die einzigen Anwendungen gibt es sicherlich nicht die Performance-kritische. Und Sie müssen kein Programmierer sein , um ein Programm aus dem Quellcode zu erstellen, wenn es ordnungsgemäß geschriebene Erstellungsskripte hat, wie dies bei allen guten Projekten mit freier / offener Software der Fall ist.
links um ca.
1
Theoretisch kann eine JIT zwar mehr Tricks als ein statischer Compiler, in der Praxis (zumindest für .NET kenne ich Java nicht so gut), aber tatsächlich macht sie nichts davon. Ich habe in letzter Zeit eine Reihe von Demontagen von .NET JIT-Code durchgeführt, und es gibt alle möglichen Optimierungen wie das Herausholen von Code aus Schleifen, die Beseitigung von totem Code usw., die .NET JIT einfach nicht tut. Ich wünschte, es wäre so, aber hey, das Windows-Team in Microsoft hat jahrelang versucht, .NET zu töten, also halte ich nicht den Atem an
Orion Edwards
-1

Die JIT-Kompilierung wirkt sich tatsächlich negativ auf die Leistung aus. Wenn Sie einen "perfekten" Compiler und einen "perfekten" JIT-Compiler entwerfen, gewinnt die erste Option immer an Leistung.

Sowohl Java als auch C # werden in Zwischensprachen interpretiert und dann zur Laufzeit zu nativem Code kompiliert, wodurch die Leistung verringert wird.

Aber jetzt ist der Unterschied für C # nicht so offensichtlich: Microsoft CLR erzeugt unterschiedlichen nativen Code für unterschiedliche CPUs, wodurch der Code für den Computer, auf dem er ausgeführt wird, effizienter wird, was nicht immer von C ++ - Compilern durchgeführt wird.

PS C # ist sehr effizient geschrieben und hat nicht viele Abstraktionsebenen. Dies gilt nicht für Java, das nicht so effizient ist. In diesem Fall zeigen C # -Programme mit ihrer großartigen CLR häufig eine bessere Leistung als C ++ - Programme. Weitere Informationen zu .Net und CLR finden Sie in Jeffrey Richters "CLR via C #" .

superM
quelle
8
Wenn sich die JIT tatsächlich negativ auf die Leistung auswirken würde, würde sie sicherlich nicht verwendet werden?
Verhalten
2
@Zavior - Ich kann mir keine gute Antwort auf Ihre Frage vorstellen, aber ich verstehe nicht, dass JIT keinen zusätzlichen Leistungsaufwand verursachen kann. JIT ist ein zusätzlicher Prozess, der zur Laufzeit ausgeführt werden muss und Ressourcen erfordert, die nicht erforderlich sind. ' Es wird nicht für die Ausführung des Programms selbst ausgegeben, während eine vollständig kompilierte Sprache "einsatzbereit" ist.
Anonym
3
JIT wirkt sich positiv auf die Leistung aus und nicht negativ, wenn Sie es in einen Kontext setzen. Es kompiliert Bytecode in Maschinencode, bevor es ausgeführt wird. Die Ergebnisse können auch zwischengespeichert werden, sodass sie schneller ausgeführt werden können als der entsprechende Bytecode, der interpretiert wird.
Casey Kuball
3
JIT (oder besser gesagt der Bytecode-Ansatz) wird nicht für die Leistung, sondern für die Benutzerfreundlichkeit verwendet. Anstatt Binärdateien für jede Plattform (oder eine gemeinsame Teilmenge, die für jede von ihnen nicht optimal ist) vorab zu erstellen, kompilieren Sie nur zur Hälfte und lassen den JIT-Compiler den Rest erledigen. "Einmal schreiben, überall bereitstellen" ist der Grund, warum dies so gemacht wird. Die Bequemlichkeit kann mit nur einem Bytecode-Interpreter erzielt werden, aber JIT macht es schneller als der unformatierte Interpreter (obwohl dies nicht unbedingt schnell genug ist, um eine vorkompilierte Lösung zu übertreffen; die JIT-Kompilierung nimmt Zeit in Anspruch und das Ergebnis macht nicht immer etwas aus dafür).
tdammers
4
@Tdammmers, eigentlich gibt es auch eine Leistungskomponente. Siehe java.sun.com/products/hotspot/whitepaper.html . Zu den Optimierungen gehören dynamische Anpassungen zur Verbesserung der Verzweigungsvorhersage und der Cachetreffer, dynamisches Inlining, De-Virtualisierung, Deaktivieren der Grenzüberprüfung und Aufheben der Schleife. Die Behauptung ist, dass diese in vielen Fällen die Kosten für die GEG mehr als tragen können.
Charles E. Grant