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

时间:2012-10-26 19:01:36

标签: performance assembly x86 intel

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

我注意到您可以使用MOVSEMOVZE说明执行此操作。例如,使用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位。唯一的问题是这样做会导致性能受到很大影响。

2 个答案:

答案 0 :(得分:3)

movzx在古代P5 (original Pentium)微架构上只是缓慢的,而不是本世纪所做的任何事情。基于最近微架构的奔腾品牌CPU,如奔腾G3258(Haswell,原版奔腾20周年纪念版)为totally different beasts,其性能与等效的i3类似,但没有AVX,BMI2或超线程。

不要根据P5指南/数字调整现代代码。但是,Knight's Corner(Xeon Phi)基于修改后的P54C微体系结构,所以它也可能慢movzx。 Agner Fog和Instlatx64都没有KNC的每指令吞吐量/延迟数。

使用16位操作数大小指令不会将整个流水线切换到16位模式或导致大的性能损失。请参阅Agner Fog's microarch pdf,了解各种x86 CPU微体系结构(包括与英特尔P5(原始Pentium)一样旧的,因为某些原因,您似乎正在讨论的内容)的确切速度和速度并不慢。

写入一个16位寄存器然后读取完整的32/64位寄存器在某些CPU上很慢(在Intel P6系列上合并时部分寄存器失速)。在其他情况下,写入16位寄存器会合并为旧值,因此即使您从未读取完整寄存器,也会在写入时对完整寄存器的旧值进行错误依赖。 (注意Haswell/Skylake only rename AH separately,与Sandybridge不同,它(如Core2 / Nehalem)也会将RA / AX与RAX分开重命名,但合并时不会停止。)

除非您特别关注有序P5(或者可能是Knight的角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。例如cmp word [src2], 0x7F很好(它可以使用符号扩展的imm8),但是 cmp word [src2], 0x80需要一个imm16并且会在某些Intel CPU上停止LCP。 (没有操作数大小前缀,相同的操作码将具有imm32,即指令的 rest 将是不同的长度)。相反,请使用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位加载。另请注意,来自L1D缓存的 Agner Fog的加载延迟数加载使用延迟;它们仅作为存储/重新加载(存储转发)延迟的一部分有意义,但相对数字将告诉我们在movzx之上添加了多少个周期mov(通常没有额外的周期)。

  • P5 Pentium(按顺序执行):movzx - 加载是一个3周期指令(加上来自0F前缀的解码瓶颈),而不是mov - 正在加载单循环吞吐量。 (但他们仍有延迟)。
  • 英特尔
  • PPro / Pentium II / III:movzx / movsx只运行一个加载端口,吞吐量与普通mov相同。
  • Core2 / Nehalem:同样,除了Nehalem上的movsxd r64, m显然需要一个ALU(并且没有微融合)。可能Core2也是这样,但是Agner没有测试那里。
  • Sandybridge-family(SnB到Skylake及更高版本):movzx / movsx加载是单uop(只是一个加载端口),并且与mov加载相同。
  • Pentium4(netburst):movzx仅在加载端口上运行,与mov相同。 movsx是加载+ ALU,需要1个额外的周期。
  • Atom(有序):对于需要ALU的内存来源movzx / movsx,Agner的表格不清楚,但它们肯定很快。延迟数仅适用于reg,reg。
  • Silvermont:和Atom一样:快速但不清楚是否需要端口。
  • KNL(基于Silvermont):Agner将movzx / movsx列为内存源,使用IP0(ALU),但延迟与mov r,m相同,因此& #39;没有惩罚。 (执行单元压力不是问题,因为KNL的解码器无论如何都几乎不能保持其2个ALU。)

  • <强> AMD

  • Bobcat: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。
  • Bulldozer-family:与K10相同,但movsx - load有5个周期延迟。 movzx - 加载有4个周期延迟,mov - 加载有3个周期延迟。因此理论上,如果来自16位mov cx, word [mem]负载的错误依赖性不需要额外的ALU,那么movsx eax, cx然后mov(1个周期)的延迟可能会更短。合并,或为循环创建循环携带依赖。
  • Ryzen:movzx / movsx加载仅在加载端口中运行,与mov相同的延迟加载。
  • VIA
  • Via Nano 2000/3000:movzx仅在加载端口上运行,与mov相同的延迟加载。 movsx是LD + ALU,额外延迟为1c。

当我说'#34;执行相同&#34;时,我的意思是不计算任何部分寄存器惩罚或缓存线分裂来自更宽的负载。例如movzx eax, word [rsi]避免了对Skylake的mov ax, word [rsi]合并惩罚,但我仍然会说movmovzx的行为相同。 (我想我的意思是mov eax, dword [rsi]没有任何缓存行拆分与movzx eax, word [rsi]一样快。)

在写入16位寄存器之前

xor-zeroing the full register 避免了以后在Intel P6系列上的部分寄存器合并停顿,以及打破错误的依赖性。

如果你想在P5上运行良好,那么在某些情况下可能会稍好一些,但在任何现代CPU上除了PPro到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位商店导致存储转发失败。

实际上,我认为Pentium上的16位mov负载可能比movzx / cmp 2指令序列慢。对于像32位数据那样高效地处理16位数据,似乎并不是一个好的选择! (当然,除了打包的MMX之外)。

有关Pentium详细信息,请参阅Agner Fog指南,但操作数大小前缀需要额外2个周期才能在P1(原始P5)和PMMX上进行解码,因此此序列实际上可能比{{1加载。在P1(但不是PMMX)上,movzx转义字节(由0F使用)也算作前缀,需要额外的周期来解码。

显然movzx无论如何都不可配对。多周期movzx将隐藏movzx的解码延迟,因此cmp ax, [src2] / movzx可能仍然是最佳选择。或者安排说明,以便cmp提前完成,movzx可以配对。无论如何,P1 / PMMX的调度规则非常复杂。

我在Core2(Conroe)上定时循环,以证明xor-zeroing避免了16位寄存器的部分寄存器停顿以及低8(如cmp):

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 输出为静态二进制文件,在以下情况下进行sys_exit系统调用:

perf stat -r4 ./testloop

每个循环2.98个指令是有意义的:3个ALU端口,所有指令都是ALU,并且没有宏融合,所以每个都是1个uop。所以我们以3/4的前端容量运行。循环有 ;; 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% ) 条指令/ uops。

Core2 上的内容非常不同,3*4 + 2 - 归零并使用xor代替

mov eax, imm32

0.9 IPC(从3开始)与每个 ;; 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% ) 插入合并uop的前端停顿2至3个周期一致。

Skylake以相同的方式运行两个循环,因为mov edx, eax仍然是依赖性破坏。 (与大多数只写目标的指令一样,但要注意false dependencies from popcnt and lzcnt/tzcnt)。

实际上,mov eax,imm32性能计数器确实显示出差异:在SnB族上,xor-zeroing不会占用执行单元,因为它在发布/重命名阶段处理。 (uops_executed.thread在重命名时也被删除,因此uop计数实际上非常低)。无论哪种方式,循环计数都小于1%。

mov    edx,eax

lsd.uops为零,因为微码更新禁用了循环缓冲区。前端的这个瓶颈:uops(融合域)/时钟= 3.960(满分为4)。最后.04可能是部分操作系统开销(中断等),因为这只是计算用户空间uops。

答案 1 :(得分:-2)

坚持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