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 的程序集似乎很愚蠢。首先,代码将 -1 写入 %rbx+0x564 和 %rbx+0x568,然后将 -1 从 %rbx+0x564 加载回寄存器,只是为了将其写入 %rbx+0x56c。这似乎是编译器应该能够轻松地优化为立即的另一个动作。

那么这个愚蠢的代码(如果是这样,为什么编译器不优化它?)或者这是否比立即使用另一个动作更聪明和更快?

(注:此代码来自 ubuntu 提供的正常发布库构建,因此推测它是由 GCC 在优化模式下编译的。minSizeszHint变量是类型的普通变量QSize。)

4

4 回答 4

12

当你说它很愚蠢时,不确定你是否正确。我认为编译器可能正在尝试优化这里的代码大小。没有 64 位立即到内存 mov 指令。所以编译器必须像上面那样生成 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 中的值)在最后一条指令/s 需要知道 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 次写入并且读取大于它们两者。这意味着第三条指令必须等到前两条指令写入缓存,然后必须从缓存中读取值;这很容易增加大约 30 个循环或更多的惩罚。然后第四条指令必须等待第三条指令(并且不能与任何东西并行发生),所以这是另一个问题。

于 2013-05-22T01:11:07.943 回答
2

我会分解成这样(认为有几个有相同的步骤)

这两行来自QSize() http://qt.gitorious.org/qt/qt/blobs/4.7/src/corelib/tools/qsize.h的内联定义, 它分别设置了每个字段。另外,我的猜测是 0x564(%rbx) 也是szHint同时设置的地址。

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

这些行最终minSize使用 64 位操作设置,因为编译器现在知道QSize对象的大小。而地址minSize是0x56c(%rbx)

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

笔记。第一部分是设置两个单独的字段,下一部分是复制一个QSize对象(无论内容)。那么问题是,编译器是否应该足够聪明以构建复合 64 位值,因为它刚刚看到了预设值?不确定那...

于 2013-05-21T21:34:45.290 回答
1

除了纪尧姆的回答,64 位加载/存储没有对齐。但根据英特尔优化指南(第 3-62 页)

未对齐的数据访问可能会导致严重的性能损失。对于高速缓存行拆分尤其如此。Pentium 4 和其他最新的 Intel 处理器(包括基于 Intel Core 微架构的处理器)中的高速缓存行大小为 64 字节。

访问在 64 字节边界上未对齐的数据会导致两次内存访问,并且需要执行多个微操作(而不是一个)。跨越 64 字节边界的访问可能会导致很大的性能损失,在具有较长管道的机器上,每个停顿的成本通常更大。

这 imo 意味着不跨越缓存线边界的未对齐加载/存储是便宜的。在这种情况下,我正在调试的进程中的基指针是 0x10f9bb0,因此这两个变量分别是缓存行中的 20 和 28 字节。

通常英特尔处理器使用存储来加载转发,因此刚刚存储的值的加载甚至不需要触及缓存。但同一指南还指出,几个较小存储的大量加载不会存储加载向前而是停止:(第 3-66 页,第 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 根本不关心缓存线边界的位置,也不在乎。

于 2013-05-21T23:13:36.727 回答