我正在尝试找到一种有效的方法来x86_64汇编中执行以下操作:
if(N < word_size) {
dst[N] = 1; // as in Nth bit of dst = 1
}
else {
dst[word_size - 1:0] = 0
}
如果"else"情况没有取消设置其他位,或者"if"案例没有取消设置其他位,我可以得到所需的结果。重要的是,如果 N> word_size它不会设置任何位
我找不到任何可能执行此操作的指令bt[s/c], shlx, sal, rol, shld
因为所有指令似乎都采用宽度src
模块。
用例基本上是我将迭代具有已知长度的位向量,并希望 A) 找到第一个设置位并返回其位置,或者 B) 测试所有位,如果没有找到设置位,则返回矢量的长度。
// rsi has length
L(keep_searching):
movq %(rdi), %rax
testq %rax, %rax
jnz L(found)
subq $64, rsi
jbe L(done) // this done will return origional value of rsi
addq $8, %rdi
jmp L(keep_searching)
我想如果我能快速设置一点rax
如果 64rsi
<这样我就可以放弃第二个分支,这可能会大大加快。但是要使其正常工作,它需要具有上述行为,即它不能设置><。>
有谁知道可以做到这一点的指令?我能想到的每个检查指令都使用 src 上的模块。任何帮助将不胜感激。
谢谢!
一些版本对我来说适用于 32 位。如果我使用MMX,@PeterCordes指出pllsq
这正是我想要的。
uint64_t __attribute__((noinline, noclone)) shift(uint64_t cnt) {
uint64_t ret = 0;
asm volatile(
"cmpq $32, %[cnt]nt"
"setbe %b[ret]nt"
"shlxq %[cnt], %[ret], %[ret]nt"
: [ ret ] "+r"(ret)
: [ cnt ] "r"(cnt)
: "cc");
return ret;
}
uint64_t __attribute__((noinline, noclone)) shift2(uint64_t cnt) {
uint64_t ret = 0, tmp = 0;
asm volatile(
"leaq -33(%[cnt]), %[tmp]nt"
"movl $1, %k[ret]nt"
"shlxq %[cnt], %[ret], %[ret]nt"
"sarq $63, %[tmp]nt"
"andq %[tmp], %[ret]nt"
: [ ret ] "+r"(ret), [ tmp ] "+r"(tmp), [ cnt ] "+r"(cnt)
:
: "cc");
return ret;
}
uint64_t __attribute__((noinline, noclone)) shift3(uint64_t cnt) {
uint64_t ret, tmp;
asm volatile(
"leaq -33(%[cnt]), %[tmp]nt"
"btsq %[cnt], %[ret]nt"
"sarq $63, %[tmp]nt"
"andq %[tmp], %[ret]nt"
: [ ret ] "+r"(ret), [ tmp ] "+r"(tmp), [ cnt ] "+r"(cnt)
:
: "cc");
return ret;
}
尚未验证,但
mov rax, 1 // common
mov rdx, 0 // common
cmp rcx, 64
shlxq rbx, rax, rcx
cmova rbx, rdx
可能比建议的替代方案性能稍高一些,因为比较和移位现在是独立的,可以并行执行。
编辑
从用例来看,这似乎是一个 XY 问题——迭代位集中位的有效方法是使用n & (n-1)
技巧或变体;popcount(n ^ (n-1))
应该给出最小位集的索引。n&=n-1
将清除 LSB。
SIMD 移位(如 SSE2psllq xmm1, xmm2
)使计数饱和,但这在这里不太可能有用,因为我认为您想将内存中的数据作为此循环的标量版本的结束条件?
我更倾向于使用仍然从sub
设置的 FLAGS 使用来自归零寄存器的cmov
. 您可以使用 BTS 在 SUB 之前或之后创建到归零寄存器中的1<<(rsi&63)
;在 SUB 之前很好,因为 BTS 修改了 CF。 请注意,rsi&63
不受rsi -= 64
的影响。
对于循环条件来说,这可能不是一个好的选择:只需使用单 uopsub
/ja
,通常不采用单独的test/jnz
。 其中一个位于底部而不是无条件jmp
:这是这里最明显和最基本的优化:为什么循环总是编译成"do...而"风格(尾跳)?
或者更好的是,使用 SSE2(x86-64 的基线)一次检查 16 个字节(2 个 qwords 或 4 个双字)是否全部为零。 或者,如果您不希望很快找到第一个设置位,甚至可以将几个向量放在一起,以便一次检查所有向量,即调整大行程计数,但代价是最终找到它的处理速度较慢。 (最后一次迭代可以是标量)。
(请查看 glibc 的strlen
,尤其是memchr
,了解有关使用 SIMD 优化大数组未发现早期案例的更多想法。 在这种情况下,如果任何向量在该位置为零,他们使用pminub
来获得零,但您希望相反:如果有任何向量具有非零,则por
得到非零。
将内存中的两个值放在一起也适用于标量,作为一种展开方式。
mov (%rdi), %rax
or 8(%rdi), %rax
jnz found
...
add $16, %rdi
但请注意,or
/jnz
是 2 uops,而test
/jnz
是 1.
OTOH,在英特尔上获得cmpq $0, (%rdi)
/jne
到微和宏保险丝可能是不可能的;IIRC可能带有注册源。 因此,内存源or
可能会花费 2 个 uops 来完成两倍的工作,而不是仅仅增加 1 个,如果你真的积极地调整的话。 您需要与一个循环进行比较,在该循环中,您展开并执行两个单独的加载/测试/jcc 或 cmp-mem/jcc,以保持指针增量逻辑的循环开销公平。 (并且还展开逻辑以处理可能的奇数个 qwords。
但作为一个练习,让我们看看我们可以用你的想法做些什么:在这种情况下,非零移位结果可以提前计算一次(因为rsi-=64不会改变rsi%64),并吊出循环。
xor %edx, %edx
bts %rsi, %rdx # rdx = 1 << (rsi&63)
// rsi has length
L(keep_searching):
add $8, %rdi
xor %eax, %eax # need to re-create a zero every time
sub $64, %rsi
cmovbe %rdx, %rax # 0 or 1<<(rsi&63) to put a bit there for us to find
or -8(%rdi), %rax
jnz L(keep_searching)
found_or_done:
tzcnt %rax, %rax
add orig_rsi?, %rax
...
不幸的是,OR不能像TEST那样与JCC进行宏融合。 (或英特尔 SnB 家族上的 SUB)。 但是内存源OR是前端的单个uop。
不幸的是,cmovbe
和cmova
花费 2 uops,因为它们需要 CF 和 ZF。 (请参阅什么是部分标志停顿?- 最近的英特尔没有部分标志停顿,甚至没有合并,只有 CF 与其余 (SPAZO),如果需要,uops 会分别读取两个输入。 但是没有明显的原因,setbe
和seta
也是 2 uops (https://uops.info) - 也许英特尔从未更新 setcc uop 格式以用作 3 输入 uop(包括它们合并到低字节的完整寄存器)。 有趣的事实:如果您的代码不在 uop 缓存中,这导致setcc
通常只能在"复杂"解码器中解码:最新英特尔微架构中的简单解码器能否处理所有 1-μop 指令?
由于cmov
,英特尔的循环体总共为 7 uops。 6 在 AMD 上。
比较与 4 uops 进行简单的标量搜索循环,而无需此技巧。 在英特尔上,每次迭代可以以 1 个周期的速度运行(Haswell 及更高版本每个时钟可以运行 2 个分支,只要最多运行 1 个分支)。 我认为也在AMD Zen上。 因此,我们每个周期搜索大约 8 个字节,大约是 SIMD 可以搜索的一半。 但是启动和结束开销较低。
L(loop): # do {
mov (%rdi), %rax # 1 uop
test %rax,%rax
jnz L(found) # 1 uop (macro-fused)
add $8, %rdi # 1 uop
cmp %rdi, %rdx
jbe L(loop) # }while(p < endp) # 1 uops (macro-fused)
L(done):
如果将负数组索引计数为零,则可以避免循环中同时add
和sub
:使用由 add 设置的 FLAGS。 (或者对于简单版本,避免使用CMP,让
你需要一个or (%r10, %r11, 8), %rax
或类似的东西,但Haswell和更高版本可以将索引寻址模式作为具有RW目的地的2操作数指令的一部分进行微融合:微融合和寻址模式)
setcc r/m8
很不方便,因为它仅适用于 8 位寄存器。 但是如果你有一个像RDX这样的归零寄存器,你可以setbe dl
/shlx %rsi, %rdx, %rcx
。 在test
/jz keep_searching
之前将其放入 RAX 似乎不太可能值得,即使您不想使用 SIMD。
正确预测的未参加测试/jnz 是一个 uop,比您为创建 RAX 值所做的所有工作都便宜。