2013-05-21 6 views
16

Beim Durchlaufen einiger Qt-Codes stieß ich auf Folgendes. Die Funktion QMainWindowLayout::invalidate() hat die folgende Umsetzung:Warum sollte ein Compiler diese Assembly generieren?

void QMainWindowLayout::invalidate() 
{ 
QLayout::invalidate() 
minSize = szHint = QSize(); 
} 

Es wird dazu zusammengestellt:

<invalidate()>  push %rbx 
<invalidate()+1>  mov %rdi,%rbx 
<invalidate()+4>  callq 0x7ffff4fd9090 <QLayout::invalidate()> 
<invalidate()+9>  movl $0xffffffff,0x564(%rbx) 
<invalidate()+19>  movl $0xffffffff,0x568(%rbx) 
<invalidate()+29>  mov 0x564(%rbx),%rax 
<invalidate()+36>  mov %rax,0x56c(%rbx) 
<invalidate()+43>  pop %rbx 
<invalidate()+44>  retq 

Die Montage von invalidate + 9 zu entkräften + 36 dumm scheint. Zuerst schreibt der Code -1 in% rbx + 0x564 und% rbx + 0x568, aber dann lädt er diese -1 von% rbx + 0x564 zurück in ein Register, nur um es in% rbx + 0x56c zu schreiben. Dies scheint etwas zu sein, was der Compiler leicht in der Lage sein sollte, sofort in einen anderen Schritt zu optimieren.

Also ist dieser dumme Code (und wenn ja, warum würde der Compiler es nicht optimieren?) Oder ist das irgendwie sehr clever und schneller als nur einen weiteren Zug sofort zu verwenden?

(Anmerkung:.. Dieser Code ist von der normalen Release-Bibliothek Build von ubuntu ausgeliefert, so wurde es vermutlich von GCC in optimize Modus kompiliert Die minSize und szHint Variablen sind normale Variablen vom Typ QSize)

+3

QT ist eine Benutzeroberfläche, richtig? Wie oft hintereinander müssten Sie ein Fenster ungültig machen? Wie performant müsste das eigentlich sein? Die Art von Mikro-Optimierung, die Sie beschreiben, ist fast sicher nicht die Mühe wert für den minimalen Nutzen, der entstehen würde. –

+0

Es scheint in der Tat suboptimal zu sein, vielleicht hat der Gucklochoptimierer das nicht bekommen. –

+11

@RobertHarvey Aber das ist nicht der Punkt hier - OP versucht nicht zu optimieren, er versucht den Grund zu verstehen. –

Antwort

13

Nicht sicher Du hast Recht, wenn du sagst, dass es dumm ist. Ich denke, der Compiler könnte hier versuchen, die Code-Größe zu optimieren. Es gibt keinen 64-Bit-Sofort-Speicher-mov-Befehl. Also muss der Compiler zwei mov-Anweisungen erzeugen, genau wie oben. Jeder von ihnen wäre 10 Bytes, die 2 erzeugten Bewegungen sind 14 Bytes. Es wurde so geschrieben, dass es höchstwahrscheinlich keine Speicherlatenz gibt, also denke ich nicht, dass du hier irgendwelche Leistungseinbußen hinnehmen würdest.

+0

... und zusätzlich, wenn Sie ein 'mov ..., (addr)' gefolgt von einem 'mov (addr), ...' machen, dann ist der 2. Cache heiß, d. H. Es gibt wenig Strafe dafür. Die einzige Optimierung, die ich mir vorstellen kann, wäre 'pcmpeq% xmm0,% xmm0; movdqu% xmm0, 0x564 (% rbx) 'um die ganzen 16 Bytes auf alle' 0xff..' zu setzen, aber es ist ziemlich schwierig, zwei Variablen auf diese Weise zu "verschmelzen" - und wahrscheinlich nicht ganz normkonform. C++ laden/speichern Sichtbarkeitsgarantien. –

+2

+1 für * "Es gibt keine 64-Bit-Anweisung für den sofortigen Speicher mov", das ist alles, was gesagt werden muss. –

+0

Ich wusste den Teil über die No 64 Bit Bewegung nicht sofort, also ist das wohl die Lösung. Darüber hinaus scheint es keine echten Kosten auf x86 zu geben, wenn ein nicht ausgerichteter Speicherzugriff die Grenzen der Cachezeile nicht überschreitet. – JanKanis

1

Ich würde brechen die Linien wie diese nach unten (man denke Kommentar gleichen Schritte mehrere haben)

Diese beiden Linien stammt aus der Inline-Definition von QSize()http://qt.gitorious.org/qt/qt/blobs/4.7/src/corelib/tools/qsize.h die separat jedes Feld gesetzt. Meine Vermutung ist auch, dass 0x564 (% rbx) die Adresse von szHint ist, die auch gleichzeitig gesetzt wird.

<invalidate()+9>  movl $0xffffffff,0x564(%rbx) 
<invalidate()+19>  movl $0xffffffff,0x568(%rbx) 

Diese Zeilen setzen schließlich minSize 64-Bit-Operationen verwenden, da der Compiler jetzt die Größe eines QSize Objekt kennen. Und die Adresse minSize ist 0x56c (% RBX)

<invalidate()+29>  mov 0x564(%rbx),%rax 
<invalidate()+36>  mov %rax,0x56c(%rbx) 

Hinweis. Der erste Teil setzt zwei separate Felder und der nächste Teil kopiert ein QSize Objekt (unabhängig vom Inhalt). Die Frage ist dann, sollte der Compiler klug genug sein, einen zusammengesetzten 64-Bit-Wert zu erstellen, weil er sah voreingestellte Werte nur früher? Nicht sicher darüber ...

+2

Ja, Compiler sind in der Regel in der Lage, diese Art von Optimierungen zu tun. Es ist bekannt als konstante Faltung. – JanKanis

+0

@Somejan Cool, wusste nicht, dass :) – epatel

0

Zusätzlich zu Guillaumes Antwort ist das 64-Bit-Laden/Speichern nicht ausgerichtet. Aber gemäß der Intel optimization guide (p 3-62)

Falscher Datenzugriff kann zu erheblichen Leistungseinbußen führen. Dies gilt insbesondere für Cache-Zeilenaufteilungen. Die Größe eines Cache Zeile ist 64 Bytes im Pentium 4 und anderen aktuellen Intel-Prozessoren, einschließlich Prozessoren auf Intel Core-Mikroarchitektur basiert.

Ein Zugriff auf Daten, die auf 64-Byte-Grenze nicht ausgerichtet sind, führt zu zwei Speicher- -Zugriffen und erfordert die Ausführung von mehreren μops (anstelle von einem). Zugriffe, die 64-Byte-Grenzen umfassen, verursachen wahrscheinlich eine große Leistungseinbuße. Die Kosten für jeden Stand sind im Allgemeinen auf Maschinen mit längeren Pipelines höher.

Welche imo bedeutet, dass eine nicht ausgerichtete load/store, die eine Cache-Grenze nicht überschreitet, billig ist. In diesem Fall war der Basiszeiger in dem Prozess, den ich debugging, 0x10f9bb0, also sind die zwei Variablen 20 und 28 Bytes in der Cache-Line.

Normalerweise verwenden Intel-Prozessoren den Speicher zum Laden der Weiterleitung, so dass eine Last eines gerade gespeicherten Werts den Cache nicht einmal berühren muss. Aber die gleiche Anleitung gibt auch an, dass eine große Last von mehreren kleineren Geschäften nicht speichern-laden-Forward aber blockiert (p 3-66, p 3-68)

Assembly/Compiler Coding Rule 49. (H Auswirkung, M Allgemeinheit) Die Daten von eine Last, die von einem Geschäft weitergeleitet wird, muss vollständig innerhalb der Geschäftsdaten enthalten sein.

; A. Large load stall 
mov  mem, eax  ; Store dword to address “MEM" 
mov  mem + 4, ebx ; Store dword to address “MEM + 4" 
fld  mem    ; Load qword at address “MEM", stalls 

der Code in Frage So verursacht wahrscheinlich einen Stall, und deshalb zu glauben, ich bin geneigt, es nicht optimal ist. Ich wäre nicht sehr überrascht, wenn der GCC solche Einschränkungen nicht vollständig berücksichtigen würde. Weiß jemand, ob/wie viel Modellierung von Store-to-Load-Weiterleitungsbeschränkungen GCC tut?

BEARBEITEN: einige experimentieren mit dem Hinzufügen von Füllwerten vor den MinSize/szZint Feldern zeigt, dass GCC überhaupt nicht interessiert, wo die Grenzen der Cache-Linie sind, und auch nicht Clang.

8

Der Code ist "weniger als perfekt".

Für die Codegröße addieren sich diese 4 Anweisungen zu 34 Bytes. Eine viel kleinere Sequenz (19 Bytes) ist möglich:

00000000 31C0    xor eax,eax 
00000002 48F7D0   not rax 
00000005 48898364050000 mov [rbx+0x564],rax 
0000000C 4889836C050000 mov [rbx+0x56c],rax 

;Note: XOR above clears RAX due to zero extension 

Für die Leistung sind die Dinge nicht so einfach. Die CPU möchte viele Anweisungen gleichzeitig ausführen, und der obige Code bricht das. Zum Beispiel:

xor eax,eax 
not rax     ;Must wait until previous instruction finishes 
mov [rbx+0x564],rax  ;Must wait until previous instruction finishes 
mov [rbx+0x56c],rax  ;Must wait until "not" finishes 

für Leistung wollen Sie dies tun:

00000000 48C7C0FFFFFFFF  mov rax,0xffffffff 
00000007 C78364050000FFFFFFFF mov dword [rbx+0x564],0xffffffff 
00000011 C78368050000FFFFFFFF mov dword [rbx+0x568],0xffffffff 
0000001B C7836C050000FFFFFFFF mov dword [rbx+0x56c],0xffffffff 
00000025 C78370050000FFFFFFFF mov dword [rbx+0x570],0xffffffff 

;Note: first MOV sets RAX to 0xFFFFFFFFFFFFFFFF due to sign extension 

Diese alle Anweisungen ermöglicht parallel ausgeführt werden, ohne Abhängigkeiten überall. Leider ist es auch viel größer (45 Bytes).

Wenn Sie versuchen, ein Gleichgewicht zwischen Code-Größe und Leistung zu erhalten; Dann können Sie hoffen, dass die erste Anweisung (die den Wert in RAX setzt) ​​abgeschlossen wird, bevor die letzte Anweisung den Wert in RAX kennen muss. Dies könnte in etwa so aussehen:

Dies sind 34 Bytes (die gleiche Größe wie der ursprüngliche Code). Dies ist wahrscheinlich ein guter Kompromiss zwischen Code-Größe und Leistung.

Jetzt; schauen sie sich den Original-Code und sieht, warum es schlecht:

mov dword [rbx+0x564],0xffffffff 
mov dword [rbx+0x568],0xffffffff 
mov rax,[rbx+0x564]    ;Massive problem 
mov [rbx+0x56C],rax    ;Depends on previous instruction 

Moderne CPUs etwas „store-Weiterleitung“ genannt hat, wo schreibt in einem Puffer gespeichert werden und die Zukunft liest den Wert aus diesem Puffer erhalten zu vermeiden den Wert aus dem Cache lesen.Ironischerweise funktioniert das nur, wenn die Größe des Lesevorgangs kleiner oder gleich der Größe des Schreibvorgangs ist. Die "Speicherweiterleitung" wird für diesen Code nicht funktionieren, da es 2 Schreibvorgänge gibt und der Lesevorgang größer als beide ist. Dies bedeutet, dass der dritte Befehl warten muss, bis die ersten 2 Befehle in den Cache geschrieben haben und dann den Wert aus dem Cache lesen müssen; das könnte leicht zu einer Strafe von etwa 30 Zyklen oder mehr führen. Dann muss der vierte Befehl auf den dritten Befehl warten (und kann nicht parallel mit irgendwas passieren), das ist ein anderes Problem.

+0

+1 für die Verwendung von Intel-Syntax. Schnelle Frage, der ursprüngliche Code hat 'mov [rbx + 0x56C], rax ', aber in Ihrem optimierten Beispiel' mov dword [rbx + 0x56C], rax'. Bedeutet das, dass das Original 8 Byte (QWORD) in '[rbx + 0x56c]' verschiebt, während sich Ihr 4 Byte (DWORD) bewegt? Ist das beabsichtigt? – greatwolf

Verwandte Themen