1

If I want to move 2 unsigned bytes from memory into a 32-bit register, can I do that with a MOV instruction and no mode switch?

I notice that you CAN do that with the MOVSE and MOVZE instructions. For example, with MOVSE the encoding 0F B7 moves 16 bits to a 32 bit register. It is a 3 cycle instruction, though.

Alternatively I guess I could move 4 bytes into the register and then somehow CMP just two of them somehow.

What is the fastest strategy for retrieving and comparing 16-bit data on 32-bit x86? Note that I am mostly doing 32-bit operations so I can't switch to 16-bit mode and stay there.


FYI to the uninitiated: the issue here is that 32-bit Intel x86 processors can MOV 8-bit data and 16-bit OR 32-bit data depending on what mode they are in. This mode is called the "D-bit" setting. You can use special prefixes 0x66 and 0x67 to use a non-default mode. For example, if you are in 32-bit mode, and you prefix the instruction with 0x66 this will cause the operand to be treated as 16-bit. The only problem is that doing this causes a big performance hit.

4

2 回答 2

5

用于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(按顺序执行): - movzxload 是 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, cxmov

  • 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/ 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) 上对这个循环进行了计时,以证明异或归零可以避免 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它仍然会破坏依赖关系。(与大多数具有只写目标的指令一样,但要注意来自/的错误依赖关系popcntlzcnttzcnt)。

实际上,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 可能部分是操作系统开销(中断等),因为这仅计算用户空间微指令。

于 2017-11-27T17:28:41.343 回答
-1

坚持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
于 2013-04-30T01:11:33.977 回答