Ein weiterer Vergleich der Leistung von C ++ und C #

    Навеяно вот этой статьей.

    Существует три мнения относительно производительности C++ и C#.

    Те кто знают (или думают что знают) C++ считают, что C++ в разы или даже на порядки быстрее.
    Те кто знают C++ и C# знают, что для обычных задач быстродействие C++ не нужно, а там где нужно — можно и C#-код заоптимизировать до невозможности. Верхний предел оптимизации у C++ выше, чем у C#, но такие рекорды никому не нужны.
    Те кто знают только C# — никогда не испытывали проблем с его быстродействием.

    Люди из первой категории все время пытаются доказать свою правоту. При этом приводят примеры оптимизированного кода на C++ и самого пессимизированного кода на C#.

    Пример «типичного» сравнения


    Bild
    Любой программист, который знает C# сразу увидит две ошибки:
    1. Ein Aufruf von GC.Collect, der alle in der Laufzeit vorgenommenen Optimierungen für die Garbage Collection zunichte macht.
    2. Verwenden einer for-Schleife, die garantiert keine Grenzwertprüfungen bei jedem Aufruf des Arrays aufhebt.

    Gleichzeitig wird in der Realität kein C # -Programmierer Code mit GC.Collect schreiben, und ein sehr kleiner Teil der Programmierer wird einen Fehler in der for-Schleife machen.
    Was ist der Sinn des Vergleichs von garantiert ineffizientem C # -Code selbst mit regulärem C ++ - Code? Es sei denn, um ihren Standpunkt zu beweisen.

    Fairer Vergleich


    Um Sprachen ehrlich zu vergleichen, müssen Sie den Beispielcode in beiden Sprachen vergleichen. Das heißt, ein solcher Code kann in Programmen mit einer Wahrscheinlichkeit gefunden werden, die größer als der statistische Fehler ist.

    Als Beispiel verwende ich die gleiche Blasensortierung eines Arrays.

    Tests für C ++


    Für den C ++ - Fall werde ich drei Optionen testen:
    1. C-Array (Zeiger)
    2. std :: array
    3. std :: vector

    Jeder Test wird 100 Mal ausgeführt und das Ergebnis wird gemittelt.
    Messcode
    std::chrono::high_resolution_clock::duration measure(std::function f, int n = 100)
    {
    	auto begin = std::chrono::high_resolution_clock::now();
    	for (int i = 0; i < n; i++)
    	{
    		f();
    	}
    	auto end = std::chrono::high_resolution_clock::now();
    	return (end - begin) / n;
    }
    


    Testcode für C-Style
    Code
    void c_style_sort(int *m, int n) 
    {
    	for (int i = 0; i < N - 1; i++)
    		for (int j = i + 1; j < N; j++) {
    			if (m[i] < m[j])
    			{
    				int tmp = m[i];
    				m[i] = m[j];
    				m[j] = tmp;
    			}
    		}
    }
    void c_style_test()
    {
    	int* m = new int[N];
    	for (int i = 0; i < N; i++)
    	{
    		m[i] = i;
    	}
    	c_style_sort(m, N);
    	delete[] m;
    }
    


    Im Test wird ein Array ehrlich erstellt, gefüllt, zerstört und eine Funktion aufgerufen, um es zu sortieren.

    Testcode für std :: array
    Code
    void cpp_style_sort(std::array &m)
    {
    	auto n = m.size();
    	for (int i = 0; i < n-1; i++)
    		for (int j = i + 1; j < n; j++) {
    			if (m[i] < m[j])
    			{
    				int tmp = m[i];
    				m[i] = m[j];
    				m[j] = tmp;
    			}
    		}
    }
    void cpp_style_test()
    {
    	std::array m;
    	for (int i = 0; i < N; i++)
    	{
    		m[i] = i; 
    	}
    	cpp_style_sort(m);
    }
    


    Der Code ist fast derselbe wie im ersten Fall, aber jetzt geben wir den Speicher nicht explizit frei, sondern geben alles für die Entsorgung der automatischen Zerstörung.

    Wer weiß, dass C ++ bereits verstanden hat, dass std :: array keine Zuweisungen verursacht, und das Array selbst im Klassentext gespeichert ist, in diesem Beispiel also auf dem Stack. std :: array sollte der klare Anführer in diesem Rennen sein.

    Testcode für std :: vector
    Code
    void vector_sort(std::vector &m)
    {
    	auto n = m.size();
    	for (int i = 0; i < n - 1; i++)
    		for (int j = i + 1; j < n; j++) {
    			if (m[i] < m[j])
    			{
    				int tmp = m[i];
    				m[i] = m[j];
    				m[j] = tmp;
    			}
    		}
    }
    void vector_test()
    {
    	std::vector m;
    	m.reserve(N);
    	for (int i = 0; i < N; i++)
    	{
    		m.push_back(i);
    	}
    	vector_sort(m);
    }
    


    Der Code ist der Version mit std :: array völlig ähnlich. Aber std :: vector ist im Gegensatz zu std :: array ein veränderbares Array. Vector verwendet daher dynamischen Speicher, um das Array zu speichern, und muss ehrlich prüfen, ob es außerhalb der Grenzen liegt.

    Tests für C #


    Ich werde auch drei Tests machen:
    1. Regelmäßige Anordnung
    2. Einfaches Array mit unsicheren (Zeigern)
    3. System.Collections.Generic.List

    Im Gegensatz zum „typischen“ Leistungsvergleichsansatz werde ich GC.Collect nicht aufrufen, sondern mich auf die Laufzeit verlassen.

    Die Tests werden mehrmals ausgeführt, sodass die Speicherbereinigung stattfindet und bei den Messungen berücksichtigt wird.
    Messcode
            static long Measure(Action f, int n = 100)
            {
                var sw = System.Diagnostics.Stopwatch.StartNew();
                for (int i = 0; i < n; i++)
                {
                    f();
                }
                return sw.ElapsedMilliseconds / n;
            }
    


    Testcode für ein reguläres Array
    Code
    static void ArrayTest()
    {
        var m = new int[N];
        for (int i = 0; i < m.Length; i++)
        {
            m[i] = i;
        }
        ArraySort(m);
    }
    static void ArraySort(int[] m)
    {
        for (int i = 0; i < m.Length - 1; i++)
            for (int j = i + 1; j < m.Length; j++)
            {
                if (m[i] < m[j])
                {
                    int tmp = m[i];
                    m[i] = m[j];
                    m[j] = tmp;
                }
            }
    }
    


    Ein sehr wichtiger Punkt ist, dass m.Length (minus eine Konstante) in der for-Schleife enthalten ist. Ein solches Muster wird von JIT definiert und eliminiert Array-Grenzüberprüfungen.

    Testcode für unsicher
    Code
    static unsafe void UnsafeTest()
    {
        var m = new int[N];
        fixed(int* ptr = &m[0])
        {
            for (int i = 0; i < N; i++)
            {
                ptr[i] = i;
            }
            UnsafeSort(ptr, N);
        }
    }
    static unsafe void UnsafeSort(int* m, int n)
    {
        for (int i = 0; i < n - 1; i++)
            for (int j = i + 1; j < n; j++)
            {
                if (m[i] < m[j])
                {
                    int tmp = m[i];
                    m[i] = m[j];
                    m[j] = tmp;
                }
            }
    }
    


    Die Sortierung sieht gleich aus, es werden nur Zeiger verwendet und garantiert keine Überprüfungen (und keine Optimierungen). Ich habe den Test nicht mit einem festen Array durchgeführt , weil Sie ihn in Wirklichkeit nicht erfüllen werden.

    Testcode für Liste

    Code
            static void ListTest()
            {
                var m = new List(N);
                for (int i = 0; i < N; i++)
                {
                    m.Add(i);
                }
                ListSort(m);
            }
            static void ListSort(List m)
            {
                var n = m.Count;
                for (int i = 0; i < n - 1; i++)
                    for (int j = i + 1; j < n; j++)
                    {
                        if (m[i] < m[j])
                        {
                            int tmp = m[i];
                            m[i] = m[j];
                            m[j] = tmp;
                        }
                    }
            }
    


    Im Gegensatz zu einem regulären Array gibt es in JIT keine Optimierungen für List. Daher speichern wir die Länge der Liste in einer Variablen.

    Ergebnisse


    Ich habe den Code in Visual Studio 2015 kompiliert und unter .NET Framework 4.6 ausgeführt. Überall Standardmäßig Einstellungen freigeben.

    Ich habe folgende Ergebnisse erhalten:
    Testx86x64
    (C ++) C-Stil55ms55ms
    (C ++) std :: array0 ms (52 ms)65ms
    (C ++) std :: vector100ms65ms
    (C #) Array67ms90ms
    (C #) unsicheres Array63ms105ms
    (C #) Liste395ms390ms

    Im x86-Modus hat das Optimierungsprogramm die Sortierung für std :: array vollständig verworfen, so dass sich herausstellte, dass 0 angezeigt wird. In Wirklichkeit arbeitet es aufgrund fehlender Zuordnungen etwas schneller als das Array im C-Stil.

    Schlussfolgerungen


    • Für beide Sprachen ist der idiomatische Code am effizientesten (im Allgemeinen seltsam, wenn dies nicht der Fall ist).
    • C # ist in solchen Tasks um 20% -50% langsamer als C ++ (dies ist die Obergrenze)
    • Für x64 müssen Sie separat optimieren (offensichtlich, aber immer noch)

    Den Code finden Sie hier - github.com/gandjustas/PerfTestCSharpVsCPP

    Was bleibt hinter den Kulissen


    In C # ist ein Array ein Referenztyp. Daher können Sie problemlos jede Funktion übergeben. In C ++ verhalten sich alle Container wie "dimensionale" Typen und kopieren alle Inhalte, wenn sie als Parameter übergeben werden. Sie müssen Code für C ++ sorgfältig schreiben, um die Arrays nicht erneut zu kopieren. Häufig müssen Sie auf intelligente Zeiger zurückgreifen, die zusätzlichen Aufwand verursachen.

    In einer großen Anwendung verschlingt ein solcher Overhead alle Vorteile der Verwendung von C ++. Sie können nur bei Problemen mit viel Mathematik, bei denen der C ++ - Compiler traditionell stark ist, oder wenn Sie C ++ - Redewendungen aufgeben und bloße Zeiger verwenden, spürbar gewinnen. Letzteres ist jedoch mit einer Reihe von Fehlern behaftet.

    Update


    Auf Wunsch der Leser habe ich die Tests geändert und ergänzt:
    • Verwendet std :: swap anstelle des manuellen Austauschs in C ++ - Code
    • Erstellte einen .at-Aufruf für einen Vektor anstelle von [], damit die Grenzen im Release-Build überprüft werden
    • Ein Projekt mit .NET Native wurde hinzugefügt

    Die Ergebnisse sind wie folgt:
    Testx86x64
    (C ++) C-Stil60ms52ms
    (C ++) std :: array51ms60ms
    (C ++) std :: vector147ms81ms
    (C #) Array67ms90ms
    (C #) unsicheres Array63ms105ms
    (C #) Liste395ms390ms
    Array (C # + .NET Native)62ms59ms
    (C # + .NET Native) unsicheres Array63ms52ms
    (C # + .NET Native) Liste274ms282ms

    Es stellt sich heraus, dass die Leistung in .NET Native dieselbe ist wie in C ++.
    Aufgrund der Bremsen beim Debuggen für std :: swap wurde das Debuggen unmöglich.

    Quellen an derselben Stelle: github.com/gandjustas/PerfTestCSharpVsCPP

    Jetzt auch beliebt: