Unerklärliche Ausgaben im Zusammenhang mit dem Abrufen von Daten aus einer Funktion in optimiertem und zeitgesteuertem C ++ - Code

9

Ich habe einen optimierten Code für einen Algorithmus geschrieben, der einen Mengenvektor berechnet. Ich habe es vor und nach verschiedenen Versuchen zeitlich abgestimmt, um die in der Funktion berechneten Daten aus der Funktion zu bekommen. Ich denke, dass die besondere Art der Berechnung oder die Art des Mengenvektors nicht relevant ist. Es folgt ein Überblick über den Code, Timings und Details.

Der gesamte Code wurde mit den folgenden Flags kompiliert:

g ++ -Wall -Wextra -Werror -std = c ++ 11 -pedantische -O3

Ich habe eine Klasse wie folgt:

%Vor%

Und ein Haupt wie folgt:

%Vor%

Hier sind einige relevante Details zu den Daten:

  • 20 Millionen Paare werden bei Standardeingabe gelesen (von einer Datei umgeleitet)
  • 20 Millionen Aufrufe an c.doWork
  • 60 Millionen TOTAL-Iterationen durch die äußere Schleife in c.doWork
  • 180 Millionen TOTAL-Iterationen durch die innere Schleife in c.doWork

All dies erfordert genau 5 Minuten und 48 Sekunden zum Ausführen. Natürlich kann ich das Array innerhalb der Klassenfunktion drucken, und das habe ich gemacht, aber ich werde den Code öffentlich veröffentlichen, und einige Anwendungsfälle können auch etwas anderes als das Drucken des Vektors beinhalten. In diesem Fall muss ich die Funktionssignatur ändern, um die Daten tatsächlich an den Benutzer zu senden. Hier entsteht das Problem. Dinge, die ich versucht habe:

  • Erstellen Sie einen Vektor in main und übergeben Sie ihn als Referenz:

    %Vor%

    Dies erfordert 7 Minuten 30 Sekunden. Wenn Sie den Aufruf von std :: fill entfernen, wird dies nur um 15 Sekunden reduziert, sodass die Diskrepanz nicht berücksichtigt wird.

  • Erstellen Sie einen Vektor innerhalb der doWork-Funktion und geben Sie ihn zurück, wobei Sie die Bewegungssemantik ausnutzen. Da dies für jedes Ergebnis eine dynamische Zuweisung erfordert, habe ich nicht erwartet, dass dies schnell erfolgt. Seltsamerweise ist es nicht viel langsamer. 7 Minuten 40 Sekunden.

  • Rückgabe des std :: -Arrays, das sich derzeit in doWork befindet, nach Wert. Natürlich muss dies die Daten bei der Rückkehr kopieren, da das Stapel-Array keine Bewegungssemantik unterstützt. 7 Minuten 30 Sekunden

  • Übergeben Sie ein std :: array durch Verweis.

    %Vor%

    Ich würde erwarten, dass dies in etwa dem Original entspricht. Die Daten werden in der Hauptfunktion auf den Stapel gelegt und durch Verweis auf doWork übergeben, der sie füllt. 7 Minuten 20 Sekunden. Dieser hemmt mich wirklich.

Ich habe nicht versucht, Zeiger in doWork zu übergeben, da dies der Übergabe durch Referenz entsprechen sollte.

Eine Lösung besteht natürlich darin, zwei Versionen der Funktion zu haben: eine, die lokal druckt und eine, die zurückkehrt. Der Roadblock ist, dass ich ALLEN Code duplizieren müsste, weil das ganze Problem hier ist, dass ich die Ergebnisse aus einer Funktion nicht effizient erhalten kann.

Ich bin also verwirrt. Ich verstehe, dass jede dieser Lösungen eine zusätzliche Dereferenzierung für jeden Zugriff auf das Array / Vektor innerhalb der Arbeit erfordert, aber diese zusätzlichen Dereferenzierungen sind im Vergleich zu der großen Anzahl anderer schneller Operationen und mühsamer datenabhängiger Verzweigungen sehr trivial.

Ich begrüße irgendwelche Ideen, um dies zu erklären. Mein einziger Gedanke ist, dass der Code vom Compiler optimiert wird, so dass einige ansonsten notwendige Komponenten der Berechnung im ursprünglichen Fall weggelassen werden, weil der Compiler erkennt, dass es nicht notwendig ist. Dies scheint jedoch in mehreren Punkten kontraindiziert zu sein:

  1. Wenn Sie den Code in den Schleifen ändern, ändert sich die Zeiteinstellung.
  2. Die ursprünglichen Timings sind 5 Minuten und 50 Sekunden, während das Lesen der Paare aus der Datei nur 12 Sekunden dauert, also wird viel gemacht.
  3. Vielleicht werden nur Operationen mit Zählimpulsen weg optimiert, aber das scheint eine seltsam selektive Optimierung zu sein, vorausgesetzt, dass wenn diese optimiert werden, der Compiler erkennen könnte, dass unterstützende Berechnungen in der Arbeit auch unnötig sind.
  4. Wenn Operationen mit Zählimpulsen weg optimiert werden, warum sind sie in den anderen Fällen nicht optimiert. Ich benutze sie nicht wirklich in main.

Ist doWork unabhängig von main kompiliert und optimiert, und wenn also die Funktion verpflichtet ist, die Daten in irgendeiner Form zurückzugeben, kann sie nicht sicher sein, ob sie verwendet wird oder nicht?

Ist meine Methode des Profilierens ohne Drucken, die die Kosten des Druckes vermeiden sollte, um die relativen Unterschiede in verschiedenen Methoden zu betonen, fehlerhaft?

Ich bin dankbar für jedes Licht, das du vergießen kannst.

    
rg6 28.04.2013, 19:44
quelle

3 Antworten

0

Was ich tun würde, ist ein paar Mal anzuhalten und zu sehen, was es die meiste Zeit macht. Wenn ich Ihren Code betrachte, würde ich annehmen, dass die meiste Zeit entweder in a) die innerste Schleife geht, insbesondere die Indexberechnung, oder 2) die Zuweisung von std::array .

Wenn die Größe von counts immer 40 ist, würde ich einfach

machen %Vor%

Das wird auf dem Stack zugewiesen, was keine Zeit kostet, und memset braucht keine Zeit im Vergleich zu dem, was Sie sonst noch tun.

Wenn die Größe zur Laufzeit variiert, dann mache ich eine statische Zuweisung wie folgt:

%Vor%

Auf diese Weise ist counts immer groß genug und Der Punkt ist 1) do new so oft wie möglich, und 2) halten Sie die Indizierung in counts so einfach wie möglich.

Aber zuerst würde ich die Pausen machen, nur um sicher zu gehen. Nachdem ich es repariert hatte, würde ich das wieder tun, um zu sehen, was ich als nächstes reparieren könnte.

    
Mike Dunlavey 28.04.2013, 22:13
quelle
0

Compiler-Optimierungen sind ein Ort, den Sie betrachten müssen, aber es gibt noch einen weiteren Ort, an dem Sie suchen müssen. Änderungen, die Sie im Code vorgenommen haben, können das Cache-Layout stören. Wenn der Speicher, der dem Array zugewiesen ist, sich jedes Mal in einem anderen Teil des Speichers befindet, kann sich die Anzahl der Cache-Misses in Ihrem System erhöhen, was wiederum die Leistung beeinträchtigt. Sie können sich Hardware-Leistungsindikatoren auf Ihrer CPU ansehen, um eine bessere Schätzung zu treffen.

    
Always Asking 28.04.2013 22:14
quelle
0

Es gibt Zeiten, in denen unorthodoxe Lösungen anwendbar sind, und dies kann eine sein. Haben Sie darüber nachgedacht, das Array global zu machen?

Dennoch ist der entscheidende Vorteil, den lokale Variablen haben, dass der Optimierer alle Zugriffe darauf finden kann, indem er nur Informationen aus der Funktion verwendet. Das erleichtert die Registerzuweisung erheblich.

Eine static Variable innerhalb der Funktion ist fast gleich, aber in Ihrem Fall würde die Adresse dieses Stack-Arrays entkommen und den Optimierer erneut schlagen.

    
MSalters 29.04.2013 00:04
quelle