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

Jan*_*nis 17 c++ assembly gcc compiler-optimization

在逐步执行一些Qt代码时,我遇到了以下内容.该函数QMainWindowLayout::invalidate()具有以下实现:

void QMainWindowLayout::invalidate()
{
QLayout::invalidate()
minSize = szHint = QSize();
}
Run Code Online (Sandbox Code Playgroud)

它被编译为:

<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
Run Code Online (Sandbox Code Playgroud)

从invalidate + 9到invalidate + 36的程序集似乎很愚蠢.首先,代码将-1写入%rbx + 0x564和%rbx + 0x568,但是然后它将-1从%rbx + 0x564加载回寄存器,只是将其写入%rbx + 0x56c.这似乎是编译器应该能够轻松优化到另一个立即行动的东西.

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

(注意:此代码来自ubuntu提供的正常发布库版本,因此它可能是由GCC在优化模式下编译的.minSize而且szHint变量是类型的正常变量QSize.)

Gui*_*ume 12

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

  • +1 for*"没有64位立即存储器mov指令,"*这就是所有需要说的. (2认同)
  • @GuillaumeMorin:对于`movq $ 0xffffffffffffffff,0x8(%rdi)`它是`48 c7 47 08 ff ff ff ff`和'movq $ 0xffffffffffffffff,(%rdi)`它是`48 c7 07 ff ff ff ff` .它看起来像是在带有符号扩展名等的64位内存中运行_move 32位字面值.聪明... (2认同)

Bre*_*dan 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
Run Code Online (Sandbox Code Playgroud)

对于表现来说,事情并非如此简单.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
Run Code Online (Sandbox Code Playgroud)

对于性能,您希望这样做:

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
Run Code Online (Sandbox Code Playgroud)

这允许所有指令并行执行,在任何地方都没有依赖性.可悲的是,它也更大(45字节).

如果您尝试在代码大小和性能之间取得平衡; 那么你可能希望第一条指令(在RAX中设置值)在最后一条指令需要知道RAX中的值之前完成.这可能是这样的:

mov rax,-1
mov dword [rbx+0x564],0xffffffff
mov dword [rbx+0x568],0xffffffff
mov dword [rbx+0x56c],rax
Run Code Online (Sandbox Code Playgroud)

这是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
Run Code Online (Sandbox Code Playgroud)

现代CPU确实有一种称为"存储转发"的东西,其中写入存储在缓冲区中,将来的读取可以从此缓冲区获取值以避免从缓存中读取值.具有讽刺意味的是,只有当读取的大小小于或等于写入的大小时,这才有效."存储转发"对此代码不起作用,因为有2次写入,并且读取大于它们.这意味着第三条指令必须等到前2条指令写入高速缓存然后必须从高速缓存中读取该值; 这可能很容易加起来约30个周期或更多的惩罚.然后第四条指令必须等待第三条指令(并且不能与任何东西并行发生),这是另一个问题.