17

いくつかのQtコードをステップ実行しているときに、次のことに遭遇しました。関数QMainWindowLayout::invalidate()には次の実装があります。

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

これは次のようにコンパイルされます。

<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

invalidate+9 から invalidate+36 へのアセンブリはばかげているようです。最初に、コードは %rbx+0x564 と %rbx+0x568 に -1 を書き込みますが、%rbx+0x564 からその -1 をレジスタにロードし直して、%rbx+0x56c に書き込みます。これは、コンパイラーがすぐに別の動きに簡単に最適化できるように思われます。

では、このばかげたコードは (もしそうなら、なぜコンパイラはそれを最適化しないのでしょうか?) それとも、これはどういうわけか非常に巧妙で、すぐに別の移動を使用するよりも高速なのでしょうか?

(注: このコードは、ubuntu によって出荷された通常のリリース ライブラリ ビルドからのものであるため、おそらく GCC によって最適化モードでコンパイルされたものです。minSizeおよびszHint変数は、タイプ の通常の変数ですQSize。)

4

4 に答える 4

12

ばかだと言っているとき、あなたが正しいかどうかはわかりません。ここでコンパイラがコードサイズを最適化しようとしているのかもしれません。メモリ mov 命令への 64 ビット イミディエイトはありません。したがって、コンパイラは、上記と同様に 2 つの mov 命令を生成する必要があります。それぞれが 10 バイトになり、生成される 2 つの移動は 14 バイトになります。書き込まれているため、メモリの待ち時間はほとんどないため、ここでパフォーマンスが低下することはないと思います。

于 2013-05-21T21:14:14.240 に答える
8

コードは「不完全」です。

コード サイズについては、これら 4 つの命令を合計すると最大 34 バイトになります。はるかに小さいシーケンス (19 バイト) が可能です。

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

パフォーマンスに関しては、それほど単純ではありません。CPU は同時に多くの命令を実行しようとしますが、上記のコードはそれを破っています。例えば:

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

パフォーマンスのために、これを行いたい:

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

これにより、すべての命令を並列に実行でき、依存関係はどこにもありません。残念なことに、サイズもかなり大きくなります (45 バイト)。

コードサイズとパフォーマンスのバランスを取ろうとする場合。次に、最後の命令が RAX の値を知る必要がある前に、最初の命令 (RAX の値を設定する) が完了することを期待できます。これは次のようになります。

mov rax,-1
mov dword [rbx+0x564],0xffffffff
mov dword [rbx+0x568],0xffffffff
mov dword [rbx+0x56c],rax

これは 34 バイトです (元のコードと同じサイズ)。これは、コード サイズとパフォーマンスの間の適切な妥協点となる可能性があります。

今; 元のコードを見て、なぜそれが悪いのか見てみましょう:

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

最新の CPU には「ストア フォワーディング」と呼ばれるものがあり、書き込みはバッファに保存され、将来の読み取りではこのバッファから値を取得して、キャッシュから値を読み取らないようにすることができます。皮肉なことに、これは読み取りのサイズが書き込みのサイズ以下の場合にのみ機能します。このコードでは、2 つの書き込みがあり、読み取りが両方よりも大きいため、「ストア フォワーディング」は機能しません。つまり、3 番目の命令は、最初の 2 つの命令がキャッシュに書き込まれるまで待機してから、キャッシュから値を読み取る必要があります。これは、約 30 サイクル以上のペナルティを簡単に追加できます。次に、4 番目の命令は 3 番目の命令を待たなければならない (また、何と並行して発生することもできない) ため、これは別の問題です。

于 2013-05-22T01:11:07.943 に答える
1

Guillaume の回答に加えて、64 ビットのロード/ストアが調整されていません。しかし、インテルの最適化ガイドによると(p 3-62)

位置合わせされていないデータ アクセスにより、パフォーマンスが大幅に低下する可能性があります。これは、特にキャッシュ ラインの分割に当てはまります。Pentium 4 およびその他の最近の Intel プロセッサ (Intel Core マイクロアーキテクチャに基づくプロセッサを含む) では、キャッシュ ラインのサイズは 64 バイトです。

64 バイト境界でアラインされていないデータへのアクセスは、2 つのメモリ アクセスにつながり、(1 つではなく) いくつかの μop を実行する必要があります。64 バイト境界にまたがるアクセスは、大きなパフォーマンスの低下を招く可能性が高く、各ストールのコストは、一般に、パイプラインが長いマシンでは大きくなります。

どちらのimoは、キャッシュラインの境界を越えないアライメントされていないロード/ストアが安価であることを意味します. この場合、デバッグしていたプロセスのベース ポインターは 0x10f9bb0 だったので、2 つの変数はキャッシュラインに 20 バイトと 28 バイトあります。

通常、Intel プロセッサはストアを使用して転送をロードするため、格納されたばかりの値のロードはキャッシュに触れる必要さえありません。しかし、同じガイドでは、いくつかの小さなストアの大きな負荷は、ストアロードフォワードではなく失速すると述べています: (p 3-66, p 3-68)

アセンブリ/コンパイラ コーディング ルール 49. (H 影響、M 一般性) ストアから転送されるロードのデータは、ストア データ内に完全に含まれている必要があります。

; 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

したがって、問題のコードはおそらくストールを引き起こすため、最適ではないと考えがちです。GCC がそのような制限を十分に考慮していなくても、私はあまり驚かないでしょう。GCCがストアからロードへの転送制限のモデリングを行うかどうか、またはどの程度モデリングするかを誰かが知っていますか?

編集: minSize/szHint フィールドの前にフィラー値を追加して実験すると、GCC はキャッシュ ラインの境界がどこにあるかをまったく気にせず、clang もしないことがわかります。

于 2013-05-21T23:13:36.727 に答える