Assembly 使用循环展开计算正数、负数和零数的最有效方法

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

假设我有下面的指令,只需检查一个数字是否为正(负或零),如果为正,则将1添加到计数器中(我们不关心数字是否为负或零)。我可以通过简单的循环展开来实现这一点:

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


但是,我确实发现您的代码中有几个大的改进:

  • 重用第一次测试设置的标志,而不是重新测试

  • 另一件重要的事情是将
    %r11=1
    从循环中提升出来,这样您就可以用一个insn递增。即使在实际代码中,在寄存器中设置常量也是非常常见的事情。大多数ISA(包括RISC load store机器)都有添加即时指令,比如x86的
    添加$1,%rax
    ,但y86没有,所以它需要这种技术,即使是增量(x86
    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


但是,我确实发现您的代码中有几个大的改进:

  • 重用第一次测试设置的标志,而不是重新测试

  • 另一件重要的事情是将
    %r11=1
    从循环中提升出来,这样您就可以用一个insn递增。即使在实际代码中,在寄存器中设置常量也是非常常见的事情。大多数ISA(包括RISC load store机器)都有添加即时指令,比如x86的
    添加$1,%rax
    ,但y86没有,所以它需要这种技术,即使是增量(x86
    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