如何解决如果 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
if rsi
有人知道可以执行此操作的指令吗?我能想到的每条指令都在 src 上使用模块。任何帮助将不胜感激。
谢谢!
一些对我来说适用于 32 位的版本。如果我使用 MMX,@PeterCordes 指出 pllsq
正是我想要的。
uint64_t __attribute__((noinline,noclone)) shift(uint64_t cnt) {
uint64_t ret = 0;
asm volatile(
"cmpq $32,%[cnt]\n\t"
"setbe %b[ret]\n\t"
"shlxq %[cnt],%[ret],%[ret]\n\t"
: [ 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]\n\t"
"movl $1,%k[ret]\n\t"
"shlxq %[cnt],%[ret]\n\t"
"sarq $63,%[tmp]\n\t"
"andq %[tmp],%[ret]\n\t"
: [ 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]\n\t"
"btsq %[cnt],[ 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 问题——迭代 bitset 中的位的有效方法是使用 n & (n-1)
技巧或变体; popcount(n ^ (n-1))
应该给出最小位集的索引。 n&=n-1
将清除 LSB。
SIMD 移位(如 SSE2 psllq xmm1,xmm2
)使计数饱和,但这在这里不太可能有用,因为我认为您想将其 OR 到内存中的数据中作为此循环的标量版本的结束条件?
我更倾向于使用仍从 cmov
设置的 FLAGS 的清零寄存器中的 sub
。您可以在 SUB 之前或之后使用 BTS 将 1<<(rsi&63)
创建到清零寄存器中; SUB之前好,因为BTS修改了CF。请注意,rsi&63
不受 rsi -= 64
的影响。
对于循环条件,这可能不是一个好的选择:只需使用单 uop sub
/ja
,单独的 test/jnz
是通常不采取。其中之一位于底部而不是无条件的 jmp
:这是这里最明显和基本的优化:Why are loops always compiled into "do...while" style (tail jump)?
或者更好的是,使用 SSE2(x86-64 的基线)一次检查 16 个字节(2 个 qwords 或 4 个双字)是否为全非零。或者,如果您不希望很快找到第一个设置位,甚至可以将几个向量一起 POR 一起检查,即调整大行程计数,代价是最终找到它的处理速度较慢。 (最后一次迭代可以是标量)。
(查看 glibc 的 strlen
或特别是 memchr
以获取有关使用 SIMD 优化大数组 not-found-early 情况的更多想法。在这种情况下,他们使用 {{1}如果任何向量在该位置具有零,则获得零,但您想要相反的:pminub
如果任何向量具有非零,则获得非零。)
将内存中的两个值“或”在一起也适用于标量,作为展开的一种方式。
por
但请注意, mov (%rdi),%rax
or 8(%rdi),%rax
jnz found
...
add $16,%rdi
/or
为 2 uop,而 jnz
/test
为 1。
OTOH,让 jnz
/ cmpq $0,(%rdi)
在 Intel 上进行微型和宏观熔断器可能是不可能的; IIRC 可能带有寄存器源。因此,如果您非常积极地调整,内存源 jne
可能会花费 2 uop 来完成两倍的工作,而不是仅多 1 uop。您需要与展开并执行两个单独的 load/test/jcc 或 cmp-mem/jcc 的循环进行比较,以保持指针增量逻辑的循环开销公平。 (还有展开逻辑以处理可能的奇数个 qwords。)
但作为练习,让我们看看我们可以用你的想法做些什么:在这种情况下,可以提前计算一次非零移位结果(因为 rsi-=64 不会改变 rsi%64),并被吊出环。
or
不幸的是,OR 不能像 TEST 那样与 JCC 进行宏融合。 (或英特尔 SnB 系列上的 SUB)。但是内存源 OR 是前端的一个 uop。
不幸的是, 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
...
和 cmovbe
需要 2 uop,因为它们需要 CF 和 ZF。 (请参阅 What is a Partial Flag Stall? - 最近的英特尔没有部分标志停顿甚至合并,只有 CF 与其余部分(SPAZO),如果需要,微指令会分别读取两个输入。)但没有明显的原因, cmova
和 setbe
也是 2 uop (https://uops.info) - 也许英特尔从未更新 setcc uop 格式以用作 3 输入 uop(包括它们合并到低字节)。有趣的事实:这导致 seta
通常只能在“复杂”解码器中解码,如果您的代码不在 uop 缓存中:Can the simple decoders in recent Intel microarchitectures handle all 1-µop instructions?
多亏了 setcc
,Intel 上的循环体总共有 7 个 uops。 6 在 AMD。
比较没有此技巧的简单标量搜索循环与 4 uops 的比较。这可以在 Intel 上以每次迭代 1 个周期的速度运行(Haswell 和更高版本可以在每个时钟运行 2 个分支,只要最多采用 1 个)。我认为也在 AMD Zen 上。所以我们每个周期搜索大约 8 个字节,大约是我们可以用 SIMD 做的一半。但启动和结束开销较低。
cmov
如果您将负数组索引计数到零,则可以避免循环中同时包含 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
:使用通过添加设置的 FLAGS。 (或者对于简单版本,避免 CMP,让
你需要一个 sub
或类似的东西,但 Haswell 和更高版本可以保持索引寻址模式微融合作为具有 RW 目标的 2 操作数指令的一部分:Micro fusion and addressing modes)
or (%r10,%r11,8),%rax
不方便,因为它只适用于 8 位寄存器。但是如果你有一个像 RDX 这样的归零寄存器,你可以setcc r/m8
/ setbe dl
。即使您不想使用 SIMD,在 shlx %rsi,%rdx,%rcx
/ test
之前将其 OR 到 RAX 似乎也不值得。
正确预测的未采取测试/jnz 是一个单一的 uop,比您为创建 RAX 值所做的所有工作都便宜。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。