在逐步执行一些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在优化模式下编译的。minSize
和szHint
变量是QSize
和{{1}}类型的正常变量1}}。)
答案 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根本不关心缓存行边界,并且也没有铿锵声。