如果我想将2个无符号字节从内存移动到32位寄存器,我可以用MOV
指令而不使用模式切换吗?
我注意到您可以使用MOVSE
和MOVZE
指令来执行此操作。例如,对于MOVSE
,编码0F B7
将16位移动到32位寄存器。不过,这是一条3个周期的指令。
或者,我想我可以将4个字节移动到寄存器中,然后以某种方式CMP其中的两个字节。
在32位x86上检索和比较16位数据的最快策略是什么?请注意,我主要做32位操作,所以我不能切换到16位模式并停留在那里。
仅供初学者参考:这里的问题是32位Intel x86处理器可以根据其所处的模式MOV
8位数据和16位OR 32位数据。此模式称为"D位"设置。您可以使用特殊前缀0x66和0x67来使用非默认模式。例如,如果您处于32位模式,并且在指令前面加上0x66,这将导致操作数被视为16位。唯一的问题是这样做会对性能造成很大的影响。
使用movzx
在现代CPU上加载窄数据(或者movsx
,如果它的符号扩展而不是零扩展有用,但movzx
有时更快,永远不会更慢。)
movzx
只是在古老的P5(原始奔腾)微体系结构上运行缓慢,而不是本世纪制造的任何产品。基于最新微体系结构的奔腾品牌CPU,如奔腾G3258(Haswell,原奔腾20周年纪念版),是完全不同的野兽,其性能类似于等效的i3,但没有AVX、BMI1/2或超线程。
不要根据P5指南/数字调整现代代码。然而,Knight‘s Corner(Xeon Phi)基于改进的P54C微体系结构,因此它可能也有较慢的movzx
。Agner Fog和Instlatx64都没有KNC的每条指令吞吐量/延迟数。
使用16位操作数大小指令不会将整个管道切换到16位模式,也不会导致较大的性能命中。请参阅Agner Fog的microsch pdf,了解各种x86 CPU微体系结构(包括像Intel P5(最初的奔腾)这样古老的微体系结构,出于某种原因,您似乎在谈论它)上的速度到底是慢还是慢。
写入一个16位寄存器,然后读取整个32/64位寄存器在某些CPU上速度较慢(在英特尔P6系列上合并时部分寄存器暂停)。在其他情况下,写入16位寄存器会合并到旧值中,因此在写入时对完整寄存器的旧值存在错误的依赖性,即使您从未读取完整寄存器。看看哪个CPU做什么。(请注意,Haswell/Skylake只单独重命名AH,而Sandybridge(与Core2/Nehalem一样)也将AL/AX与RAX单独重命名,但合并时不会停滞。)
除非您特别关心顺序P5(或者可能是骑士角至强Phi,基于相同内核,但如果movzx
在那里也很慢,则为IDK),否则使用此:
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。例如,cmp 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]
(或rbx
),而不是[bx+1234]
。
在现代x86上:Intel P6/SnB家族/Atom/Silvermont,AMD至少从K7开始,即本世纪制造的任何东西,比实际的P5 Pentium更新,movzx
负载非常高效。
在许多CPU上,加载端口直接支持movzx
(有时也支持movsx
),因此它仅作为加载uop运行,而不是作为加载+ALU运行
Agner Fog指令集表中的数据:请注意,它们可能不会涵盖所有角落的情况,例如mov
-负载编号可能仅适用于32/64位负载。还请注意,Agner Fog的加载延迟数是,而不是从L1D缓存加载使用延迟;它们只是作为存储/重新加载(存储转发)延迟的一部分才有意义,但相对数字会告诉我们movzx
在mov
之上增加了多少个周期(通常没有额外的周期)。
(更新:https://uops.info/具有更好的测试结果,可以实际反映负载使用延迟,并且它们是自动化的,因此更新电子表格时的打字错误和笔误不会成为问题。但uops.info只能追溯到英特尔的Conroe(第一代酷睿2),AMD的Zen。)
P5 Pentium(按顺序执行):
movzx
-load是一条3周期指令(加上来自0F
前缀的解码瓶颈),而mov
-load是单周期吞吐量。(不过,它们仍然有延迟)。英特尔:
PPro/Pentium II/III:
movzx
/movsx
仅在一个加载端口上运行,吞吐量与普通mov
相同。Core2/Nehalem:相同,包括64位
movsxd
,但在Core 2上,movsxd r64, m32
负载的成本为负载+ALU uop,而不是微熔断。Sandybridge系列(SnB到Skylake及更高版本):
movzx
/movsx
负载是单个uop(只是一个负载端口),其性能与mov
负载相同。Pentium4(netburst):
movzx
仅在加载端口上运行,性能与mov
相同。movsx
是负载+ALU,并且需要额外的1个周期。Atom(按顺序):Agner的表不清楚需要ALU的内存源
movzx
/movsx
,但它们肯定很快。延迟数仅适用于reg,reg。Silvermont:与Atom相同:快速但不清楚是否需要端口。
KNL(基于Silvermont):Agner将带有内存源的
movzx
/movsx
列为使用IP0(ALU),但延迟与mov r,m
相同,因此没有惩罚。(执行单元压力不是问题,因为KNL的解码器几乎无法保持其2个ALU的馈送。)AMD:
山猫:
movzx
/movsx
负载为每个时钟1个,5个周期延迟。mov
负载为4c延时。Jaguar:
movzx
/movsx
负载为每个时钟1个,4个周期延迟。mov
负载为每个时钟1个,32/64位为3c延迟,mov r8/r16, m
为4c延迟(但仍然只是AGU端口,而不是像Haswell/Skylake那样的ALU合并)。K7/K8/K10:
movzx
/movsx
负载具有每时钟2个吞吐量,延迟比mov
负载高1个周期。它们使用AGU和ALU。推土机系列:与K10相同,但
movsx
-负载有5个周期延迟。movzx
负载具有4个周期延迟,mov
负载具有3个周期延迟。因此,理论上,如果来自16位mov
加载的错误依赖性不需要额外的ALU合并,或者为循环创建循环携带的依赖性,那么mov cx, word [mem]
和movsx eax, cx
(1个周期)的延迟可能更低。Ryzen:
movzx
/movsx
加载仅在加载端口中运行,延迟与mov
加载相同。VIA
通过Nano 2000/3000:
movzx
仅在加载端口上运行,延迟与mov
加载相同。movsx
是LD+ALU,具有1c的额外延迟。
当我说";执行相同";,我的意思是不计算任何部分寄存器惩罚或缓存线从更宽的负载拆分。例如,与Skylake上的mov ax, word [rsi]
相比,movzx eax, word [rsi]
避免了合并惩罚,但我仍然认为mov
的性能与movzx
相同。(我想我的意思是,没有任何缓存线拆分的mov eax, dword [rsi]
与movzx eax, word [rsi]
一样快。)
xor
-在写入16位寄存器之前将整个寄存器归零可避免以后在英特尔P6系列上出现部分寄存器合并停滞,并打破错误依赖关系。
如果你也想在P5上运行得很好,那么在除了PPro到PIII之外的任何现代CPU上,这可能会更好,但不会更糟,因为在PIII中,xor
-归零并不是破坏性的,尽管它仍然被认为是一种使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位存储的存储转发失败,则可以考虑使用32位加载。
实际上,我认为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)上定时了这个循环,以证明xor归零避免了16位寄存器的部分寄存器暂停以及低8(如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
指令/uops。
Core2上的情况非常不同,对xor
-归零进行了评论,并使用了mov 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
上插入合并uop。
Skylake以相同的方式运行两个循环,因为mov eax,imm32
仍然是依赖关系破坏。(与大多数具有只读目的地的指令一样,但要注意来自popcnt
和lzcnt
/tzcnt
的错误依赖关系)。
实际上,uops_executed.thread
perf计数器确实显示了一个区别:在SnB系列中,xor归零不需要执行单元,因为它是在发布/重命名阶段处理的。(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(融合域)/时钟=3.960(共4个)。最后一个.04可能部分是操作系统开销(中断等等),因为这只是在计算用户空间uop。
坚持32位模式并使用16位指令
mov eax, 0 ; clear the register
mov ax, 10-binary ; do 16 bit stuff
或者,我想我可以将4个字节移动到寄存器中,然后以某种方式CMP其中的两个
mov eax, xxxx ; 32 bit num loaded
mov ebx, xxxx
cmp ax, bx ; 16 bit cmp performed in 32 bit mode