用于movzx
在现代 CPU 上加载窄数据。 (或者movsx
如果将其符号扩展而不是零扩展很有用,但movzx
有时更快,而且从不慢。)
movzx
只是在古老的P5(原始 Pentium)微架构上很慢,本世纪没有任何东西。Pentium 品牌的 CPU 基于最近的微架构,如 Pentium G3258(Haswell,原始 Pentium 的 20 周年纪念版)是完全不同的野兽,它们的性能与等效的 i3 相似,但没有 AVX、BMI1/2 或超线程。
不要根据 P5 指南/数字调整现代代码。然而,Knight's Corner (Xeon Phi) 是基于修改后的 P54C 微架构,所以它可能也很慢movzx
。Agner Fog 和Instlatx64都没有 KNC 的每条指令吞吐量/延迟数。
使用 16 位操作数大小的指令不会将整个流水线切换到 16 位模式或导致大的性能命中。请参阅Agner Fog 的 microarch pdf以准确了解各种 x86 CPU 微架构(包括您似乎出于某种原因在谈论的与 Intel P5(原始 Pentium)一样古老的微架构)上什么是慢的和不慢的。
在某些 CPU 上写入16 位寄存器然后读取完整的 32/64 位寄存器会很慢(在 Intel P6 系列上合并时部分寄存器停顿)。在其他情况下,写入 16 位寄存器会合并到旧值中,因此即使您从未读取完整寄存器,在写入时也会对完整寄存器的旧值产生错误依赖。看看哪个 CPU 做什么。(请注意,Haswell/Skylake 仅单独重命名 AH,不像 Sandybridge(如 Core2/Nehalem)也将 AL / AX 与 RAX 分开重命名,但合并时不会停止。)
除非您特别关心有序 P5(或者可能是 Knight's Corner Xeon Phi,基于相同的内核,但 IDK 如果movzx
在那里也很慢),请使用:
movzx eax, word [src1] ; as efficient as a 32-bit MOV load on most CPUs
cmp ax, word [src2]
cmp
在所有现代 CPU 上有效解码的操作数大小前缀。在写入完整寄存器后读取 16 位寄存器总是可以的,另一个操作数的 16 位加载也可以。
操作数大小前缀不会改变长度,因为没有 imm16 / imm32。egcmp word [src2], 0x7F
很好(它可以使用符号扩展的 imm8),但
cmp word [src2], 0x80
需要一个 imm16 并且会在某些 Intel CPU 上停止 LCP。(没有操作数大小前缀,相同的操作码将有一个 imm32,即指令的其余部分将是不同的长度)。相反,使用mov eax, 0x80
/ cmp word [src2], ax
。
地址大小前缀可以在 32 位模式下改变长度(disp32 与 disp16),但我们不想使用 16 位寻址模式来访问 16 位数据。我们仍在使用[ebx+1234]
(or rbx
),而不是[bx+1234]
.
在现代 x86 上:Intel P6 / SnB-family / Atom / Silvermont,至少从 K7 开始的 AMD,即本世纪制造的任何东西,比实际的 P5 Pentium 更新,movzx
负载非常有效。
在许多 CPU 上,加载端口直接支持movzx
(有时也支持movsx
),因此它仅作为加载 uop 运行,而不是作为加载 + ALU。
来自 Agner Fog 指令集表的数据:请注意,它们可能无法涵盖所有极端情况,例如mov
-load 数字可能仅适用于 32 / 64 位加载。另请注意,Agner Fog 的加载延迟数字不是来自 L1D 缓存的加载使用延迟;movzx
它们仅作为存储/重新加载(存储转发)延迟的一部分才有意义,但相对数字会告诉我们在其上增加了多少周期mov
(通常没有额外的周期)。
(更新:https ://uops.info/具有更好的测试结果,实际上反映了负载使用延迟,并且它们是自动化的,因此更新电子表格时的拼写错误和文书错误不是问题。但 uops.info 只会返回英特尔的 Conroe(第一代酷睿 2),AMD 的只有 Zen。)
P5 Pentium(按顺序执行): - movzx
load 是 3 周期指令(加上0F
前缀的解码瓶颈),而mov
-loads 是单周期吞吐量。(不过,它们仍然有延迟)。
英特尔:
PPro / Pentium II / III: movzx
/movsx
仅在加载端口上运行,吞吐量与普通mov
.
Core2 / Nehalem:相同,包括 64-bit movsxd
,除了在 Core 2 上的movsxd r64, m32
负载需要负载 + ALU uop,它们不会微熔。
Sandybridge 系列(SnB 通过 Skylake 及更高版本):movzx
/movsx
加载是单 uop(只是一个加载端口),并且执行与mov
加载相同。
Pentium4 (netburst):movzx
仅在加载端口上运行,性能与mov
. movsx
是负载+ ALU,并且需要1个额外的周期。
Atom(按顺序):Agner 的表不清楚内存源movzx
/movsx
需要 ALU,但它们肯定很快。延迟数仅适用于 reg,reg。
Silvermont:与 Atom 相同:速度快但不清楚是否需要端口。
KNL(基于 Silvermont):Agner 将movzx
/movsx
与内存源列为使用 IP0 (ALU),但延迟相同,mov r,m
因此没有惩罚。(执行单元压力不是问题,因为 KNL 的解码器无论如何都几乎无法保持其 2 个 ALU 的馈送。)
超微:
Bobcat:movzx
/movsx
负载是每个时钟1个,5个周期延迟。 mov
-load 是 4c 延迟。
Jaguar:movzx
/movsx
负载是每个时钟 1 个,4 个周期延迟。 mov
负载为每个时钟 1 个,32/64 位延迟 3c,或 4c mov r8/r16, m
(但仍然只有 AGU 端口,而不是像 Haswell/Skylake 那样的 ALU 合并)。
K7/K8/K10: movzx
/movsx
负载有 2-per-clock 吞吐量,延迟比mov
负载高 1 个周期。他们使用 AGU 和 ALU。
Bulldozer-family:与 K10 相同,但movsx
-load 有 5 个周期延迟。 movzx
-load 有 4 个周期延迟,mov
-load 有 3 个周期延迟。因此,理论上,如果来自 16 位加载的错误依赖不需要额外的 ALU 合并,或者为循环创建循环携带的依赖,那么它可能会降低到mov cx, word [mem]
然后(1 个周期)的延迟。movsx eax, cx
mov
Ryzen: movzx
/movsx
负载仅在负载端口中运行,延迟与mov
负载相同。
通过
通过 Nano 2000/3000:movzx
仅在加载端口上运行,延迟与mov
加载相同。 movsx
是 LD + ALU,有 1c 的额外延迟。
当我说“性能相同”时,我的意思是不计算任何部分寄存器惩罚或缓存行拆分来自更广泛的负载。例如 amovzx eax, word [rsi]
避免了与mov ax, word [rsi]
Skylake 的合并惩罚,但我仍然会说它的mov
性能与movzx
. (我想我的意思是,mov eax, dword [rsi]
没有任何缓存行拆分与 .)一样快movzx eax, word [rsi]
。)
xor
-在写入 16 位寄存器之前将完整寄存器归零可避免以后在英特尔 P6 系列上出现部分寄存器合并停顿,以及破坏错误的依赖关系。
如果您也想在 P5 上运行良好,那么这可能会更好一些,而在除 PPro 到 PIII 之外的任何现代 CPU 上都不会差很多,其中xor
-zeroing 不会破坏深度,即使它仍然被认为是归零 -使 EAX 等同于 AX 的成语(在写入 AL 或 AX 后读取 EAX 时不会出现部分寄存器停顿)。
;; Probably not a good idea, maybe not faster on anything.
;mov eax, 0 ; some code tuned for PIII used *both* this and xor-zeroing.
xor eax, eax ; *not* dep-breaking on early P6 (up to PIII)
mov ax, word [src1]
cmp ax, word [src2]
; safe to read EAX without partial-reg stalls
操作数大小前缀对于 P5 并不理想,因此如果您确定它没有故障、跨越缓存线边界或导致最近的存储转发失败,您可以考虑使用 32 位加载16 位存储。
实际上,我认为 Pentium 上的 16 位mov
加载可能比movzx
/ cmp
2 指令序列慢。对于像 32 位一样高效地处理 16 位数据,似乎真的不是一个好的选择!(当然,除了打包的 MMX 东西)。
有关 Pentium 的详细信息,请参阅 Agner Fog 的指南,但是操作数大小前缀在 P1(原始 P5)和 PMMX 上解码需要额外的 2 个周期,因此这个序列实际上可能比movzx
负载更糟糕。在 P1(但不是 PMMX)上,0F
转义字节(由 使用movzx
)也算作前缀,需要一个额外的周期来解码。
显然movzx
无论如何都不能配对。多周期movzx
会隐藏 的解码延迟cmp ax, [src2]
,因此movzx
/cmp
可能仍然是最佳选择。或安排说明,以便movzx
更早完成,并且cmp
可以与某些东西配对。无论如何,P1/PMMX 的调度规则相当复杂。
我在 Core2 (Conroe) 上对这个循环进行了计时,以证明异或归零可以避免 16 位寄存器和低 8 位寄存器(如 for setcc al
)的部分寄存器停顿:
mov ebp, 100000000
ALIGN 32
.loop:
%rep 4
xor eax, eax
; mov eax, 1234 ; just break dep on the old value, not a zeroing idiom
mov ax, cx ; write AX
mov edx, eax ; read EAX
%endrep
dec ebp ; Core2 can't fuse dec / jcc even in 32-bit mode
jg .loop ; but SnB does
perf stat -r4 ./testloop
在静态二进制文件中为此输出,该二进制文件在之后进行 sys_exit 系统调用:
;; Core2 (Conroe) with XOR eax, eax
469,277,071 cycles # 2.396 GHz
1,400,878,601 instructions # 2.98 insns per cycle
100,156,594 branches # 511.462 M/sec
9,624 branch-misses # 0.01% of all branches
0.196930345 seconds time elapsed ( +- 0.23% )
每个周期 2.98 条指令有意义:3 个 ALU 端口,所有指令都是 ALU,并且没有宏融合,因此每个是 1 uop。所以我们以前端容量的 3/4 运行。循环有3*4 + 2
指令/微指令。
Core2 上的情况非常不同,带有xor
-zeroing 注释并使用 themov eax, imm32
代替:
;; Core2 (Conroe) with MOV eax, 1234
1,553,478,677 cycles # 2.392 GHz
1,401,444,906 instructions # 0.90 insns per cycle
100,263,580 branches # 154.364 M/sec
15,769 branch-misses # 0.02% of all branches
0.653634874 seconds time elapsed ( +- 0.19% )
0.9 IPC(从 3 下降)与前端停止 2 到 3 个周期以在每个mov edx, eax
.
Skylake 以相同的方式运行两个循环,因为mov eax,imm32
它仍然会破坏依赖关系。(与大多数具有只写目标的指令一样,但要注意来自和/的错误依赖关系popcnt
lzcnt
tzcnt
)。
实际上,uops_executed.thread
性能计数器确实显示出不同:在 SnB 系列上,异或归零不需要执行单元,因为它是在问题/重命名阶段处理的。(mov edx,eax
在重命名时也被消除,因此 uop 计数实际上非常低)。无论哪种方式,循环计数都相同到不到 1%。
;;; Skylake (i7-6700k) with xor-zeroing
Performance counter stats for './testloop' (4 runs):
84.257964 task-clock (msec) # 0.998 CPUs utilized ( +- 0.21% )
0 context-switches # 0.006 K/sec ( +- 57.74% )
0 cpu-migrations # 0.000 K/sec
3 page-faults # 0.036 K/sec
328,337,097 cycles # 3.897 GHz ( +- 0.21% )
100,034,686 branches # 1187.243 M/sec ( +- 0.00% )
1,400,195,109 instructions # 4.26 insn per cycle ( +- 0.00% ) ## dec/jg fuses into 1 uop
1,300,325,848 uops_issued_any # 15432.676 M/sec ( +- 0.00% ) ### fused-domain
500,323,306 uops_executed_thread # 5937.994 M/sec ( +- 0.00% ) ### unfused-domain
0 lsd_uops # 0.000 K/sec
0.084390201 seconds time elapsed ( +- 0.22% )
lsd.uops 为零,因为循环缓冲区被微码更新禁用。前端的瓶颈:uops(融合域)/clock = 3.960(共 4 个)。最后一个 .04 可能部分是操作系统开销(中断等),因为这仅计算用户空间微指令。