与其他宽度不同,为什么短(16 位)变量将值移动到寄存器并存储该值?

whi*_*ear 5 c x86 assembly cpu-architecture micro-optimization

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

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

Pet*_*des 12

GCC 做了同样的事情,使用mov reg, imm32/mov m16, reg代替mov mem, imm16.

这是为了避免 16 位操作数大小的 Intel P6 系列 CPU 上出现 LCP 停顿mov imm16

与没有前缀的相同机器代码字节相比,当前缀改变了指令其余部分的长度时,就会发生 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
Run Code Online (Sandbox Code Playgroud)

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

请参阅长度更改前缀 (LCP) 是否会导致简单 x86_64 指令停顿?有关什么是长度更改前缀停顿的详细信息,以及将 Intel P6 和 SnB 系列 CPU 中的预解码阶段停顿几个周期的性能影响,以及 Sandybridge 系列(现代主流 Intel)特殊情况mov避免 16 位立即数造成 LCP 停顿的操作码。


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

Sandybridge 系列mov专门删除了 LCP 停顿(对于其他指令仍然存在),因此此调整决策仅对 Nehalem 和更早的版本有帮助。

AFAIK,这不是 Silvermont 系列的问题,也不是任何 AMD 的问题,所以这可能是 MSVC 和 GCC 应该更新的内容,tune=generic因为现在 P6 系列 CPU 的相关性越来越低。(如果 GCC / MSVC 的最新开发版本现在发生变化,则需要大约一年左右的时间才能使用新编译器构建许多软件发行版/版本。)

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


例子

该函数完全存储到堆栈当然是由于未启用优化。由于这些变量不是volatile,因此应该将它们完全优化掉,因为以后没有任何东西会读取它们。当您想要制作asm输出的示例时,不要编写a main,而是编写必须有一些副作用的函数,例如通过指针存储,或者使用volatile.

void foo(short *p){
    volatile short x = 123;
    *p = 123;
}
Run Code Online (Sandbox Code Playgroud)

使用 MSVC 19.14 编译-O2https://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
Run Code Online (Sandbox Code Playgroud)

或者使用 GCC11.2 -O3,这更糟糕,而不是CSEing /重用寄存器常量

foo:
        mov     eax, 123
        mov     edx, 123
        mov     WORD PTR [rsp-2], ax
        mov     WORD PTR [rdi], dx
        ret
Run Code Online (Sandbox Code Playgroud)

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

foo:
        mov     WORD PTR [rsp-2], 123
        mov     WORD PTR [rdi], 123
        ret
Run Code Online (Sandbox Code Playgroud)

mov不幸的是,它仍然对with进行 LCP 避免-march=skylake,因此它不知道完整的规则。

如果我们使用*p += 12345;(一个足够大的数字,无法放入imm8,与 mov 不同,add 允许)而不是仅仅使用=,具有讽刺意味的是,GCC 然后使用长度变化前缀-march=skylake(如 MSVC 一样),创建一个停顿: add WORD PTR [rdi], 12345