如果 N <宽度 (DST),x86_64有效地在寄存器 DST 中设置位 N,否则 DST = 0



我正在尝试找到一种有效的方法来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。

不幸的是,cmovbecmova花费 2 uops,因为它们需要 CF 和 ZF。 (请参阅什么是部分标志停顿?- 最近的英特尔没有部分标志停顿,甚至没有合并,只有 CF 与其余 (SPAZO),如果需要,uops 会分别读取两个输入。 但是没有明显的原因,setbeseta也是 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):

如果将负数组索引计数为零,则可以避免循环中同时addsub:使用由 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 值所做的所有工作都便宜。

最新更新