使用MOV在32位x86中移动2个字节而不会导致模式切换或cpu停滞的任何方法



如果我想将2个无符号字节从内存移动到32位寄存器,我可以用MOV指令而不使用模式切换吗?

我注意到您可以使用MOVSEMOVZE指令来执行此操作。例如,对于MOVSE,编码0F B7将16位移动到32位寄存器。不过,这是一条3个周期的指令。

或者,我想我可以将4个字节移动到寄存器中,然后以某种方式CMP其中的两个字节。

在32位x86上检索和比较16位数据的最快策略是什么?请注意,我主要做32位操作,所以我不能切换到16位模式并停留在那里。


仅供初学者参考:这里的问题是32位Intel x86处理器可以根据其所处的模式MOV8位数据和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缓存加载使用延迟;它们只是作为存储/重新加载(存储转发)延迟的一部分才有意义,但相对数字会告诉我们movzxmov之上增加了多少个周期(通常没有额外的周期)。

(更新: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/cmp2指令序列慢。对于像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仍然是依赖关系破坏。(与大多数具有只读目的地的指令一样,但要注意来自popcntlzcnt/tzcnt的错误依赖关系)。

实际上,uops_executed.threadperf计数器确实显示了一个区别:在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

最新更新