Performancefrage zu C# vs C++

11/06/2014 13:24 Terreox#1
In letzter Zeit hab ich immer mal wieder ein Auge auf C++ geworfen und möchte auch in Zukunft meine Kenntnisse dorthin erweitern.
Bisher habe ich hauptsächlich C# und Java programmiert.

Eines Abends war mir langweilig und ich habe ein wenig mit C# und C++ gespielt und ein wenig die Performance verglichen, soweit es mir möglich war.
Die Idee hinter dem Test war eigentlich nur, möglichst viele möglichst kleine Packete in Form von structs zu verarbeiten.

Dazu habe ich mir zwei sehr einfache structs in C# und C++ geschrieben.
Hierbei ging es mir nur darum, ein sehr kleines Packet zu erzeugen.
Code:
//C++
struct Packet
{
    int x;
};

//C#
struct Packet
{
    public int x;
}
Nun habe ich zwei for-Schleifen genommen, die äußere für die Anzahl der Runden und die innere für die Iterationen pro Runde.

Code:
//C++
using namespace std;

srand(time(NULL));

for (int r = 0; r < rounds; r++)
{
    clock_t start = clock();

    int hits = 0;
    for (int i = 0; i < iterationsPerRound; i++)
    {
        Packet packet;
        packet.x = rand();
        if (packet.x == 0)
            hits++;
    }

    clock_t end = clock();

    cout << "\nRound #" << (r + 1) << ": " << double((end-start))/CLOCKS_PER_SEC << "s (Hits: " << hits << ")" << endl;
}

//C#
var random = new Random(0xDEAD);
var swatch = new Stopwatch();
for (var r = 0; r < rounds; r++)
{
    swatch.Restart();

    var hits = 0;
    for (var i = 0; i < iterationsPerRound; i++)
    {
        Packet packet;
        packet.x = random.Next();

        if (packet.x == 0)
            hits++;
    }

    swatch.Stop();

    Console.WriteLine("Round#{0}: {1} s ({2} Hits)", (r + 1), (double)swatch.ElapsedMilliseconds / 1000, hits);
}
Mir ist bekannt, dass es oft nicht sinnvoll ist, Code 1:1 von einer Sprache in die andere zu konvertieren, da andere Sprache anders arbeiten. Aber bei diesem Test habe ich es mal gemacht.
Ich habe den Test nun durchlaufen lassen mit 10 Runden und 10.000.000 Iterationen pro Runde.
Erwartet habe ich eigentlich, dass C++ zumindestens ein wenig schneller ist als C#, ich wurde allerdings eines besseren belehrt.
Pro Runde hat die C++ Version ca. 120ms und die C# Version ca. 100ms gebraucht. Auf jedenfall war die C# Version bisher immer schneller.

Es ist ewig her, dass ich mit C++ gearbeitet habe und wollte nun mal nachfragen, wo der Grund dafür liegt.
Ist der Test einfach nur schlecht bzw nicht geeignet, ist der C++ Code/C# Code nicht optimal oder gibt es irgendeinen anderen Grund?
Oder ist C# in diesem Szenario einfach C++ überlegen?

Update1:
Nachdem ich mich nach möglichen Performanceverbesserungen umgeschaut habe, kam ich zum Thema RNG (random number generation) und einen interessanten Beitrag von Intel ([Only registered and activated users can see links. Click Here To Register...]
Dies dort beschriebene Methode soll den Standardgenerator ablösen und wesentlich schneller sein.

Also hab ich mir den Code kopiert und diese Methode verwendet (sowohl bei C++ als auch C#, wobei die C# Version etwas umgeschrieben werden musste).

Das hat zumindestens den ersten Durchbruch gebracht.
Der C++ Code braucht nun nur noch ca. 15ms für eine Runde und der C# Code ganze 50ms.
Jetzt ist der Jagdtrieb erst recht geweckt :D
Mal schauen, was ich noch so finde.

Update2:
Aufgrund von snow's Anmerkung, dass std::cout seines Wissens nach langsam sei, habe ich die Ausgabe dahingehend verändert, dass ich die Rundenergebnisse in ein Array speicher und am Ende ausgeben lasse.
Allerdings hat diese Änderung keinen Einfluss auf die Rundenzeit, da die Ausgabe bereits in der alten Version nicht mehr mitgemessen wurde.
11/06/2014 14:14 snow#2
Ich sehe dort kein C++, das ist C mit std::cout.

Mit dem reinen Code und deinem Ergebnis kann man fast nichts anfangen. Welcher Compiler wurde verwendet? Welche Optimierungsstufe? Zudem ist std::cout meines Wissens nach relativ langsam, vor allem wenn du bei jeder Runde aufs neue den Buffer flusht.
11/06/2014 14:27 Terreox#3
Quote:
Originally Posted by snow View Post
Ich sehe dort kein C++, das ist C mit std::cout
Dann ist es trotzdem C++, nur halt mit einem kleinen Anteil an Funktionalität, den es nur in C++ gibt.

Quote:
Originally Posted by snow View Post
Mit dem reinen Code und deinem Ergebnis kann man fast nichts anfangen. Welcher Compiler wurde verwendet? Welche Optimierungsstufe? Zudem ist std::cout meines Wissens nach relativ langsam, vor allem wenn du bei jeder Runde aufs neue den Buffer flusht.
Ich habe zwei verschiedene genutzt.
Einmal den eingebauten in Visual Studio 2013 (falls dieser cl.exe ist, dann hab ich Version 18.00.30723) und einmal mingw-gcc 4.8.1.
Bei dem Visual Studio Projekt habe ich bei den Optimierungen nur verändert, dass der Compiler die /Ot Flag eingestellt (Favor fast code). Ansonsten hab ich am Standard nichts verändert (/02 /Oi /GL sind unter Optimierungen noch angeschaltet).
Beim Gcc hab ich nichts als Optionen angegeben.
11/06/2014 15:12 Hiris#4
Wenn man C++ als C mit Klassen versteht ist die Annahme korrekt. Eine echte c++ Lösung sähe anders aus
11/06/2014 15:43 Terreox#5
Quote:
Originally Posted by Hiris View Post
Wenn man C++ als C mit Klassen versteht ist die Annahme korrekt. Eine echte c++ Lösung sähe anders aus
Dann zeig doch mal eine reinrassige C++ Lösung oder sag, was du an meinem bisherigen Code ändern würdest.

Vorallem wüsste ich grad echt nicht, was man da groß ändern könnte.
Das Einzige was ich bisher geändert habe, ist memset zu std::fill von den Arrays, die ich zum Zwischenspeichern der Zeiten und Hits nehme.
Ansonsten, wenn es zu clock() und clock_t keine C++ Alternative gibt, ist der Rest völlig in Ordnung.

Ich versteh sowieso nicht, warum man vorallem bei C++ immer die neuen Sachen nehmen soll.
Solange die Dinge aus C funktionieren, keine Bugs enthalten und es keine besseren Alternativen gibt, find ich die Nutzung von C in C++ völlig legitim.

Würde mich mal interessieren.
11/06/2014 17:05 Tasiro#6
Es gäbe std::default_random_engine (in random), auto start=std::chrono::high_resolution_clock::now() (in chrono) und für das Zwischenergebnis-Array std::for_each (in algorithm), aber davon abgesehen gibt es tatsächlich sehr wenig Alternativen...
Ich weiß nicht, wie schnell diese neuen Zufallsgeneratoren sind.
11/06/2014 20:10 bloodx#7
Damit liege ich bei ~48ms
Code:
#include <iostream>
#include <chrono>
#include <vector>
#include <random>

static std::vector<int> iteration(10000000);

typedef std::chrono::high_resolution_clock Clock;
typedef std::chrono::duration<double> double_seconds;

struct Packet
{
	int x;
};

int main()
{
	int hits = 0;
	int rounds = 10;

	std::random_device rd;
	std::mt19937 mt( rd() );
	std::uniform_real_distribution<double> dist(1,10000000);

	do
	{
		Clock::time_point Start = Clock::now();

		for (auto i : iteration)
		{
			Packet packet;
			packet.x = dist(mt);
			if (!packet.x)
				hits++;
		}
		Clock::time_point End = Clock::now();

		auto ms = std::chrono::duration_cast<double_seconds>(End - Start);
		std::cout << "\nRound #" << rounds << ": " << ms.count() << "s (Hits: " << hits << ")" << std::endl;

		rounds--;
	} while (rounds);
	std::cin.get();

	return 0;
}
11/06/2014 20:19 Schlüsselbein#8
Nehme die Ausgabe aus der Schleife.
Ausserdem: Du verwendest schon bei beiden Versionen den Release build oder?

@Über mir: Einen std::vector<int> für eine einfache Schleife missbrauchen? Wozu? Und wozu die Referenz auf einen Integer in der Schleife??
11/06/2014 20:27 bloodx#9
Quote:
Originally Posted by Schlüsselbein View Post
Nehme die Ausgabe aus der Schleife.
Ausserdem: Du verwendest schon bei beiden Versionen den Release build oder?

@Über mir: Einen std::vector<int> für eine einfache Schleife missbrauchen? Wozu? Und wozu die Referenz auf einen Integer in der Schleife??
Wollte es nur als Beispiel mit einbringen den Vector :).

Habe ich bereits berichtigt, hatte noch andere Sachen getestet. hehe
11/06/2014 20:30 qqdev#10
Nicht vergessen beim Testen in den 'Höchstleistungsmodus' zu wechseln, falls ihr nicht in dem Modus seid.
11/06/2014 21:55 Terreox#11
Ja also die neue Zeitmessung find ich gut, ist auch in der Ausgabe genauer.

Allerdings ist das mit dem Vector quatsch, vorallem muss du bedenken, dass du mit diesem Vector grad mal 40.000.000 Bytes im Speicher hälst :D
Deine Version verbraucht bei mir ~39MB und braucht pro Runde ~60ms.

Aber mit der momentanen Version von mir bin ich ganz zufrieden.
Speicherverbrauch liegt bei 532KB und jede Runde braucht ~17ms.

Code:
#include <iostream>
#include <ctime>
#include <chrono>

static unsigned int g_seed;

typedef std::chrono::high_resolution_clock Clock;
typedef std::chrono::duration<double> double_seconds;

struct Packet
{
    int x;
};

inline void fast_srand(int seed) { g_seed = seed; }

inline int fastrand()
{
    g_seed = (214013*g_seed+2531011);
    return (g_seed>>16)&0x7FFF;
}

void Benchmark(int rounds, int iterationsPerRound)
{
    fast_srand(0xDEAD);

    double timings[rounds];
    int hits[rounds];

    std::fill(timings, timings + rounds, 0.0);
    std::fill(hits, hits + rounds, 0);

    for (int r = 0; r < rounds; r++)
    {
//      clock_t start = clock();
        std::chrono::high_resolution_clock::time_point Start = Clock::now();

        int lHits = 0;
        for (int i = 0; i < iterationsPerRound; i++)
        {
            Packet packet;
            packet.x = fastrand();
            if (!packet.x)
                lHits++;
        }

//      clock_t end = clock();
        Clock::time_point End = Clock::now();

        //timings[r] = double((end-start))/CLOCKS_PER_SEC;
        timings[r] = std::chrono::duration_cast<double_seconds>(End - Start).count();
        hits[r] = lHits;
    }

    for(int r = 0; r < rounds; r++)
        std::cout << "Round " << (r+1) << ": " << timings[r] << "s " << hits[r] << "hits." << std::endl;
}

int main()
{
    Benchmark(50, 10000000);

    std::cin.get();

    return 0;
}
Auf jedenfall wieder etwas dazu gelernt :)
11/07/2014 14:24 .Lol#12
Ihr vergleicht hier Äpfel mit Birnen.


Laut [Only registered and activated users can see links. Click Here To Register...] wird in C# zur Erzeugung von Zufallszahlen mittels der Radom Klasse der "Donald E. Knuth's subtractive random number generator algorithm" verwendet.

Wohingegen C++ den Mersenne Twister 19937 generator als default Generator verwendet.
(default_random_engine ist bei mir der mt19937, ob das auch so im Standard steht müsste man nachlesen)


Die Verwendung von rand() solltet ihr aus mehreren Gründen vermeiden - Hier ein [Only registered and activated users can see links. Click Here To Register...] über rand().
Ich habe mir mal die Mühe gemacht und die einzelnen random Engines genauer unter die Lupe genommen

[Only registered and activated users can see links. Click Here To Register...]
(i7-2600K, Windows7-64 )
(Ich bin mir nicht sicher ob die knuth_b der gleiche ist wie der in C#)
11/07/2014 14:48 Schlüsselbein#13
Mit welchem Code? Wie viel Durchläufe?
Ein paar tipps zum Plotten: Achsenbeschriftung besser wählen (mit so riesen Werten kann man auf den ersten Blick nicht viel anfangen), Messwerte ggf. mit Fehlerbalken auftragen und anschliessend ne Regression reinlegen.
11/10/2014 07:31 ƬheGame#14
Wenn ihr Programmiersprachen vergleicht solltet ihr vielleicht nicht mit RNG arbeiten, da die Implementierung bei jeder Sprache ne andere ist. Eine Einfache for schleife mit Zeitmessung wäre da wohl sinnvoller. Nebst dem sollte man bei beiden die release Version und maximale Optimierung oder keine Optimierung verwenden.