提问者:小点点

使用simd查找double数组中的nan


这个问题非常类似于:

浮点相等比较的SIMD指令(NaN==NaN)

尽管这个问题集中在128位向量上,并且对识别0和-0有要求。

我有一种感觉,我可能自己也能得到这个,但英特尔内部指南页面似乎已关闭:/

我的目标是取一个双打数组,并返回数组中是否存在NaN。我预计大多数时候不会有,并希望那条路线有最好的性能。

最初,我打算对 4 个双精度值进行比较,以反映用于 NaN 检测的非 SIMD 方法(即只有 NaN 值,其中 a != a 为真)。像这样:

data *double = ...
__m256d a, b;
int temp = 0;

//This bit would be in a loop over the array
//I'd probably put a sentinel in and loop over while !temp
a = _mm256_loadu_pd(data);
b = _mm256_cmp_pd(a, a, _CMP_NEQ_UQ);
temp = temp | _mm256_movemask_pd(b);

然而,在一些比较的例子中,除了比较本身之外,似乎还有某种NaN检测正在进行。我短暂地想,如果像_CMP_EQ_UQ这样的东西会检测到NaN,我可以使用它,然后我可以将4个双打与4个双打进行比较,并神奇地同时查看8个双打。

__m256d a, b, c;
a = _mm256_loadu_pd(data);
b = _mm256_loadu_pd(data+4);
c = _mm256_cmp_pd(a, b, _CMP_EQ_UQ);

在这一点上,我意识到我没有很好地思考,因为我可能碰巧比较了一个不是NaN(即3 == 3)的数字本身,并以这种方式得到一个命中结果。

所以我的问题是,比较4个doubles值(如上所述)是我能做的最好的事情吗?还是有其他更好的方法来找出我的数组是否有NaN?


共1个答案

匿名用户

您可能可以通过检查fenv状态来完全避免这种情况,或者如果没有,则缓存阻止它和/或将其折叠到同一数据的另一个通道中,因为它的计算强度非常低(加载/存储的每字节的工作量),因此很容易造成内存带宽瓶颈。见下文。

您要查找的比较谓词是_CMP_UNORD_Q_CMP_ORD_Q,以告诉您比较是无序的或有序的,即至少一个操作数是NaN,或者两个操作数分别是非NaN。有序/无序比较是什么意思?

cmppd 的 asm 文档列出了谓词,并且具有与内部函数指南相同或更好的详细信息。

所以是的,如果你希望NaN很少,并且想快速扫描大量非NaN值,你可以vcmppd两个不同的向量相互对抗。如果你关心NaN在哪里,一旦你知道两个输入向量中的任何一个中至少有一个,你就可以做额外的工作来整理它。(如_mm256_cmp_pd(a, a,_CMP_UNORD_Q)为movem干bit扫描提供最低设置位。)

与其他SSE/AVX搜索循环一样,您还可以通过将一些比较结果与_mm256_or_pd(查找任何无序)或-mm256_and_pd结合起来,来分摊movemask成本。E、 g.检查每个移动掩码/测试/分支的两条缓存线(4x<code>_mm256d</code>和2x<code<_mm256_cmp_pd</code>)。(glibc的asm<code>memchr</code>和<code>strlen</code>使用了这个技巧。)同样,这优化了您的常见情况,即您不需要提前退出,并且必须扫描整个阵列。

还要记住,检查同一个元素两次是完全可以的,所以清理可以很简单:一个向量加载到数组的末尾,可能与已经检查过的元素重叠。

// checks 4 vectors = 16 doubles
// non-zero means there was a NaN somewhere in p[0..15]
static inline
int any_nan_block(double *p) {
    __m256d a = _mm256_loadu_pd(p+0);
    __m256d abnan = _mm256_cmp_pd(a, _mm256_loadu_pd(p+ 4), _CMP_UNORD_Q);
    __m256d c = _mm256_loadu_pd(p+8);
    __m256d cdnan = _mm256_cmp_pd(c, _mm256_loadu_pd(p+12), _CMP_UNORD_Q);
    __m256d abcdnan = _mm256_or_pd(abnan, cdnan);
    return _mm256_movemask_pd(abcdnan);
}
// more aggressive ORing is possible but probably not needed
// especially if you expect any memory bottlenecks.

我把C写得像汇编一样,每个源代码行一条指令。(加载/内存源cmppd)。如果在Intel上使用非索引寻址模式,这6条指令在现代CPU上都是融合域中的单个uoptest/jnz作为一个break条件将使其达到7个uops。

在一个循环中,一个加法规则,16*8 指针增量是另一个 1 uop,而 cmp / jne 作为循环条件是另一个,使其达到 9 uops。所以不幸的是,在 Skylake 上,前端的瓶颈为 4 uops /时钟,至少需要 9/4 个周期才能发出 1 次迭代,加载端口没有完全饱和。Zen 2 或 Ice Lake 每个时钟可以承受 2 个负载,而不会再展开或另一级别的涡旋组合。

另一个可能的技巧是在两个向量上使用vptestvtestpd来检查它们是否都是非零的。但是我不确定是否有可能正确检查两个向量的每个元素是否都是非零的。PTEST可以用来测试两个寄存器是否都是零或其他条件吗?表明另一种方式(_CMP_UNORD_Q输入都是全零)是不可能的。

但这并没有真正的帮助:vtestpd/jcc总共3个uops,而vorpd/vmovmskpd/测试jcc在现有的带有AVX的Intel/AMD CPU上也是3个融合域uops,因此,如果您在结果上进行分支,这甚至不是吞吐量的胜利。因此,即使这是可能的,它也可能收支平衡,尽管它可能会节省一点代码大小。如果需要多个分支才能从全1的情况中排序出全0或混合_zeros_And_ones的情况,则不值得考虑。

如果您的数组是此线程计算的结果,只需检查FP异常粘性标志(在MXCSR中手动,或通过fenv. hfeget

如果已设置,则必须检查;对于未传播到此数组的临时结果,可能引发了无效异常。

如果fenv标志不能让你完全避免工作,或者对你的程序来说不是一个好的策略,试着把这个检查折叠到产生数组的任何东西中,或者折叠到读取它的下一个传递中。所以你在数据已经加载到向量寄存器中的时候重用它,增加了计算强度。(ALU每个加载/存储的工作。)

即使数据在 L1d 中已经很热,它仍然会在负载端口带宽上成为瓶颈:每 cmppd 2 个负载在 2/时钟负载端口带宽上仍然是瓶颈,在具有 2/时钟 vcmppd ymm(Skylake 但不是 Haswell)的 CPU 上。

同样值得调整指针,以确保L1d缓存的满载吞吐量,特别是当L1d中的数据有时已经很热时。

或者至少对它进行缓存分块,以便在同一块上运行另一个循环之前检查一个128kiB的块,而它在缓存中是热的。这是256k L2大小的一半,因此您的数据应该仍然是上一次传递的热点,和/或下一次传递的热点。

绝对避免在一个完整的兆字节数组上运行此程序,并支付从DRAM或L3缓存将其放入CPU核心的成本,然后在另一个循环读取它之前再次逐出。这是最糟糕的计算强度,需要支付将其多次放入CPU核心专用缓存的成本。