Assembly 使用循环展开计算正数、负数和零数的最有效方法
假设我有下面的指令,只需检查一个数字是否为正(负或零),如果为正,则将1添加到计数器中(我们不关心数字是否为负或零)。我可以通过简单的循环展开来实现这一点:Assembly 使用循环展开计算正数、负数和零数的最有效方法,assembly,optimization,micro-optimization,loop-unrolling,y86,Assembly,Optimization,Micro Optimization,Loop Unrolling,Y86,假设我有下面的指令,只需检查一个数字是否为正(负或零),如果为正,则将1添加到计数器中(我们不关心数字是否为负或零)。我可以通过简单的循环展开来实现这一点: Loop: mrmovq (%rdi), %r10 # read val[0] from src andq %r10, %r10 # val[0] <= 0? jle Npos1 # if so, goto Npos: iaddq $1, %ra
Loop:
mrmovq (%rdi), %r10 # read val[0] from src
andq %r10, %r10 # val[0] <= 0?
jle Npos1 # if so, goto Npos:
iaddq $1, %rax # count++
Npos1:
mrmovq 8(%rdi), %r11 # read val[1] from src+8
andq %r11,%r11 # val <= 0?
jle Npos2 # if so, goto next npos:
iaddq $1, %rax
Npos2:
mrmovq 16(%rdi), %r11 # read val[2] from src+8
andq %r11,%r11 # val <= 0?
jle Npos3 # if so, goto next npos:
iaddq $1, %rax
更新:查看非分支版本的最终版本,该版本应该更好,并且可以轻松展开。但其余的答案仍然值得一读 我确实找到了一种方法,通过展开保存每个测试值执行的两条指令,但与使用循环尾复制的优化版本相比,这是非常小的。(见下文)
在很多情况下,与实际体系结构相比,y86过于精简,无法实现高效的代码。首先,似乎没有一种方法可以在不破坏标志的情况下有条件地递增。(x86有
lea-rax[rax+1]
)
我看不到一种方法可以通过只计算正和零来节省大量指令,并在循环结束后从中计算负计数。您仍然需要分支来测试每个值更新:不,您不需要,因为您可以使用y86的cmov
模拟x86的setcc
强>
但是,我确实发现您的代码中有几个大的改进:
- 重用第一次测试设置的标志,而不是重新测试
- 另一件重要的事情是将
从循环中提升出来,这样您就可以用一个insn递增。即使在实际代码中,在寄存器中设置常量也是非常常见的事情。大多数ISA(包括RISC load store机器)都有添加即时指令,比如x86的%r11=1
,但y86没有,所以它需要这种技术,即使是增量(x86添加$1,%rax
)inc%rax
设置标志,因此使用它们而不是单独进行测试sub
irmovq $1, %r11 # hoisted out of the loop
irmovq $8, %r8
Loop: mrmovq (%rdi), %r10 # read val from src...
andq %r10, %r10 # set flags from val
jle not_positive
addq %r11, %rax # Count positives in rax - count_pos++
jmp Rest
not_positive:
je Zero # flags still from val
addq %r11, %rbx # Count negatives in rbx - count_neg++
jmp Rest
Zero:
addq %r11, %rcx # Count zeroes in rcx - count_zero++
Rest:
addq %r8, %rdi # src+=8
//addq %r8, %rsi # dst++ // why? Not used... Also note that si stands for source index, so prefer keeping src pointers in rsi, and dest pointers in rdi for human readability.
subq %r11, %rdx # len--, setting flags
jg Loop # } while( len-- > 1). fall through when rdx=0
环尾重复: 您可以通过复制循环尾部来增加代码大小,但减少实际运行的指令数 我还重新构造了循环,这样在循环体中每次迭代只有一个执行的分支
irmovq $1, %r11 # hoisted out of the loop
irmovq $8, %r8
Loop: mrmovq (%rdi), %r10 # read val from src...
addq %r8, %rdi # src+=8 for next iteration
andq %r10, %r10 # set flags from val
je Zero
jl Negative
# else Positive
addq %r11, %rax # Count positives in rax - count_pos++
subq %r11, %rdx
jg Loop
jmp end_loop
Negative:
addq %r11, %rbx # Count negatives in rbx - count_neg++
subq %r11, %rdx
jg Loop
jmp end_loop
Zero:
addq %r11, %rcx # Count zeroes in rcx - count_zero++
subq %r11, %rdx # len--, setting flags
jg Loop # } while( len-- > 1). fall through when rdx=0
end_loop:
展开没有什么好处,因为循环体太大了。如果您这样做了,您可能会这样做:
请注意,我们每次迭代只更新和检查len
一次。这意味着我们需要一个清理循环,但一次只减少和检查一个循环,这通常会破坏展开的目的
由两个展开,尾部重复
如果在我的循环条件中有一个off-by-one错误或其他什么,我不会感到惊讶
正如Jester在评论中指出的,x86可以使用
sar $63, %r10 # broadcast the sign bit to all bits: -1 or 0
sub %r10, %rbx # subtracting -1 (or 0): i.e. add 1 (or 0)
更新:使用cmov模拟setcc的非分支版本 我们可以使用
cmov
将寄存器设置为0或1,然后将其添加到计数中。这避免了所有分支。(0是加法标识。此基本技术适用于具有标识元素的任何操作。例如,“所有”是的标识元素,而“1”是乘法的标识元素。)
展开这是很简单的,但是有3条指令的循环开销,而8条指令需要重复。收益将相当小
irmovq $1, %r11 # hoisted out of the loop
irmovq $8, %r8
mov %rdx, %rbx # neg_count is calculated later
Loop: mrmovq (%rdi), %r10 # read val from src...
addq %r8, %rdi # src+=16 for next iteration
andq %r10, %r10 # set flags from val
irmovq $0, %r13
cmovg %r11, %r13 # emulate setcc
irmovq $0, %r14
cmove %r11, %r14
add %r13, %rax # pos_count += (val > 0)
add %r14, %rcx # zero_count += (val == 0)
subq %r11, %rdx # len-=1, setting flags
jg Loop # fall through when rdx=0
end_loop:
sub %rax, %rbx
sub %rcx, %rbx # neg_count = initial_len - pos_count - zero_count
如果分支(特别是不可预测的分支)很昂贵,那么这个版本会做得更好。使用Jester的建议,从另外两个计算其中一个计数,在这种情况下有很大帮助
这里有相当好的指令级并行性。一旦测试结果准备好,两个单独的setcc->add依赖项链就可以并行运行。更新:请参阅最后一个非分支版本,该版本应该会更好,而且不需要展开。但其余的答案仍然值得一读 我确实找到了一种方法,通过展开保存每个测试值执行的两条指令,但与使用循环尾复制的优化版本相比,这是非常小的。(见下文)
在很多情况下,与实际体系结构相比,y86过于精简,无法实现高效的代码。首先,似乎没有一种方法可以在不破坏标志的情况下有条件地递增。(x86有
lea-rax[rax+1]
)
我看不到一种方法可以通过只计算正和零来节省大量指令,并在循环结束后从中计算负计数。您仍然需要分支来测试每个值更新:不,您不需要,因为您可以使用y86的cmov
模拟x86的setcc
强>
但是,我确实发现您的代码中有几个大的改进:
- 重用第一次测试设置的标志,而不是重新测试
- 另一件重要的事情是将
从循环中提升出来,这样您就可以用一个insn递增。即使在实际代码中,在寄存器中设置常量也是非常常见的事情。大多数ISA(包括RISC load store机器)都有添加即时指令,比如x86的%r11=1
,但y86没有,所以它需要这种技术,即使是增量(x86添加$1,%rax
)inc%rax
设置标志,因此使用它们而不是单独进行测试sub
sar $63, %r10 # broadcast the sign bit to all bits: -1 or 0
sub %r10, %rbx # subtracting -1 (or 0): i.e. add 1 (or 0)
irmovq $1, %r11 # hoisted out of the loop
irmovq $8, %r8
mov %rdx, %rbx # neg_count is calculated later
Loop: mrmovq (%rdi), %r10 # read val from src...
addq %r8, %rdi # src+=16 for next iteration
andq %r10, %r10 # set flags from val
irmovq $0, %r13
cmovg %r11, %r13 # emulate setcc
irmovq $0, %r14
cmove %r11, %r14
add %r13, %rax # pos_count += (val > 0)
add %r14, %rcx # zero_count += (val == 0)
subq %r11, %rdx # len-=1, setting flags
jg Loop # fall through when rdx=0
end_loop:
sub %rax, %rbx
sub %rcx, %rbx # neg_count = initial_len - pos_count - zero_count