更新:看到一个非分支版本的最后,它应该会更好,而且展开起来很简单。但其余的答案仍然值得一读,IMO。
我确实找到了一种方法来保存每个测试值执行的几条指令,并展开,但与我使用循环尾复制的优化版本管理的相比,它非常小。(见下文)。
在很多情况下,与真实架构相比,y86 过于精简,无法提供高效的代码。一方面,似乎没有一种方法可以在不破坏标志的情况下有条件地递增。(x86 有lea rax, [rax+1]
)。
我没有看到一种方法可以通过仅计算正数和零并在循环后计算负数来节省大量指令。您仍然需要分支来测试每个值。 更新:不,您不需要,因为您可以setcc
使用 y86模拟 x86 cmov
!
但是,我确实在您的代码中发现了一些重大改进:
重用第一次测试设置的标志,而不是重新测试
另一个重要的事情是提升%r11 = 1
循环外,所以你可以只增加一个insn。即使在实际代码中,在寄存器中设置常量也是很常见的事情。大多数 ISA(包括 RISC 加载存储机器)都有 add-immediate 指令,比如 x86's add $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
每次迭代只更新和检查一次。这意味着我们需要一个清理循环,但一次只减少和检查一个会破坏展开的目的。
由两个展开,尾部重复
irmovq $1, %r11 # hoisted out of the loop
irmovq $2, %r12
irmovq $16, %r8
sub %r12, %rdi
jl end_loop # unrolled loop requires len >= 2
Loop: mrmovq (%rdi), %r10 # read val from src...
mrmovq 8(%rdi), %r9 # read next val here so we don't have to duplicate this
addq %r8, %rdi # src+=16 for next iteration
andq %r10, %r10 # set flags from val
je Zero1
jl Negative1
# else Positive1
addq %r11, %rax # Count positives in rax - count_pos++
andq %r9, %r9 # set flags from val2
je Zero2
jl Negative2
Positive2:
addq %r11, %rax # Count positives in rax - count_pos++
subq %r12, %rdx # loop tail
jge Loop
jmp end_loop
Negative1:
addq %r11, %rbx # Count negatives in rbx - count_neg++
andq %r9, %r9 # set flags from val2
je Zero2
jg Positive2
Negative2:
addq %r11, %rbx # Count negatives in rbx - count_neg++
subq %r12, %rdx # loop tail
jge Loop
jmp end_loop
Zero1:
addq %r11, %rcx # Count zeroes in rcx - count_zero++
andq %r9, %r9 # set flags from val2
jg Positive2
jl Negative2
Zero2:
addq %r11, %rcx # Count zeroes in rcx - count_zero++
subq %r12, %rdx # len-=2, setting flags
jge Loop # fall through when rdx=-1 or -2
end_loop:
# loop epilogue to handle cases where there was an odd number of elements, so rdx=-1 here:
add %r12, %rdx
jne all_done
#else there's one more to do
#... load and test a single element
如果在我的循环条件或其他东西中有一个错误,我不会感到惊讶。
就像 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 是加法恒等式。这种基本技术适用于任何具有恒等元素的操作。例如,全一是 AND 的恒等元素。1 是乘法的恒等元素。)
展开这很简单,但是与需要重复的 8 条指令相比,有 3 条循环开销指令。收益将相当小。
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 依赖链可以并行运行。