Hi,
ich arbeite derzeit an einer 3D-Mathematik Bibliothek für C. Ich hab ein
kleines Programm geschrieben um zu schauen, wie schnell den C damit ist
(ganz einfache Vektoren Berechnungen) und ob ich die Vektoren als Wert
oder Pointer übergeben soll (also CallByVal vs CallByRef). Das Programm
brauchte bei der ersten Variante ~30ms und die zweite Variante 16ms. Wow
echt schnell :). Zum Spaß schrieb ich das Programm in C# nach und
erwartete, dass C# gnadenlos verlieren würde, doch genau das Gegenteil
war der Fall. C# brauchte bei 1Milliarden Vektor Berechnungen nur 1339ms
(CallByRef, CallByVal: ~5sec), wobei C ganze 4985ms brauchte. Wie kommt
das? Meine Vermutung: Entweder ist der C# Kompiler schlauer und kann
eine wichtige Stelle perfekt optimieren oder der JIT Kompiler kennt den
Prozessor (AMD Athlon2 X3 440 3Ghz) sehr gut und kann somit einen
Maschienenbefehlssatz, wie z.B. SSE2 (obwohl ich den auch im gcc
angeschalten habe), nutzen.
Deshalb frage ich euch, wie kann C# schneller sein?
Hier der Code:
Performance.c
Wahrscheinlich liegt der Fehler bei mir, da ich mich mit Optimieren
nicht so gut auskenne, aber wie kann der Unterschied dennoch so
gravierend sein?
mfg
POinter sind dann eben nicht genau dasselbe wie eine Referenz in C#. An
dieser Stelle sind sie für den Compiler bezüglich Optimierung
hinderlich, selbst wenn er die Funktion inlined.
> Zum Spaß schrieb ich das Programm in C# nach und erwartete,> dass C# gnadenlos verlieren würde
Eigentlich nicht. Da ist in deinem Programm noch viel zu wenig los, als
dass da große Unterschiede zu erwarten wären. Die reine Rechnerei geht
ja immer gleich schnell.
Wenn du Vergleiche haben willst, dann musst du mit C++ vergleichen. Dort
gibt es dann das Konzept einer Referenz als 'anderer Name für ein
ansonsten existierendes Objekt', welches dann auch gnadenlos optimiert
werden kann.
Aber auch dort ist nicht zu erwarten, dass sich das in der Laufzeit groß
unterscheidet. Wo es sich unterscheiden wird, ist der Memory-Footprint.
Ja tatsächlich die C++ Variante braucht auch nur 1344ms dank Referenz
und OO Optimierungen. Aber ist eine Referenz nicht einfach ein Zeiger,
der vom Kompiler verwaltet wird, wo also Dereferenzierung etc.
automatisch vorgenommen wird? Und wie kann ich die C Variante
beschleunigen?
Christopher C. schrieb:> Ja tatsächlich die C++ Variante braucht auch nur 1344ms dank Referenz> und OO Optimierungen. Aber ist eine Referenz nicht einfach ein Zeiger,> der vom Kompiler verwaltet wird
Nein.
Eine Referenz ist erstmal nur
"ein anderer Name für ein anderweitig existierendes Objekt"
Das KANN in manchen Situationen dazu führen, dass der Compiler das mit
einem Pointer implementiert, aber er MUSS es nicht. Wenn der Compiler
eine Funktion inlined, kann er daher die Referenz ganz einfach gegen die
Originalvariable austauschen, da ja Referenz und Original grundsätzlich
dasselbe Objekt bezeichnen. Und das führt dann zu ganz einfach
durchzuführenden schlagkräftigen Optimierungen.
> Und wie kann ich die C Variante beschleunigen?
Man könnte versuchen, ob man dem Compiler mit ein paar const klar machen
kann, dass er den Pointer zwischendurch wegoptimieren darf.
Du gibst am Ende der Berechung im C-Programm die Laufzeit und
x-Komponente des Vektors aus, im C#-Programm nur die Laufzeit.
Was nicht ausgegeben oder anderweitig benötigt wird, kann der Compiler
wegoptimieren.
Sorge mal dafür, dass in beiden Fällen alle drei Komponenten des
Ergebnisvektors ausgegeben werden und miss die Zeiten noch einmal.
Zumindest beim GCC macht es einen deutlichen Unterschied, ob keine,
eine oder alle drei Komponenten ausgegeben werden.
Ok habe alle drei Programme so umgeschrieben, dass sie die Vektoren
ausgeben. Und schon sieht das anders aus:
C: 5204ms
C++: 1563ms
C#: 4964ms
Wow C++ klarer Gewinner. Der C# Kompiler hat gute Arbeit geleistet, der
ließ einfach die anderen float Werte weg.
Nun hab ich das mit den Referenzen verstanden ;).
Karl Heinz Buchegger schrieb:
>> Und wie kann ich die C Variante beschleunigen?>> Man könnte versuchen, ob man dem Compiler mit ein paar const klar machen> kann, dass er den Pointer zwischendurch wegoptimieren darf.
Wie meinst du das? Bei der Funktion Vec3_addP wurden die Parameter schon
als konstanten angegeben und bei Vec3_add hat es nichts gebracht.
Christopher C. schrieb:> Wie meinst du das? Bei der Funktion Vec3_addP wurden die Parameter schon> als konstanten angegeben
Mein Fehler.
Hab ich nicht gesehen
Ein
könnte vielleicht in der Optimierung noch was bringen, wenn es den
Compiler davon überzeugen kann, dass er für pA und pB gar keine eigenen
Variablen anlegen muss, sondern er mit den Originaladressen des
Aufrufers arbeiten kann.
(Ausserdem hat diese Funktion einen Returnwert, den die C# Version nicht
hat)
Hi,
also ich kann Deine Ergebnisse nicht nachvollziehen. Mit CallByRef,
VS2010 und Release-Code (...und NICHT aus dem VS ausgeführt) liegen die
Ergebnisse aller drei Programmiersprachen im Rahmen der Messgenauigkeit
vollkommen aufeinander.
Ehrlich gesagt hätte ich auch nichts anderes erwartet, denn bei solch
einfachen Konstrukten hat der Optimierer leichtes Spiel und die
Unterschiede sollten dann auch marginal ausfallen.
Auch bei CallByValue sind die Unterschiede <10%:
CallByValue [ms]:
C# C++ C
4629 4228 4228
CallByRef [ms]:
C# C++ C
2695 2730 2714
Gruß,
gcc und g++ sind derselbe Compiler (bzw. jeweils nur ein Frontend, das
denselben Compiler aufruft; g++ allerdings mit Linkeroptionen, um die
C++-Lib mitzunehmen), der Unterschied wird wohl woanders herkommen
Klaus Wachtler schrieb im Beitrag:
> Außerdem darf man nicht ungeschickte Programmierung in einer Sprache> verwenden, wenn man sie mit einer anderen vergleichen will:> inline Vec3 vec3_add(Vec3 a, Vec3 b)> {> Vec3 tmp;> tmp.x = a.x + b.x;> tmp.y = a.y + b.y;> tmp.z = a.z + b.z;> return tmp;> }>> Hier wird evtl. ein komplettes Objekt auf den Stack kopiert, und beim> Aufrufer wieder runter.
Das ist mir durchaus bewusst, ich wollte ja den Geschwindigkeits
Unterschied messen.
Performance.cpp
Ich habe mal ein wenig herumgespielt, um herauszufinden, woher die
deutlich unterschiedliche Laufzeit zwischen C- und C++-Modus des GCC
in Christophers Benchmark kommt. Hier ist die Erklärung:
Der C-Standard schreibt vor, dass das Ergebnis einer Berechnung unab-
hängig davon ist, ob die verwendeten Variablen in Registern oder im
Speicher gehalten werden. Da die internen Register der i86-FPU 80 Bit
breit sind, die Speichertypen double und float aber nur 64 bzw. 32 Bit,
wird das Rechenergebnis bei einer Zuweisung vom Register an den ent-
sprechenden Speicherort geschrieben und bei der nächsten Verwendung
wieder von dort gelesen, um dem Standard zu gehorchen. Das passiert in
Christophers Beispiel in jedem Schleifendurchlauf für alle drei Vektor-
Komponenten und kostet entsprechend Zeit.
Im C++-Modus werden FP-Variablen, wenn möglich, in Registern gehalten.
Ich kenne den C++-Standard nicht so genau, aber möglicherweise ist er
diesbezüglich weniger streng als der C-Standard. Wie der C#-Standard das
regelt, weiß ich erst recht nicht.
Wenn man im C-Modus nicht die Option -std=c* (* = 89, 90, 99 oder 11)
oder -ansi, sondern stattdessen -std=gnu* angibt, können auch in C
FP-Variablen in Registern gehalten werden, womit eine höhere Rechen-
geschwindigkeit erzielt wird. Allerdings kann es dann passieren, dass
das Programm je nach Optimierungstufe unterschiedliche Ergebnisse
liefert. In Christophers Beispiel wird dieser Unterschioed besonders
deutlich: Bei der Verwendung von Registern ist das Ergebnis 1000000000,
bei standardkonformer Codegenerierung aber 16777216, weil ab diesem Wert
die Addition von 1 in der mangelnden Auflösung von float verloren geht.
Ich nehme an, Christopher hat beim Kompilieren des C-Programms -std=c99
angegeben, um die Definition "Int32 i" im Kopf der For-Schleifen zu
ermöglichen. Mit -std=gnu99 hat man diese Möglichkeit ebenfalls und
zusätzlich die Registeroptimierung.
Es gibt beim GCC auch noch die Optionen -mpc64 und -mpc32, um die Re-
chengenauigkeit der FPU künstlich zu drosseln (der Default ist -mpc80).
Mit -mpc32 wäre im obigen Beispiel das Speichern und Laden der Variablen
in jedem Schleifendurchlauf auch bei Standardkonformität nicht erforder-
lich. Diese Chance scheint aber der GCC nicht wahrzunehmen. Die Verwen-
dung der -mpc-Option wäre aber auch sonst keine gute Idee, weil sie die
Rechengenauigkeit global im gesamten Programm verschlechtert.
@Yalu: Sehr interessantes debugging.
Der gcc verwendet, zumindest unter linux, nur im 32 Bit modus die 387
FPU mit 80 Bit Registern. Wenn man das Programm für AMD64 compiliert,
sollte der gcc SSE für die FP Berechnungen verwenden, die ebenfalls
32/64 Bit verwenden.
Unter 32 Bit kann man das dem gcc mit der Option "-mfpmath=sse"
beibringen.
Sehr gute Analyse, denn ich benutze -std=c99. Sobald ich mein C Programm
mit -mfpmath=sse kompiliere, ist genau so schnell wie das C++ Programm.
Vielen Dank!
mfg