我正在编写一个简单的汇编程序,当然,它的目的是尽可能快.但是,位于最嵌套循环中的某个部分看起来并不"正确",我相信有可能提出更聪明,更快速的实现,甚至可能不使用条件跳转.代码实现了一个简单的事情:
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可能是负数.
将标量数据移动到一个或两个指令的向量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
.
您需要一个寄存器(或存储器位置)保持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是单指令,但标量需要更多代码.