与其他宽度不同,为什么短(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
.
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 个周期,应尽可能避免。
请参阅 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
.