为什么编译器会生成此程序集?

时间:2013-05-21 20:41:34

标签: c++ gcc assembly compiler-optimization

在逐步执行一些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和{{1}}类型的正常变量1}}。)

4 个答案:

答案 0 :(得分:13)

当你说这是愚蠢的时候,不确定你是否正确。我认为编译器可能会尝试在此处优化代码大小。没有64位立即到内存的mov指令。所以编译器必须像上面那样生成2个mov指令。它们中的每一个都是10个字节,生成的2个移动是14个字节。它已被写入,所以很可能没有内存延迟,所以我认为你不会在这里受到任何性能影响。

答案 1 :(得分: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次写入,并且读取大于它们。这意味着第三条指令必须等到前2条指令写入高速缓存然后必须从高速缓存中读取该值;这可能很容易加起来约30个周期或更多的惩罚。然后第四条指令必须等待第三条指令(并且不能与任何东西并行发生),这是另一个问题。

答案 2 :(得分:1)

我打破这一行(想想几个人有相同的评论步骤)

这两行来自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)

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

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

请注意。第一部分是设置两个单独的字段,下一部分是复制QSize对象(无论内容如何)。那么问题是,如果编译器足够智能以构建复合64位值,因为它之前看到了预设值?不确定...

答案 3 :(得分:0)

除Guillaume的答案外,64位加载/存储未对齐。但根据Intel optimization guide(第3-62页)

  

未对齐的数据访问可能会导致严重的性能损失。   对于缓存行拆分尤其如此。缓存的大小   奔腾4和其他最近的英特尔处理器中的行是64字节,   包括基于英特尔酷睿微体系结构的处理器。

     

访问64字节边界上未对齐的数据会导致两个内存   访问并需要执行几个μop(而不是一个)。   跨越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的存储到转发转发限制是否/多少建模?

编辑:一些尝试在minSize / szHint字段之前添加填充值显示GCC根本不关心缓存行边界,并且也没有铿锵声。