【问题标题】:Why are mov ah,bh and mov al, bl together much faster than single instruction mov ax, bx?为什么 mov ah,bh 和 mov al, bl 一起比单指令 mov ax, bx 快得多?
【发布时间】:2011-10-25 06:58:50
【问题描述】:

我发现了

mov al, bl
mov ah, bh

快很多
mov ax, bx

谁能解释一下为什么? 我在 Core 2 Duo 3 Ghz 上运行,在 Windows XP 下以 32 位模式运行。 使用 NASM 编译,然后与 VS2010 链接。 nasm编译命令:

nasm -f coff -o triangle.o triangle.asm

这是我用来渲染三角形的主循环:

; some variables on stack
%define cr  DWORD [ebp-20]
%define dcr DWORD [ebp-24]
%define dcg DWORD [ebp-32]
%define dcb DWORD [ebp-40]

loop:

add esi, dcg
mov eax, esi
shr eax, 8

add edi, dcb
mov ebx, edi
shr ebx, 16
mov bh, ah

mov eax, cr
add eax, dcr
mov cr, eax

mov ah, bh  ; faster
mov al, bl
;mov ax, bx

mov DWORD [edx], eax

add edx, 4

dec ecx
jge loop

我可以为整个 VS 项目提供测试源。

【问题讨论】:

  • 这当然没有任何任何意义。您有两条指令要解码和执行,而不是一条。这会受到其他因素的影响,比如循环的大小吗? “快得多”是多少?它的重现性如何?
  • 我在这里猜测,但可能是因为已经使用了 8 位部分寄存器mov bh, ah?我认为仅使用 32 位寄存器的版本可能会比所提供的任何一个版本都快。
  • @NathanFellman,在 32 位代码中使用 16 位操作数会强制进行上下文切换,它是减速的根源,而不是单条指令。
  • @NathanFellman,你在谈论一个与我非常不同的上下文切换。我不知道操作系统在何处以及如何进入此模式、保护模式或虚拟 x86 模式。这只是 CPU 如何处理寄存器重命名和流水线的简单上下文切换(所有这些都因 CPU 的版本而异)。
  • @Johan:这不是上下文切换。因此,您最多会看到指令执行出现意外停顿,无论哪种方式,OP 都在询问相反的情况:部分寄存器访问比完全访问快。

标签: x86 assembly


【解决方案1】:

为什么慢
使用 16 位寄存器比使用 8 位寄存器更昂贵的原因是 16 位寄存器指令以微码解码。这意味着解码过程中有一个额外的周期,并且在解码时无法配对。
另外,由于 ax 是一个部分寄存器,它需要一个额外的周期来执行,因为寄存器的顶部需要与对下部的写入结合起来。
8 位写入有专门的硬件来加快速度,但 16 位写入没有。同样,在许多处理器上,16 位指令需要 2 个周期而不是 1 个周期,并且它们不允许配对。

这意味着您现在只能执行 1 条指令,而不是能够在 4 个周期内处理 12 条指令(每个周期 3 条),因为在将指令解码为微码时会出现停顿,而在处理微码时会出现停顿。

我怎样才能让它更快?

mov al, bl
mov ah, bh

(此代码至少需要 2 个 CPU 周期,并且可能会在第二条指令上出现停顿,因为在某些(较旧的)x86 CPU 上,您会锁定 EAX)
以下是发生的事情:

  • EAX 被读取。 (周期 1)
    • EAX的低字节改变了(还是循环1)
    • 并将完整值写回 EAX。 (周期 1)
  • EAX 被锁定以进行写入,直到第一次写入完全解决。 (可能等待多个周期)
  • 对 EAX 中的高字节重复该过程。 (周期 2)

在最新的 Core2 CPU 上,这不是什么大问题,因为已经安装了额外的硬件,知道blbh 真的不会互相妨碍。

mov eax, ebx

一次移动 4 个字节,单条指令将在 1 个 cpu 周期内运行(并且可以与其他指令并行配对)。

  • 如果您想要快速编码,请始终使用 32 位 (EAX、EBX 等) 寄存器。
  • 尽量避免使用 8 位子寄存器,除非必须这样做。
  • 切勿使用 16 位寄存器。即使您必须在 32 位模式下使用 5 条指令,那仍然会更快。
  • 使用 movzx reg, ...(或 movsx reg, ...)指令

加速代码
我看到了一些加快代码速度的机会。

; some variables on stack
%define cr  DWORD [ebp-20]
%define dcr DWORD [ebp-24]
%define dcg DWORD [ebp-32]
%define dcb DWORD [ebp-40]

mov edx,cr

loop:

add esi, dcg
mov eax, esi
shr eax, 8

add edi, dcb
mov ebx, edi
shr ebx, 16   ;higher 16 bits in ebx will be empty.
mov bh, ah

;mov eax, cr   
;add eax, dcr
;mov cr, eax

add edx,dcr
mov eax,edx

and eax,0xFFFF0000  ; clear lower 16 bits in EAX
or eax,ebx          ; merge the two. 
;mov ah, bh  ; faster
;mov al, bl


mov DWORD [epb+offset+ecx*4], eax ; requires storing the data in reverse order. 
;add edx, 4

sub ecx,1  ;dec ecx does not change the carry flag, which can cause
           ;a false dependency on previous instructions which do change CF    
jge loop

【讨论】:

  • 段寄存器的使用与寻址是16位还是32位无关。它仅取决于代码是在保护模式还是实模式下执行。在内存访问中总是有一个段描述符,它与数据(或就此而言,地址)大小无关。使用 32 位寻址,段的大小可以达到 4 GB,但它们仍然存在。
  • 寻址模式控制包含地址的指令如何计算段内的偏移量。这个偏移量的使用方式完全独立于地址大小;它将与段限制进行比较,并以完全相同的方式添加到基地址。并且不涉及上下文切换。
  • 段寄存器在 32 位和 16 位保护模式下完全相同。它们在 real mode 中的工作方式不同,但这既不存在也不存在。使用 16 位数据不会强制切换到实模式(也不会强制切换到虚拟 8086 模式)。
  • 约翰是对的,这就是问题所在。切勿使用 16 位操作,除非您完全切换到该模式并在该模式下停留很长时间,然后再次切换。在代码中添加随机 16 位操作会破坏性能。
  • 我认为地址大小和操作数大小有单独的前缀,因此可以使用mov ax,[esi+ebx*4]mov eax,[bx+23]。我可以理解现代处理器不会针对后者进行优化,但前者不应该需要超出操作数大小前缀的任何上下文切换。虽然由于mov bx,ax 覆盖了部分EBX 而不是全部,寄存器调度可能会变得复杂,但同样适用于mov bh,ahmov bl,al
【解决方案2】:

总结:16位指令直接不是问题。 问题是在写入部分寄存器后读取更宽的寄存器,导致 Core2 上的部分寄存器停顿。这在 Sandybridge 和后来的问题上要少得多,因为它们合并的成本要低得多。 mov ax, bx 导致额外的合并,但即使是 OP 的“快速”版本也有一些停顿。

请参阅此答案的末尾以获取另一个标量内循环,它应该比其他两个答案更快,使用 shld 在寄存器之间随机播放字节。在循环外预移位 8b 的内容会将我们想要的字节放在每个寄存器的顶部,这使得这非常便宜。它应该在 32 位核心 2 上以每 4 个时钟周期进行一次迭代的速度运行略好,并且使所有三个执行端口饱和而没有停顿。它应该在 Haswell 上以每 2.5c 一次迭代运行。

要真正快速地做到这一点,请查看 auto-vectorized compiler output,可能会削减它或使用向量内在函数重新实现。


与 16 位操作数大小的指令速度慢的说法相反,Core2 在理论上可以维持每个时钟交替 mov ax, bxmov ecx, edx 的 3 个 insn。没有任何类型的“模式开关”。 (正如大家所指出的,“上下文切换”是一个糟糕的选择名称,因为它已经具有特定的技术含义。)

问题是当您读取之前只写了一部分的 reg 时,部分寄存器会停止。 Intel P6 系列 CPU 不会强制写入 ax 以等待 eax 的旧内容准备好(错误依赖),而是单独跟踪部分 reg 的依赖关系。读取更广泛的 reg 会强制合并,根据Agner Fog 会停滞 2 到 3 个周期。使用 16 位操作数大小的另一个大问题是立即数操作数,您可以在英特尔 CPU 上的解码器中遇到不适合 imm8 的立即数的 LCP 停顿。

SnB 系列效率更高,只需插入一个额外的 uop 即可进行合并,而不会在合并时停止。 AMD 和 Intel Silvermont(和 P4)根本不会单独重命名部分寄存器,因此它们确实对以前的内容具有“错误”依赖关系。在这种情况下,我们稍后会读取完整的寄存器,所以这是一个真正的依赖关系,因为我们想要合并,所以这些 CPU 具有优势。 (英特尔 Haswell/Skylake(可能还有 IvB)不会将 AL 与 RAX 分开重命名;它们只会分别重命名 AH/BH/CH/DH。读取 high8 寄存器有额外的延迟。 见this Q&A about partial registers on HSW/SKL for the details。)


partial-reg 停顿都不是长依赖链的一部分,因为合并的 reg 在下一次迭代中被覆盖。显然 Core2 只是停止了前端,甚至是整个乱序执行核心?我的意思是问一个问题,关于 Core2 上的部分寄存器减速有多昂贵,以及如何衡量 SnB 的成本。 @user786653 的 oprofile 答案对此有所启发。 (并且还有一些非常有用的 C 从 OP 的 asm 逆向工程,以帮助弄清楚这个函数真正想要完成的事情)。

用现代 gcc 编译 C 可以生成向量化的 asm,在 xmm 寄存器中一次循环 4 个双字。不过,当它可以使用 SSE4.1 时,它会做得更好。 (并且 clang 根本不会使用 -march=core2 对其进行自动矢量化,但它确实展开了很多,可能会交错多次迭代以避免部分注册的东西。)如果你不告诉 gcc dest 是对齐的,它会在矢量化循环周围生成大量标量序言/尾声,以达到对齐的点。

它将整数 args 转换为向量常量(在堆栈上,因为 32 位代码只有 8 个向量寄存器)。 The inner loop is

.L4:
        movdqa  xmm0, XMMWORD PTR [esp+64]
        mov     ecx, edx
        add     edx, 1
        sal     ecx, 4
        paddd   xmm0, xmm3
        paddd   xmm3, XMMWORD PTR [esp+16]
        psrld   xmm0, 8
        movdqa  xmm1, xmm0
        movdqa  xmm0, XMMWORD PTR [esp+80]
        pand    xmm1, xmm7
        paddd   xmm0, xmm2
        paddd   xmm2, XMMWORD PTR [esp+32]
        psrld   xmm0, 16
        pand    xmm0, xmm6
        por     xmm0, xmm1
        movdqa  xmm1, XMMWORD PTR [esp+48]
        paddd   xmm1, xmm4
        paddd   xmm4, XMMWORD PTR [esp]
        pand    xmm1, xmm5
        por     xmm0, xmm1
        movaps  XMMWORD PTR [eax+ecx], xmm0
        cmp     ebp, edx
        ja      .L4

请注意,整个循环中只有一家商店。所有负载只是它之前计算的向量,作为局部变量存储在堆栈中。


有几种方法可以加快 OP 的代码速度。最明显的是我们不需要创建堆栈帧,从而释放ebp。它最明显的用途是保存cr,OP 会将其溢出到堆栈中。 user786653 的triAsm4 这样做了,除了他使用了疯狂的巨魔逻辑变体:他制作了一个堆栈帧并像往常一样设置ebp,但随后将esp 存储在一个静态位置并将其用作暂存寄存器! !如果您的程序有任何信号处理程序,这显然会严重破坏,但除此之外很好(除了使调试更加困难)。

如果您想使用esp 作为草稿,请将函数 args 也复制到静态位置,这样您就不需要寄存器来保存任何指向堆栈内存的指针。 (将旧的esp 保存在 MMX 寄存器中也是一种选择,因此您可以在多个线程同时使用的可重入函数中执行此操作。但如果您将 args 复制到某个静态的地方,除非它是线程本地存储带有段覆盖或其他东西。您不必担心从同一个线程内重新进入,因为堆栈指针处于不可用状态。任何可以在同一个线程中重新进入您的函数的信号处理程序反而会崩溃。>.

溢出cr 实际上不是最佳选择:我们可以只在寄存器中保留一个 dst 指针,而不是使用两个寄存器进行循环(计数器和指针)。通过计算结束指针(结束后的指针:dst+4*cnt)来执行循环边界,并使用带有内存操作数的cmp 作为循环条件。

在 Core2 上与 cmp/jb 进行比较实际上比 dec / jge 更优化。无符号条件可以与cmp 进行宏融合。在 SnB 之前,只有 cmptest 可以进行宏熔断。 (对于 AMD Bulldozer 也是如此,但 cmp 和 test 可以与 AMD 上的任何 jcc 融合)。 SnB 系列 CPU 可以宏熔断 dec/jge。有趣的是,Core2 只能将带符号的比较(如jge)与test 进行宏融合,而不是cmp。 (无论如何,无符号比较是地址的正确选择,因为0x8000000 并不特殊,但0 是。我没有将jb 用作有风险的优化。)


我们不能将cbdcb 预移到低字节,因为它们需要在内部保持更高的精度。但是,我们可以 left 移动另外两个,因此它们位于寄存器的左边缘。将它们右移到它们的目标位置不会留下任何可能溢出的垃圾高位。

我们可以做重叠存储,而不是合并到eax。从eax 存储4B,然后从bx 存储低2B。这将保存 eax 中的部分注册失速,但会生成一个用于将 bh 合并到 ebx 中,因此价值有限。可能一个 4B 写入和两个重叠的 1B 存储在这里实际上很好,但那开始是很多存储。不过,它可能会分布在足够多的其他指令上,不会成为存储端口的瓶颈。

user786653 的 triAsm3 使用掩码和 or 指令进行合并,这对于 Core2 来说似乎是一种明智的方法。对于 AMD、Silvermont 或 P4,使用 8b 和 16b mov 指令来合并部分寄存器可能实际上是好的。如果你只写 low8 或 low16 来避免合并惩罚,你也可以在 Ivybridge/Haswell/Skylake 上利用它。但是,我对此进行了多项改进,以减少掩蔽。

;使用定义你可以把 [] 放在周围,所以很明显它们是内存引用 ; % 定义 cr ebp+0x10 %define cr esp+取决于我们推了多少的东西 % 定义 dcr ebp+0x1c ;;也将这些更改为从 ebp 工作。 %define dcg ebp+0x20 %define dcb ebp+0x24 ; esp-relative 偏移量可能是错误的,只是在没有测试的情况下很快就在我的脑海中做了: ;我们在 ebp 之后再推送 3 个 reg,这是 ebp 在堆栈帧版本中快照 esp 的点。所以加 0xc(即在心里加 0x10 并减去 4) ;无论如何,32位代码是愚蠢的。 64 位在 regs 中传递 args。 %define dest_arg esp+14 % 定义 cnt_arg esp+18 ...其他一切 tri_pjc: 推送ebp 推送编辑 推esi 推 ebx ;只有这 4 个需要保留在正常的 32 位调用约定中 mov ebp, [cr] mov esi, [cg] mov edi, [cb] shl esi, 8 ;将我们想要的位放在高边沿,因此我们不必在移入零后进行屏蔽 shl [dcg], 8 shl edi, 8 shl [dcb], 8 ;显然,原始代码并不关心 cr 是否溢出到最高字节。 mov edx, [dest_arg] mov ecx, [cnt_arg] lea ecx, [edx + ecx*4] ;一过去,用作循环边界 mov [dest_arg], ecx ;将它溢出回堆栈,我们只需要读取它。 对齐 16 。环形: ;见下文,这个内部循环可以更加优化 添加 esi,[dcg] mov eax, esi shr eax, 24 ; eax 字节 = { 0 0 0 cg } 添加 edi,[dcb] shld eax, edi, 8 ; eax 字节 = { 0 0 cg cb } 添加 ebp, [dcr] mov ecx, ebp 和 ecx, 0xffff0000 或 eax, ecx ; eax bytes = { x cr cg cb} 其中 x 是从 cr 溢出的。通过将掩码更改为 0x00ff0000 来杀死它 ;另一个要合并的 shld 可能在其他 CPU 上更快,但不是 core2 ;与 mov cx 合并,ax 也可以在价格便宜的 CPU 上(AMD 和 Intel IvB 及更高版本) mov DWORD [edx], eax ;或者: ; mov DWORD [edx], ebp ; mov WORD [edx], eax ;这个 insn 替换了 mov/and/or 合并 添加 edx, 4 cmp edx, [dest_arg] ; core2 可以宏融合 cmp/unsigned 条件,但没有签名 jb .循环 流行音乐 流行音乐 流行音乐 流行音乐 ret

在完成省略帧指针并将循环边界放入内存之后,我得到的寄存器比我需要的多一个。您可以在寄存器中缓存一些额外的东西,或者避免保存/恢复寄存器。也许将循环边界保持在ebx 是最好的选择。它基本上节省了一个序言指令。将dcbdcg 保存在寄存器中需要在序言中额外添加一个insn 才能加载它。 (即使在 Skylake 上,具有内存目标的移位也很丑陋且缓慢,但代码量很小。它们不在循环中,并且 core2 没有 uop 缓存。单独加载/移位/存储仍然是 3 uop,因此,除非您将其保存在 reg 中而不是存储中,否则您无法击败它。)

shld 是 P6 (Core2) 上的 2-uop insn。幸运的是,对循环进行排序很容易,因此它是第五条指令,前面是四个单指令。它应该作为第二组 4 个中的第一个 uop 击中解码器,因此它不会导致前端延迟。 (Core2 can decode 1-1-1-1、2-1-1-1、3-1-1-1 或 4-1-1-1 uops-per-insn 模式。SnB 和后来重新设计了解码器,并添加了一个 uop 缓存,这使得解码通常不是瓶颈,并且只能处理 1-1-1-1、2-1-1、3-1 和 4 组。)

shldhorrible on AMD K8, K10, Bulldozer-family, and Jaguar。 6 m-ops、3c 延迟和每 3c 吞吐量一个。它在 32 位操作数大小的 Atom/Silvermont 上很棒,但在 16 或 64b 寄存器时就很糟糕了。

这个 insn 排序可能会使用 cmp 作为组的最后一个 insn 进行解码,然后将 jb 单独解码,使其不是宏熔断器。如果前端效果是此循环的一个因素,这可能会为合并的重叠存储方法带来额外的优势,而不仅仅是节省 uop。 (而且我怀疑它们会是,考虑到高度的并行性并且循环承载的 dep 链很短,因此可以同时进行多次迭代。)

所以:每次迭代的融合域微指令:Core2 上 13 个(假设宏融合实际上可能不会发生),SnB 系列上 12 个。所以 IvB 应该在每 3c 一次迭代中运行它(假设 3 个 ALU 端口都不是瓶颈。mov r,r 不需要 ALU 端口,存储也不需要。add 和布尔值可以使用任何端口。@ 987654381@ 和 shld 是唯一不能在广泛选择的端口上运行的,并且每三个周期只有两个班次。)即使它设法避免任何前端瓶颈,Core2 每次迭代也需要 4c 来发布它,甚至更长的运行时间。

我们在 Core2 上的运行速度可能仍然足够快,如果我们仍然这样做,那么每次迭代将cr 溢出/重新加载到堆栈将成为瓶颈。它将内存往返 (5c) 添加到循环携带的依赖链中,使总 dep 链长度为 6 个循环(包括添加)。


嗯,实际上即使 Core2 也可能会从使用两个 shld insns 合并中获胜。它还保存了另一个寄存器!

对齐 16 ;mov ebx, 111 ; IACA 开始 ;db 0x64, 0x67, 0x90 。环形: 添加 ebp, [dcr] mov eax, ebp shr eax, 16 ; eax bytes = { 0 0 x cr} 其中 x 是从 cr 溢出的。像其他人一样杀死那个预移动 cr 和 dcr,并在此处使用 shr 24 添加 esi,[dcg] shld eax, esi, 8 ; eax 字节 = { 0 x cr cg} 添加 edx, 4 ;这介于 `shld` 之间,以帮助提高前 SnB 上的解码器吞吐量,并且不破坏宏融合。 添加 edi,[dcb] shld eax, edi, 8 ; eax 字节 = { x cr cg cb} mov DWORD [edx-4], eax cmp edx, ebx ;在这里使用我们的备用寄存器 jb .循环; core2 可以宏融合 cmp/unsigned 条件,但没有签名。宏融合仅在 Core2 上以 32 位模式工作。 ;mov ebx, 222 ; IACA 结束 ;db 0x64, 0x67, 0x90

每次迭代:SnB:10 个融合域微指令。 Core2:12 个融合域微指令,所以这个 比 Intel CPU 上的先前版本短(但在 AMD 上很糟糕)。使用shld 可以节省mov 指令,因为我们可以使用它来非破坏性地提取源的高字节。

Core2 可以每 3 个时钟迭代一次循环。 (这是 Intel 第一个具有 4 uop 宽流水线的 CPU)。

来自Agner Fog's tableMerom/Conroe (first gen Core2)(请注意,David Kanter 的框图将 p2 和 p5 颠倒了):

  • shr: 在 p0/p5 上运行
  • shld:p0/p1/p5 2 微指令? Agner 的 pre-Haswell 表没有说明哪些 uops 可以去哪里。
  • mov r,raddand:p0/p1/p5
  • 融合 cmp-and-branch: p5
  • 存储:p3 和 p4(这些微融合到 1 个融合域存储 uop)
  • 每个负载:p2。 (所有负载都与融合域中的 ALU 操作进行微融合)。

根据 IACA,它有一个 Nehalem 模式,但没有 Core2,大部分shld uops 都转到 p1,在其他端口上运行的每个 insn 平均只有不到 0.6 个。 Nehalem 具有与 Core2 基本相同的执行单元。此处涉及的所有指令在 NHM 和 Core2 上具有相同的 uop 成本和端口要求。 IACA 的分析对我来说看起来不错,我不想自己检查所有内容来回答一个 5 年前的问题。不过,回答很有趣。 :)

无论如何,根据 IACA 的说法,微指令应该在端口之间很好地分布。它认为 Nehalem 可以每 3.7 个周期运行一次循环,使所有三个执行端口饱和。它的分析对我来说看起来不错。 (请注意,我必须从cmp 中删除内存操作数,以使 IACA 不会给出愚蠢的结果。)这显然是需要的,因为 pre-SnB 每个周期只能执行一次加载:我们会在端口 2 上遇到四次加载的瓶颈在循环中。

IACA 不同意 Agner Fog 对 IvB 和 SnB 的测试(根据我对 SnB 的测试,它认为 shld 仍然是 2 uop,而实际上它是 1)。所以它的数字很傻。

IACA 看起来对 Haswell 来说是正确的,它说瓶颈是前端。它认为 HSW 可以以每 2.5c 一个的速度运行它。 (Haswell 中的循环缓冲区至少可以在每次迭代中发出非整数循环数的循环。Sandybridge may be limited to whole numbers of cycles, where the taken loop-branch ends an issue-group。)

我还发现我需要使用iaca.sh -no_interiteration,否则它会认为存在迭代循环携带的依赖项,并认为循环在 NHM 上会占用 12c。

【讨论】:

  • 很好的分析!是的,使用esp 而不是ebp 作为额外的寄存器是非常规的,但是这样我就不需要重新定义宏。 :)
  • @user786653:这对于快速和肮脏的测试来说很重要。很公平:P
【解决方案3】:

在我的 Core 2 Duo CPU L9300 1.60GHz 上也更快。正如我在评论中所写,我认为这与部分寄存器的使用有关(ahalax)。查看更多例如hereherehere(第 88 页)。

我编写了一个小型测试套件来尝试改进代码,虽然不使用 OP 中提供的 ax 版本是最聪明的,但尝试消除部分寄存器使用确实会提高速度(甚至更多所以比我快速尝试释放另一个寄存器)。

要获得有关为什么一个版本比另一个版本更快的更多信息,我认为需要更仔细地阅读源材料和/或使用英特尔 VTune 或 AMD CodeAnalyst 之类的东西。 (事实证明我错了)

更新,虽然 oprofile 的以下输出并不能证明任何事情,但它确实表明在两个版本中都发生了很多部分寄存器停顿,但在最慢版本 (triAsm2) 中大约是“快速”版本中的两倍' 版本 (triAsm1)。

$ opreport -l test                            
CPU: Core 2, speed 1600 MHz (estimated)
Counted CPU_CLK_UNHALTED events (Clock cycles when not halted) with a unit mask of 0x00 (Unhalted core cycles) count 800500
Counted RAT_STALLS events (Partial register stall cycles) with a unit mask of 0x0f (All RAT) count 1000000
samples  %        samples  %        symbol name
21039    27.3767  10627    52.3885  triAsm2.loop
16125    20.9824  4815     23.7368  triC
14439    18.7885  4828     23.8008  triAsm1.loop
12557    16.3396  0              0  triAsm3.loop
12161    15.8243  8         0.0394  triAsm4.loop

Complete oprofile output.

结果:

triC:7410.000000 ms,a5afb9(asm 代码的 C 实现)

triAsm1:6690.000000 ms,a5afb9(来自 OP 的代码,使用 alah

triAsm2:9290.000000 ms,a5afb9(来自 OP 的代码,使用 ax

triAsm3: 5760.000000 ms, a5afb9(直接将 OPs 代码转换为不使用部分寄存器的代码)

triAsm4:5640.000000 ms,a5afb9(快速尝试使其更快)

这是我的测试套件,用-std=c99 -ggdb -m32 -O3 -march=native -mtune=native编译:

test.c:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>

extern void triC(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm1(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm2(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm3(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm4(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);

uint32_t scanline[640];

#define test(tri) \
    {\
        clock_t start = clock();\
        srand(60);\
        for (int i = 0; i < 5000000; i++) {\
            tri(scanline, rand() % 640, 10<<16, 20<<16, 30<<16, 1<<14, 1<<14, 1<<14);\
        }\
        printf(#tri ": %f ms, %x\n",(clock()-start)*1000.0/CLOCKS_PER_SEC,scanline[620]);\
    }

int main() {
    test(triC);
    test(triAsm1);
    test(triAsm2);
    test(triAsm3);
    test(triAsm4);
    return 0;
}

tri.c:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

void triC(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb) {
    while (cnt--) {
        cr += dcr;
        cg += dcg;
        cb += dcb;
        *dest++ = (cr & 0xffff0000) | ((cg >> 8) & 0xff00) | ((cb >> 16) & 0xff);
    }
}

atri.asm:

    bits 32
    section .text
    global triAsm1
    global triAsm2
    global triAsm3
    global triAsm4

%define cr DWORD [ebp+0x10]
%define dcr DWORD [ebp+0x1c]
%define dcg DWORD [ebp+0x20]
%define dcb DWORD [ebp+0x24]

triAsm1:
    push ebp
    mov ebp, esp

    pusha

    mov edx, [ebp+0x08] ; dest
    mov ecx, [ebp+0x0c] ; cnt
    mov esi, [ebp+0x14] ; cg
    mov edi, [ebp+0x18] ; cb

.loop:

    add esi, dcg
    mov eax, esi
    shr eax, 8

    add edi, dcb
    mov ebx, edi
    shr ebx, 16
    mov bh, ah

    mov eax, cr
    add eax, dcr
    mov cr, eax

    mov ah, bh  ; faster
    mov al, bl

    mov DWORD [edx], eax

    add edx, 4

    dec ecx
    jge .loop

    popa

    pop ebp
    ret


triAsm2:
    push ebp
    mov ebp, esp

    pusha

    mov edx, [ebp+0x08] ; dest
    mov ecx, [ebp+0x0c] ; cnt
    mov esi, [ebp+0x14] ; cg
    mov edi, [ebp+0x18] ; cb

.loop:

    add esi, dcg
    mov eax, esi
    shr eax, 8

    add edi, dcb
    mov ebx, edi
    shr ebx, 16
    mov bh, ah

    mov eax, cr
    add eax, dcr
    mov cr, eax

    mov ax, bx ; slower

    mov DWORD [edx], eax

    add edx, 4

    dec ecx
    jge .loop

    popa

    pop ebp
    ret

triAsm3:
    push ebp
    mov ebp, esp

    pusha

    mov edx, [ebp+0x08] ; dest
    mov ecx, [ebp+0x0c] ; cnt
    mov esi, [ebp+0x14] ; cg
    mov edi, [ebp+0x18] ; cb

.loop:
    mov eax, cr
    add eax, dcr
    mov cr, eax

    and eax, 0xffff0000

    add esi, dcg
    mov ebx, esi
    shr ebx, 8
    and ebx, 0x0000ff00
    or eax, ebx

    add edi, dcb
    mov ebx, edi
    shr ebx, 16
    and ebx, 0x000000ff
    or eax, ebx

    mov DWORD [edx], eax

    add edx, 4

    dec ecx
    jge .loop

    popa

    pop ebp
    ret

triAsm4:
    push ebp
    mov ebp, esp

    pusha

    mov [stackptr], esp

    mov edi, [ebp+0x08] ; dest
    mov ecx, [ebp+0x0c] ; cnt
    mov edx, [ebp+0x10] ; cr
    mov esi, [ebp+0x14] ; cg
    mov esp, [ebp+0x18] ; cb

.loop:
    add edx, dcr
    add esi, dcg
    add esp, dcb

    ;*dest++ = (cr & 0xffff0000) | ((cg >> 8) & 0xff00) | ((cb >> 16) & 0xff);
    mov eax, edx ; eax=cr
    and eax, 0xffff0000

    mov ebx, esi ; ebx=cg
    shr ebx, 8
    and ebx, 0xff00
    or eax, ebx
    ;mov ah, bh

    mov ebx, esp
    shr ebx, 16
    and ebx, 0xff
    or eax, ebx
    ;mov al, bl

    mov DWORD [edi], eax
    add edi, 4

    dec ecx
    jge .loop

    mov esp, [stackptr]

    popa

    pop ebp
    ret

    section .data
stackptr: dd 0

【讨论】:

  • 您在这里用来衡量性能的opreport 是什么?我在哪里可以找到有关它的信息?
  • 我知道这个答案已经有 4 年的历史了,但是在对最高投票答案中的错误发表评论后,我最终写了一个完整的答案。我还写了一个 asm 版本,它应该比你的 triAsm4 快得多,因为我使用了一个开销更少的循环,更重要的是,我想出了两种方法来减少屏蔽。 (预向左移动的东西,所以不能有垃圾高位,只有低位要摆脱。并且还使用 shld)。我认为我的在 core2 上每次迭代的运行速度应该低于 4c,但我没有测试它。 @blackbear,你可能也喜欢我的回答,然后:)
【解决方案4】:

在 32 位代码中,mov ax, bx 需要一个操作数大小的前缀,而字节大小的移动不需要。显然,现代处理器设计人员并没有花太多精力来让操作数大小的前缀快速解码,但令我惊讶的是,惩罚足以代替执行两个字节大小的移动。

【讨论】:

  • 我怀疑添加 66 前缀会导致这种情况。解码两条 2 字节指令比解码一条 3 字节指令更“昂贵”。
  • 嗯,据我所知——从相对幸福的无知来说——66h 可能会通过陷阱解码为微码,而且肯定会更慢。除了内存存储(无论如何都需要在解码器逻辑中使用特殊大小写),我认为编译后的代码不会有太多机会包含 16 位指令。
  • 我从专业角度说话(我是英特尔的验证工程师,我曾参与过 Core Duo 和 Core 2 Duo 项目),我可以告诉你,66h 不需要任何微码帮助.
  • (向权威低头)那我什么都没得到。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-18
相关资源
最近更新 更多