我有一个巨大的内存块(位向量),大小为 N 位,在一个内存页面内,考虑 N 平均为5000,即5 k位存储一些标志信息。
在某个时间点(超频繁临界),我需要找到整个大位向量中的第一位集。现在我每64个字执行一次,即借助__builtin_ctzll
)。但当 N 增长且搜索算法无法改进时,可能会通过扩展内存访问宽度来扩展此搜索。这是几个字中的主要问题
有一条名为BSF
的汇编指令,它给出了最高设置位的位置(GCC的__builtin_ctzll()
)。所以在x86-64 arch中,我可以在64位字中找到最高设置位。
但是,如何通过内存宽度进行扩展呢?
例如,有没有一种方法可以有效地使用128 / 256 / 512位寄存器?
基本上我感兴趣的是用一些C API函数来实现这个,同时也想知道这个方法是基于什么。
**UPD:**对于CPU,我对这次优化感兴趣,以支持以下CPU阵容:
英特尔至强E3- 12 XX、英特尔至强E5- 22 XX/26 XX/E56 XX、英特尔酷睿i3-5XX/4XXX/8XXX、英特尔酷睿i5- 7 XX、英特尔酷睿G18 XX/G49 XX(英特尔凌动N2600、英特尔酷睿N2807、Cortex-A53/72可选)
**P.S.**在提到的算法中,在最后一次位扫描之前,我需要用CPU AND对 k(平均20-40)N 位向量求和(AND结果只是位扫描的准备阶段)。这也是存储器宽度缩放所需的(即比每64位字AND更有效)
3条答案
按热度按时间0sgqnhkj1#
这个答案是不同的,但是如果你事先知道你将维护一个B位的集合,并且需要能够有效地设置和清除位,同时还需要弄清楚哪个位是第一个设置的位,你可能想要使用像van Emde Boas tree或y-fast trie这样的数据结构。这些数据结构被设计用于存储小范围内的整数,因此,您可以添加或删除您想要设置/清除的位的索引,而不是设置或清除单个位。它们非常快-您可以在时间O(log log B)内添加或删除项,并且它们可以让您在时间O(1)内找到最小项。请注意,如果B = 50000,则log log B约为4。
我知道这并没有直接解决如何在一个巨大的位向量中找到最高位集的问题。如果你的设置是这样的,你必须使用位向量,其他的答案可能会更有帮助。但是如果你可以选择以一种不涉及位向量搜索的方式重新定义问题,这些其他的数据结构可能会更适合。
bnl4lu3b2#
在整个向量(AFAIK)中找到第一个设置位的最佳方法是找到第一个非零SIMD元素(例如字节或双字),然后对其进行位扫描。(
__builtin_ctz
/bsf
/tzcnt
/ffs
-1)。因此,ctz(vector)本身并不是搜索数组的有用构建块,仅用于循环后。相反,您希望循环数组以搜索非零向量,使用涉及SSE 4.1
ptest xmm0,xmm0
/jz .loop
(3个uops)的全向量检查,或使用SSE 2pcmpeqd v, zero
/pmovmskb
/cmp eax, 0xffff
/je .loop
(cmp/jcc宏融合后的3个uops)。一旦找到非零向量,
pcmpeqb
/movmskps
/bsf
* 在该 * 上查找双字索引,然后加载该双字并bsf
它。(CHAR_BIT*4*dword_idx
)到该元素内的bsf
位位置。这是一个相当长的延迟依赖链,包括一个整数L1 d的加载延迟。但是因为你刚刚加载了这个向量,至少你可以相当自信地说,当你再次用整数加载它时,你会在缓存中命中。(如果向量是动态生成的,那么最好还是存储/重新加载它,让存储转发工作,而不是尝试为vpermilps
/movd
或SSSE 3pshufb
/movd
/movzx ecx, al
生成随机播放控件。)循环问题非常类似于
strlen
或memchr
,除了我们 * 拒绝 * 单个值(0)并寻找任何其他值 *。尽管如此,我们仍然可以从手工优化的asm strlen / memchr实现(如glibc)中获得灵感,例如加载多个向量并检查其中 * 任何 * 是否有他们想要的。(对于strlen,如果任何元素为0,则将合并与pminub
组合以获得0。对于pcmpeqb
比较结果,对于memchr为OR)。对于我们的目的,我们想要的归约操作是OR -任何非零输入将使输出非零,并且按位布尔运算可以在任何向量ALU端口上运行。(If预期的第一位位置不是 * 非常 * 高,这不值得 * 太 * 激进:如果第一个设置位在第一个向量中,那么在您加载的2个向量之间进行排序将更慢。5000位仅为625字节,或19.5 AVX 2
__m256i
向量。并且第一个设置位可能并不总是正确的。AVX 2版本:
这会检查32字节的向量对(即整个缓存行)是否为非零,如果找到,则将其排序到一个64位位图中,用于单个CTZ操作。额外的移位/OR会导致关键路径的延迟,但希望我们能更快地到达第一个1位。
用OR将2个向量组合成1个向量意味着知道OR结果的哪个元素是非零的不是特别有用。我们基本上在if中重新做了工作。这是我们为保持实际搜索部分的uop数量较低而付出的代价。
(The
if
主体以return
结尾,所以在asm中它实际上像if()break
,或者实际上是循环外的if()goto
,因为它去的地方与循环外未找到的返回不同。字符串
在Godbolt与clang 12 -O3 -march=haswell:
型
这可能并不适用于所有CPU,例如,也许我们可以为至少一个输入使用内存源
vpcmpeqd
,并且不需要任何额外的前端uop,只需要后端。只要编译器继续使用指针增量,而不是indexed addressing modes that would un-laminate。这将减少分支之后所需的工作量(这可能会错误预测)。要仍然使用
vptest
,您可能必须利用CF = (~dst & src == 0)
操作对全1向量的CF结果,因此我们可以检查所有元素是否匹配(即输入为全0)。不幸的是,Can PTEST be used to test if two registers are both zero or some other condition?-不,我认为如果没有vpor
,我们无法有效地使用vptest
。Clang决定在循环后不实际减去指针,而是在搜索循环中做更多的工作。:/循环是9 uops(在
cmp
/jb
的宏融合之后),所以不幸的是,它每2个周期只能运行不到1次迭代。所以它只管理不到一半的L1 d缓存带宽。但显然,单个数组并不是您的真实的问题。
不带AVX
16-字节向量意味着我们不必处理AVX 2 shuffle的“通道内”行为。因此,我们可以使用
packssdw
或packsswb
进行合并组合,而不是OR。包输入的高半部分中的任何设置位都会使结果符号饱和为0x 80或0x 7 f。(因此,有符号饱和度是关键,而不是unsignedpackuswb
,它会使符号负输入饱和为0。)但是,shuffle只能在Intel CPU的端口5上运行,所以要注意吞吐量限制。例如Skylake上的
ptest
是2个uops,p5和p0,所以使用packsswb
+ptest
+jz
将限制为每2个时钟进行一次迭代。但是pcmpeqd
+pmovmskb
没有。不幸的是,在打包/合并之前对每个输入单独使用
pcmpeq
会消耗更多的uop,但会减少清理的工作量,如果循环退出通常涉及分支预测错误,这可能会减少整体延迟。2x
pcmpeqd
=>packssdw
=>pmovmskb
=>not
=>bsf
将给予一个必须乘以2的数字,作为字节偏移量来获得非零双字。例如memcpy(&tmp_u32, p + (2*idx), sizeof(tmp_u32));
。即bsf eax, [rdi + rdx*2]
。使用AVX-512:
你提到了512位向量,但你列出的CPU都不支持AVX-512。即使是这样,你可能也想避免512位向量,因为SIMD instructions lowering CPU frequency,除非你的程序花了很多时间来做这件事,而且你的数据在L1 d缓存中很热,所以你可以真正受益,而不是仍然检查L2缓存带宽。但即使是256位向量,AVX-512有新的指令,这是有用的:
vpcmpb/w/d/q
)有一个 predicate 的选择,所以你可以做不相等,而不是必须在后面用NOT反转。或者甚至测试到寄存器vptestmd
,所以你不需要一个零向量来比较。k
寄存器中,在你可以tzcnt
之前仍然需要kmovq rax, k0
。kortest
-根据两个掩码寄存器的OR非零设置FLAGS。因此搜索循环可以执行vpcmpd k0, ymm0, [rdi]
/vpcmpd k1, ymm0, [rdi+32]
/kortestw k0, k1
vplzcntd
(或q
)-结合SIMDisolate_lowest = v &= -v
,可以找到最低设置位的位置(在每个SIMD向量中)。对于非零输入,bit_index = 31-lzcnt = 31^lzcnt。vpcompressq
/d
-在Intel和Zen 4上为reg-reg版本(https://uops.info)执行2个uops。然后是vmovq eax, ymm0
,它可以提取最低的非零元素(给定比较掩码),其延迟可能低于掩码上的标量tzcnt
,以索引另一个负载。但是您仍然需要标量
tzcnt
来找出要添加到双字内位索引中的内容,因此这只会花费额外的uop来缩短关键路径延迟。型
对多个输入数组进行AND运算
你提到你的真实的问题是你有多达20个位数组,你想用AND求它们的交集,并找到交集中的第一个设置位。
你可能想在几个向量的块中做这件事,乐观地希望在早期的某个地方会有一个设置位。
与4个或8个输入的组,用OR在结果之间累加,这样你就可以判断在这个可能有4个向量的块中是否有1。(如果没有任何1位,那么在仍然加载指针的情况下,执行另一个4个向量的块,64或128字节,因为如果你现在移动到其他输入,交集肯定是空的)。调整这些块的大小取决于你的1有多稀疏,例如,可能总是在6或8个向量的块中工作。但是,2的幂数字很好,因为你可以将分配填充到64或128字节的倍数,所以你不必担心提前停止。)
(For奇数个输入,可能会将同一个指针传递两次到期望4个输入的函数,而不是为每个可能的数字分配特殊版本的循环。
L1 d缓存是8路关联的(在Ice Lake之前有12路),有限数量的整数/指针寄存器可能会让一次读取太多的流是一个坏主意。你可能也不希望有一个间接级别,使编译器在指针内存中的实际数组上循环。
6bc51xsx3#
你可以试试这个函数,你的编译器应该会为你的CPU优化这段代码。它不是超级完美,但它应该是相对快速的,而且大部分是可移植的。
PS
length
应能被8整除以获得最大速度字符串