c-与其他宽度不同,为什么短(16位)变量mov将值存储到寄存器中


int main()
{
00211000  push        ebp  
00211001  mov         ebp,esp  
00211003  sub         esp,10h  
char charVar1;
short shortVar1;
int intVar1;
long longVar1;

charVar1 = 11;
00211006  mov         byte ptr [charVar1],0Bh  
shortVar1 = 11;
0021100A  mov         eax,0Bh  
0021100F  mov         word ptr [shortVar1],ax  
intVar1 = 11;
00211013  mov         dword ptr [intVar1],0Bh 

longVar1 = 11;
0021101A  mov         dword ptr [longVar1],0Bh  
}

其他数据类型不通过寄存器,但只有短类型通过寄存器。怎么了?

GCC也做同样的事情,使用mov reg, imm32/mov m16, reg而不是mov mem, imm16

这是为了避免16位操作数大小为mov imm16的英特尔P6系列CPU上的LCP暂停

与没有前缀的相同机器代码字节相比,当前缀改变指令其余部分的长度时,就会发生LCP(长度改变前缀(暂停。

mov word ptr [ebp - 8], 11将涉及66前缀,该前缀使指令的其余部分为5字节(操作码+modrm+disp8+imm16(,而不是相同操作码/modrm的7字节(操作代码+modrm+disp8+imm32(。(

66 c7 45 f8 0b 00          mov     WORD PTR [ebp-0x8],0xb
c7 45 f8 0b 00 00 00    mov    DWORD PTR [ebp-0x8],0xb
^
opcode

这种长度变化混淆了在机器代码块被路由到实际解码器之前发生的指令长度查找阶段(预解码(。他们被迫备份并使用一种较慢的方法,该方法以查看操作码的方式考虑前缀。(x86机器代码的并行解码很难(。根据微体系结构和指令的对齐情况,此备份的代价可能高达11个周期,如果可能,应避免。

请参阅在简单的x86_64指令上,长度更改前缀(LCP(是否会导致暂停了解什么是长度变化前缀暂停的详细信息,以及在英特尔P6和SnB系列CPU中暂停预解码阶段几个周期的性能影响,以及Sandybridge系列(现代主流英特尔(特殊情况下的mov操作码,以避免LCP从16位立即暂停。


mov在现代英特尔上没有问题

Sandybridge家族专门为mov删除了LCP暂停(对于其他指令仍然存在(,所以这个调优决定只对Nehalem和更早的版本有帮助。

AFAIK,这不是Silvermont家族的事情,也不是任何AMD的事情,所以这可能是MSVC和GCC应该为他们的tune=generic更新的事情,因为P6家族CPU现在越来越不相关了。(如果GCC/MSVC的最新开发版本现在发生了变化,那么还需要一年左右的时间才能用新的编译器构建出许多软件发行版。(

clang没有进行这种优化,即使在旧的P6系列CPU上也不是灾难,因为大多数软件都不使用大量的short/int16_t变量。(瓶颈并不总是前端,通常是缓存未命中。(


示例

将该函数存储到堆栈中当然是由于没有实现优化。由于这些变量不是volatile,因此应该对它们进行完全优化,因为以后不会有任何东西读取它们。当你想做asm输出的例子时,不要写main,写一个必须有一些副作用的函数,例如通过指针存储,或者使用volatile

void foo(short *p){
volatile short x = 123;
*p = 123;
}

用MSVC 19.14-O2编译(https://godbolt.org/z/eWhzhEsEa):

x$ = 8
p$ = 8
foo     PROC                                          ; COMDAT
mov     eax, 123                      ; 0000007bH
mov     WORD PTR x$[rsp], ax
mov     WORD PTR [rcx], ax
ret     0
foo     ENDP

或者使用GCC11.2-O3,它甚至更糟,而不是CSE调用/重用寄存器常数

foo:
mov     eax, 123
mov     edx, 123
mov     WORD PTR [rsp-2], ax
mov     WORD PTR [rdi], dx
ret

但我们可以看到,这是英特尔的调整,因为-O3 -march=znver1(AMD Zen 1(:

foo:
mov     WORD PTR [rsp-2], 123
mov     WORD PTR [rdi], 123
ret

不幸的是,它仍然用-march=skylake来避免mov的LCP,所以它不知道完整的规则。

如果我们使用*p += 12345;(一个大到不能放在imm8中的数字,与mov不同,它允许加法(而不是仅使用=,具有讽刺意味的是,GCC会使用带有-march=skylake的长度可变前缀(MSVC也是如此(,从而创建一个stall:add WORD PTR [rdi], 12345

最新更新