当前位置:  开发笔记 > 编程语言 > 正文

x86装配 - 夹紧rax优化到[0 ..极限)

如何解决《x86装配-夹紧rax优化到[0..极限)》经验,为你挑选了1个好方法。

我正在编写一个简单的汇编程序,当然,它的目的是尽可能快.但是,位于最嵌套循环中的某个部分看起来并不"正确",我相信有可能提出更聪明,更快速的实现,甚至可能不使用条件跳转.代码实现了一个简单的事情:

if rax < 0 then rax := 0 else if rax >= r12 then rax := r12 - 1

这是我天真的实施:

cmp rax, 0
jge offsetXGE
   mov rax, 0
   jmp offsetXReady
offsetXGE:
   cmp rax, r12
   jl offsetXReady
   mov rax, r12
   dec rax
offsetXReady:

任何想法都是受欢迎的,即使是那些使用MMX和一些掩盖技巧的想法.

编辑:回答评论中的一些问题 - 是的,我们可以假设r12> 0但rax可能是负数.



1> Peter Cordes..:

一个比较和分支,以及LEA + cmov.


将标量数据移动到一个或两个指令的向量regs,然后将其移回是不值得的.如果您可以一次有效地执行整个向量,那么您可以使用PMINSD/PMAXSD将值钳位到这样的范围.

在你的原版中,有些东西显然不是最佳的.前两个大部分时间只对代码大小有影响,但LEA对于非破坏性的添加是一个小而明确的胜利:

cmp eax, 0 应该 test eax, eax

mov rax, 0 应该是xor eax, eax.不,eax不是一个错字rax.

mov rax, r12 / dec rax 应该是lea rax, [r12 - 1].

请参阅x86 wiki中的链接,尤其是 Agner Fog的指南.


在搜索了一些之后,我发现了一个类似的问题,关于最佳x86 asm用于钳位到一个范围.我从中获得了一些灵感,但大部分是用cmov而不是用cmov重写的setcc/dec/and.

对于最新的Intel或AMD CPU,我认为这和你能得到的一样好:

您需要一个寄存器(或存储器位置)保持0,或者需要一个额外的指令mov reg, 0.

    ...
    cmp  rax, r12
    jae  .clamp      ; favour the fast-path more heavily by making it the not-taken case
.clamp_finished:     ; rdx is clobbered, since the clamp code uses a scratch reg

    ...
    ret

.clamp:   
    ; flags still set from the cmp rax, r12
    ; we only get here if rax is >= r12 (`ge` signed compare), or negative (`l` rax < r12, signed)

    ; mov r15d, 0        ; or zero it outside the loop so it can be used when needed.  Can't xor-zero because we need to preserve flags

    lea    rax, [r12-1]  ; still doesn't modify flags
    cmovl  eax, r15d     ; rax=0 if  orig_rax

英特尔Haswell的快速​​性能分析:

快速路径:一个不采用比较和分支的uop.rax的延迟:0个周期.

需要夹紧的情况:一个采用比较和分支的uop,再加上4个uop(lea,2个用于cmov,1个用于jmp返回.)rax的延迟:从rax和r12的后期开始的3个周期(cmp-> flags ,flags-> cmov).

显然,您可以使用jb而不是jae跳过夹紧lea/cmov,而不是将它们拉出主流.请参阅以下部分,了解其动机.(和/或看到Anatolyg的出色答卷,其中涵盖这一点.我使用的酷技巧jb[0 .. limit]与Anatolyg的回答一个分支,也是如此).

我认为使用cmov的版本是最好的选择,尽管cmov有许多缺点并且并不总是更快.它的输入操作数已经是必需的,因此它不会增加太多的延迟(除了带分支的钳位到零的情况,见下文).

.clamp不需要归零寄存器的代码的替代分支实现将是:

.clamp:
    lea    rax, [r12-1]
    jge  .clamp_finished
    xor    eax, eax
    jmp  .clamp_finished

它仍会计算出它可能会丢弃的结果,cmov风格.但是,以下xor启动了一个新的依赖链,因此它不必等待lea写入rax.


一个重要的问题是你经常期望采取这些分支.如果存在常见情况(例如,无钳位情况),请将其作为代码的快速路径(尽可能少的指令和尽可能少的分支).根据不经常使用分支的方式,在函数结束时将非常见情况的代码放在一边是值得的.

func:
    ...
    test
    jcc .unlikely
    ...        
.ret_from_unlikely:
    ...
    ... ;; lots of code
    ret

.unlikely:
    xor   eax,eax
    jmp .ret_from_unlikely   ;; this extra jump makes the slow path slower, but that's worth it to make the fast path faster.

Gcc这样做,我认为当它决定不太可能采取分支时.因此,不是让典型案例采用跳过某些指令的分支,而是常见的情况.通常,默认分支预测不用于前向跳转,因此在看到不太可能的情况之前,甚至不需要分支预测器条目.


随意的想法:代码

if (eax < 0) { eax = 0; }
else if (eax >= r12) { eax := r12 - 1 }  // If r12 can be zero, the else matters

相当于

eax = min(eax, r12-1);
eax = max(eax, 0);

r12不能否定,但OP并没有说它不能为零.此排序保留if/else语义.(编辑:实际上OP确实说你可以假设r12> 0,而不是> = 0.)如果我们在asm中有一个快速的最小值/最大值,我们可以在这里使用它.vector-max是单指令,但标量需要更多代码.

推荐阅读
惬听风吟jyy_802
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有