Forum: PC-Programmierung C vs. C#: float Performance


von Christopher C. (Gast)


Lesenswert?

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
1
#include <stdio.h>
2
#include <stdlib.h>
3
#include <string.h>
4
#include <CFramework/Types.h>
5
#include <CFramework/Diagnostics.h>
6
#include <CFramework/Memory.h>
7
8
#define COUNT 1000000000
9
10
typedef struct Vec3
11
{
12
    Float32 x;
13
    Float32 y;
14
    Float32 z;
15
} Vec3;
16
17
Vec3 vec3(Float32 x, Float32 y, Float32 z)
18
{
19
    Vec3 tmp;
20
    tmp.x = x;
21
    tmp.y = y;
22
    tmp.z = z;
23
    return tmp;
24
}
25
26
inline Vec3 vec3_add(Vec3 a, Vec3 b)
27
{
28
    Vec3 tmp;
29
    tmp.x = a.x + b.x;
30
    tmp.y = a.y + b.y;
31
    tmp.z = a.z + b.z;
32
    return tmp;
33
}
34
35
inline Vec3* vec3_addP(Vec3* pOut, const Vec3* pA, const Vec3* pB)
36
{
37
    pOut->x = pA->x + pB->x;
38
    pOut->y = pA->y + pB->y;
39
    pOut->z = pA->z + pB->z;
40
    return pOut;
41
}
42
43
int main()
44
{
45
    StopWatch sw = stopwatch_new();
46
    Vec3 vec0, vec1;
47
    vec0 = vec3(0.0f, 0.0f, 0.0f);
48
    vec1 = vec3(1.0f, 1.0f, 1.0f);
49
50
    stopwatch_start(&sw);
51
    for (Int32 i=0;i<COUNT;i++)
52
        vec0 = vec3_add(vec0, vec1);
53
    stopwatch_stop(&sw);
54
55
    printf("Call-By-Val Result: %i ms Value: %f\n", stopwatch_getElapsedMilliseconds(&sw), vec0.x);
56
57
    stopwatch_restart(&sw);
58
    for (Int32 i=0;i<COUNT;i++)
59
        vec3_addP(&vec0, &vec0, &vec1);
60
    stopwatch_stop(&sw);
61
62
    printf("Call-By-Ref Result: %i ms Value: %f\n",     stopwatch_getElapsedMilliseconds(&sw), vec0.x);
63
64
    getchar();
65
    return 0;

Performance.cs:
1
using System;
2
using System.Collections.Generic;
3
using System.Linq;
4
using System.Text;
5
using System.Runtime.InteropServices;
6
using System.Diagnostics;
7
8
class Program
9
    {
10
        static void Main(string[] args)
11
        {
12
            Stopwatch stopWatch = new Stopwatch();
13
            Vec3 vec0 = new Vec3(0.0f, 0.0f, 0.0f), vec1 = new Vec3(1.0f, 1.0f, 1.0f);
14
15
            stopWatch.Start();
16
            for (int i = 0; i < 1000000000; i++)
17
                //vec0.Add(vec1);
18
                vec0 = Vec3Add(vec0, vec1);
19
            stopWatch.Stop();
20
            Console.WriteLine("CallByVal Result: {0}", stopWatch.ElapsedMilliseconds);
21
22
            vec0 = new Vec3(0.0f, 0.0f, 0.0f);
23
            stopWatch.Restart();
24
            for (int i = 0; i < 1000000000; i++)
25
                //vec0.Add(ref vec1);
26
                Vec3Add(ref vec0, ref vec1, out vec0);
27
            stopWatch.Stop();
28
            Console.WriteLine("CallByRef Result: {0}", stopWatch.ElapsedMilliseconds);
29
            Console.ReadLine();
30
        }
31
32
        public struct Vec3
33
        {
34
            public float x;
35
            public float y;
36
            public float z;
37
38
            public Vec3(float x, float y, float z)
39
            {
40
                this.x = x;
41
                this.y = y;
42
                this.z = z;
43
            }
44
45
            public void Add(Vec3 a)
46
            {
47
                x += a.x;
48
                y += a.y;
49
                z += a.z;
50
            }
51
52
            public void Add(ref Vec3 a)
53
            {
54
                x += a.x;
55
                y += a.y;
56
                z += a.z;
57
            }
58
        }
59
60
        static Vec3 Vec3Add(Vec3 a, Vec3 b)
61
        {
62
            return new Vec3(a.x + b.x, a.y + b.y, a.z + b.z);
63
        }
64
65
        static void Vec3Add(ref Vec3 a, ref Vec3 b, out Vec3 vec)
66
        {
67
            vec.x = a.x + b.x;
68
            vec.y = a.y + b.y;
69
            vec.z = a.z + a.z;
70
        }
71
}

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

von Karl H. (kbuchegg)


Lesenswert?

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.

von Christopher C. (Gast)


Lesenswert?

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?

von Karl H. (kbuchegg)


Lesenswert?

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.

von Karl H. (kbuchegg)


Lesenswert?

> 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.

von Yalu X. (yalu) (Moderator)


Lesenswert?

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.

von Christopher C. (Gast)


Lesenswert?

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.

von Karl H. (kbuchegg)


Lesenswert?

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
1
inline Vec3* vec3_addP(Vec3* pOut, const Vec3* const pA, const Vec3* const pB)
2
{

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)

von Christopher C. (Gast)


Lesenswert?

Nein hat leider nichts gebracht :(. Auch den Rückgabewert zu void zu 
machen, aber das wurde wahrscheinlich sowieso vom Kompiler rausgenommen.

von Oliver R. (superberti)


Lesenswert?

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ß,

von Christopher C. (Gast)


Lesenswert?

Oh ich hätte erwähnen sollen, dass ich gcc und für c++ g++ verwende, g++ 
bekommt das anscheinend besser hin.

von Christopher C. (Gast)


Lesenswert?

Ganz vergessen, beim vs 2010 Kompiler braucht C++ auch 4907ms.

von Klaus W. (mfgkw)


Lesenswert?

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

von Klaus W. (mfgkw)


Lesenswert?

Außerdem darf man nicht ungeschickte Programmierung in einer Sprache 
verwenden, wenn man sie mit einer anderen vergleichen will:
1
inline Vec3 vec3_add(Vec3 a, Vec3 b)
2
{
3
    Vec3 tmp;
4
    tmp.x = a.x + b.x;
5
    tmp.y = a.y + b.y;
6
    tmp.z = a.z + b.z;
7
    return tmp;
8
}

Hier wird evtl. ein komplettes Objekt auf den Stack kopiert, und beim 
Aufrufer wieder runter.

von Christopher C. (Gast)


Lesenswert?

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
1
#include <iostream>
2
#include <windows.h>
3
using namespace std;
4
5
#define COUNT 1000000000
6
7
struct Vec3
8
{
9
  float x;
10
  float y;
11
  float z;
12
13
  Vec3(float x, float y, float z);
14
  void Add(Vec3 a);
15
  void AddRef(Vec3& a);
16
};
17
18
Vec3::Vec3(float x, float y, float z)
19
{
20
  this->x = x;
21
  this->y = y;
22
  this->z = z;
23
}
24
25
void Vec3::Add(Vec3 a)
26
{
27
  this->x += a.x;
28
  this->y += a.y;
29
  this->z += a.z;
30
}
31
32
void Vec3::AddRef(Vec3& a)
33
{
34
  this->x += a.x;
35
  this->y += a.y;
36
  this->z += a.z;
37
}
38
39
inline Vec3 vec3_add(Vec3 a, Vec3 b)
40
{
41
    return Vec3(a.x + b.x, a.y + b.y, a.z + b.z);
42
}
43
44
inline Vec3& vec3_addP(Vec3& pOut, const Vec3& pA, const Vec3& pB)
45
{
46
    pOut.x = pA.x + pB.x;
47
    pOut.y = pA.y + pB.y;
48
    pOut.z = pA.z + pB.z;
49
    return pOut;
50
}
51
52
int main()
53
{
54
  Vec3 vec0(0.0f, 0.0f, 0.0f);
55
  Vec3 vec1(1.0f, 1.0f, 1.0f);
56
  int begin, end;
57
58
  begin = GetTickCount();
59
  for (int i=0;i<COUNT;i++)
60
    //vec0.Add(vec1);
61
    vec0 = vec3_add(vec0, vec1);
62
  end = GetTickCount();
63
  cout << "CallByVal Result: " << end - begin << " ms ValueX: " << vec0.x << ", ValueY: " << vec0.y << ", ValueZ: " << vec0.z << endl;
64
65
  vec0 = Vec3(0.0f, 0.0f, 0.0f);
66
67
  begin = GetTickCount();
68
    for (int i=0;i<COUNT;i++)
69
      //vec0.AddRef(vec1);
70
      vec3_addP(vec0, vec0, vec1);
71
  end = GetTickCount();
72
  cout << "CallByRef Result: " << end - begin << " ms Value: " << vec0.x << ", ValueY: " << vec0.y << ", ValueZ: " << vec0.z << endl;
73
74
  return 0;
75
}

von Yalu X. (yalu) (Moderator)


Lesenswert?

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.

von mete (Gast)


Lesenswert?

@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.

von Christopher C. (Gast)


Lesenswert?

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

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.