Ich habe meinen Code mit dem Zeitprofiler von Instrument profiliert und in die Disassembly gezoomt, hier ist ein Ausschnitt seiner Ergebnisse:
Ich würde nicht erwarten, dass eine Anweisung mov
23,3% der Zeit benötigt, während eine Anweisung div
praktisch nichts zu tun hat.
Dies lässt mich glauben, dass diese Ergebnisse unzuverlässig sind.
Ist das wahr und bekannt? Oder habe ich gerade einen Instrumenten-Bug? Oder gibt es eine Option, die ich verwenden muss, um zuverlässige Ergebnisse zu erhalten?
Gibt es einen Hinweis, der sich auf dieses Problem ausdehnt?
Erstens ist es möglich, dass einige Zählungen, die wirklich zu divss
gehören, späteren Anweisungen in Rechnung gestellt werden, was als" skid " bezeichnet wird. (Siehe auch den Rest dieses Kommentarthreads für weitere Details.) Vermutlich ist Xcode wie Linux perf
und verwendet den festen cpu_clk_unhalted.thread
-Zähler für cycles
anstelle eines der programmierbaren Zähler. Dies ist kein "präzises" Ereignis (PEBS), so dass Skids möglich sind. Wie @BeeOnRope darauf hinweist , Sie können ein PEBS-Ereignis verwenden, das einmal pro Zyklus (wie UOPS_RETIRED < 16
) als PEBS-Ersatz für den Zähler mit festen Zyklen tickt, wodurch ein Teil der Abhängigkeit vom Interrupt-Verhalten entfernt wird.
Aber die Art und Weise, wie Zähler grundsätzlich für die Pipeline- / Out-of-Order-Ausführung funktionieren, erklärt auch das meiste, was Sie sehen. Oder es könnte; Sie haben die vollständige Schleife nicht angezeigt, sodass wir den Code nicht in einem einfachen Pipeline-Modell wie IACA oder manuell mit Hardware-Handbüchern wie Ссылка simulieren können und Intels Optimierungshandbuch. (Und Sie haben nicht einmal angegeben, welche Mikroarchitektur Sie haben. Ich denke, es ist ein Mitglied der Intel Sandybridge-Familie auf einem Mac).
Zählungen für cycles
werden normalerweise für den Befehl berechnet, der auf das Ergebnis wartet , nicht normalerweise der Befehl, der das Ergebnis langsam erzeugt. Pipelined-CPUs werden nicht blockiert, bis Sie versuchen, ein Ergebnis zu lesen, das noch nicht fertig ist.
Die Out-of-Order-Ausführung erschwert dies massiv, ist aber im Allgemeinen immer noch wahr, wenn es eine wirklich langsame Anweisung gibt, wie eine Last, die häufig im Cache fehlt. Wenn der cycles
-Zähler überläuft (einen Interrupt auslöst), gibt es viele Befehle im Flug, aber nur einer kann der RIP sein, der diesem Leistungszähler-Ereignis zugeordnet ist. Es ist auch der RIP, bei dem die Ausführung nach dem Interrupt fortgesetzt wird.
Was passiert also, wenn ein Interrupt ausgelöst wird? Siehe dazu Andy Glews Antwort Dies erklärt die Interna von Perf-Counter-Interrupts in der Pipeline der Intel P6-Mikroarchitektur, und warum (vor PEBS) wurden sie immer verzögert. Sandybridge-Familie ist dafür P6 ähnlich.
Ich denke, ein vernünftiges mentales Modell für Perf-Counter-Interrupts auf Intel-CPUs ist, dass es alle Ups verwirft, die noch nicht an eine Ausführungseinheit gesendet wurden. Aber ALU-Ups, die bereits versandt wurden, gehen durch die Pipeline in den Ruhestand (wenn keine jüngeren UPs verworfen werden), anstatt abgebrochen zu werden, was sinnvoll ist, weil die maximale zusätzliche Latenz ~ 16 Zyklen für sqrtpd
und Das Löschen der Speicherwarteschlange kann leicht länger dauern. (Ausstehende Geschäfte, die bereits in den Ruhestand gegangen sind, können nicht zurückgesetzt werden). IDK über Ladungen / Geschäfte, die nicht in den Ruhestand gegangen sind; zumindest sind die Lasten wahrscheinlich verworfen.
Ich gehe davon aus, dass es einfach ist, Schleifen zu konstruieren, die keine Zählungen für divss
anzeigen, wenn die CPU manchmal darauf wartet, dass sie ihre Ausgaben erzeugt. Wenn es verworfen wird, ohne sich zurückzuziehen, wäre es die nächste Anweisung, wenn der Interrupt fortgesetzt wird, also würden Sie (außer skids) viele Zählimpulse dafür sehen.
Somit zeigt die Verteilung von cycles
counts Ihnen an, welche Anweisungen die meiste Zeit als älteste noch nicht verteilte Anweisung im Scheduler ausgeben . (Oder im Falle von Front-End-Ständen, welche Anweisungen die CPU blockiert, versucht zu holen / zu dekodieren / ausgeben). Denken Sie daran, dies bedeutet normalerweise, dass es Ihnen die Anweisungen zeigt, die auf Eingaben warten, und nicht die Anweisungen, die sie langsam produzieren.
(Hmm, Das ist vielleicht nicht richtig , und ich habe nicht so viel getestet. Normalerweise verwende ich perf stat
, um die Gesamtzählung für eine ganze Schleife in einem Mikrobenchmark zu betrachten, nicht statistische Profile mit perf record
. addss
und mulss
haben eine höhere Latenz als andps
, also erwarten Sie, dass andps
auf die Eingabe von xmm5 wartet, wenn mein vorgeschlagenes Modell richtig war.)
Wie auch immer, das allgemeine Problem ist mit mehreren Anweisungen im Flug auf einmal, was macht die HW "verantwortlich", wenn der cycles
-Zähler umgeht?
Beachten Sie, dass divss
langsam ist, um das Ergebnis zu erzeugen, aber es ist nur ein einzelner Befehl (anders als Ganzzahl div
, der auf AMD und Intel mikrocodiert ist). Wenn Sie die Latenz oder den nicht vollständig pipelinebedingten Durchsatz nicht einschränken, Es ist nicht langsamer als mulss
, weil es sich genauso gut mit dem umgebenden Code überschneiden kann.
( divss
/ divps
ist nicht vollständig pipelined. Auf Haswell zum Beispiel kann ein unabhängiger divps
alle 7 Zyklen starten. Aber jeder benötigt nur 10-13 Zyklen um sein Ergebnis zu produzieren. Alle anderen Ausführungseinheiten sind Vollständig pipeline-fähig, in der Lage, jeden Zyklus eine neue Operation mit unabhängigen Daten zu starten.)
Betrachten Sie eine große Schleife, die Engpässe beim Durchsatz, nicht die Latenz einer Loop-getragenen Abhängigkeit, und nur divss
benötigt, um einmal pro 20 FP-Anweisungen ausgeführt zu werden. Die Verwendung von divss
durch eine Konstante anstelle von mulss
mit der reziproken Konstante sollte (fast) keinen Unterschied in der Leistung machen. (In der Praxis ist das Scheduling außerhalb der Reihenfolge nicht perfekt, und längere Abhängigkeitsketten schaden manchen sogar, wenn sie nicht durchgeschleift werden, weil sie mehr Instruktionen benötigen, um diese Latenz zu verbergen und den maximalen Durchsatz aufrechtzuerhalten, dh für das Out -Order-Kern, um die Parallelität auf Befehlsebene zu finden.)
Wie auch immer, der Punkt hier ist, dass divss
ein einzelner UOP ist und es sinnvoll ist, nicht viele Counts für das Ereignis cycles
zu erhalten, abhängig vom umgebenden Code.
Sie sehen den gleichen Effekt bei einer Cache-Miss-Last: Die Last selbst erhält meistens nur dann Zählungen, wenn sie auf die Register im Adressierungsmodus warten muss und die erste Anweisung in der Abhängigkeitskette, die die geladenen Daten verwendet, erhält viel zählt.
Was Ihr Profilergebnis uns möglicherweise sagt :
Der divss
muss nicht warten, bis seine Eingaben fertig sind. (Der movaps %xmm3, %xmm5
vor dem divss
benötigt manchmal einige Zyklen, aber der divss
tut dies nie.)
Es könnte sein, dass wir Engpässe beim Durchsatz von divss
Die Abhängigkeitskette mit xmm5
nach divss
erhält einige Zählungen. Die Out-of-Order-Ausführung muss funktionieren, um mehrere unabhängige Iterationen gleichzeitig im Flug zu halten.
Die maxss
/ movaps
Schleifen-getragene Abhängigkeitskette kann ein erheblicher Engpass sein. (Besonders, wenn Sie auf Skylake sind, wo divss
throughput ist einer pro 3 Uhren, aber maxss
Latenz ist 4 Zyklen. Und Ressourcenkonflikte von Wettbewerb für Ports 0 und 1 wird maxss verzögern.)
Die hohen Zählungen für movaps
könnten darauf zurückzuführen sein, dass maxss
die einzige schleifengebundene Abhängigkeit in dem Teil der Schleife bildet, den Sie anzeigen. Es ist also plausibel, dass maxss
wirklich langsam Ergebnisse produziert. Aber wenn es wirklich eine loopgeführte dep-Kette wäre, die den größten Engpass darstellt, würden Sie erwarten, viele Zählungen in maxss
selbst zu sehen, da es auf seine Eingabe von der letzten Iteration warten würde.
Aber vielleicht ist mov-elimination "besonders" und alle Zahlen werden aus irgendeinem Grund auf movaps
angerechnet? Auf Ivybridge und später CPUs, Registerkopien benötigen keine Ausführungseinheit, sondern werden stattdessen in der Phase "Issue / Rename" der Pipeline behandelt .
Ist das wahr und bekannt?
Ja, es ist ein bekanntes Problem mit Profiling-Tools auf Intel x86. Ich habe es beobachtet (Zeit, die verdächtig scheinbar unschuldigen Anweisungen zugewiesen wurde), sowohl mit Linux perf_events als auch mit Intel VTune. Es wurde auch von anderen Leuten woanders berichtet.
Eine bessere und ehrlichere Visualisierung der gesammelten Ergebnisse hätte alle Stichproben innerhalb jedes Basisblocks zusammengefasst und den resultierenden Wert, der einem Basisblock zugeordnet ist, demonstriert, nicht seine einzelnen Anweisungen. Nicht 100% idiotensicher, aber ein bisschen besser und ehrlich,
Oder gibt es eine Option, die ich verwenden muss, um zuverlässige Ergebnisse zu erhalten?
Ich weiß nicht, ob neuere Profiling-Hardware, nämlich Tools, die auf Intel Processor Trace basieren (verfügbar ab Broadwell, aber in Skylake verbessert) anstelle von älteren PEBS, genauere Daten liefern würden. Ich denke, man muss zuerst mit solchen Werkzeugen experimentieren.
Tags und Links x86 profiling xcode instruments intel-pmu