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

Why does the short (16-bit) variable mov a value to a register and store that, unlike other widths?

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  
}

其他数据类型不走寄存器,只有short类型走寄存器。怎么了?

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)而不是 7 个(操作码 + modrm + disp8 + imm32)相同的操作码/modrm。)

 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 个周期,应尽可能避免。

请参阅 了解有关长度更改前缀停顿的详细信息,以及 停顿预解码阶段的性能影响在 Intel P6 和 SnB 系列 CPU 中几个周期,以及 Sandybridge 系列(现代主流英特尔)特例 mov 操作码以避免 16 位立即数的 LCP 停顿。


mov 在现代 Intel 上特别没有问题

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,这更糟糕,而不是 CSEing/重用寄存器常量

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

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

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