10

我正在尝试将以下代码转换为 SSE/AVX:

float x1, x2, x3;
float a1[], a2[], a3[], b1[], b2[], b3[];
for (i=0; i < N; i++)
{
    if (x1 > a1[i] && x2 > a2[i] && x3 > a3[i] && x1 < b1[i] && x2 < b2[i] && x3 < b3[i])
    {
        // do something with i
    }
}

这里 N 是一个很小的常数,比如说 8。 if(...) 语句在大多数情况下的计算结果为 false。

第一次尝试:

__m128 x; // x1, x2, x3, 0
__m128 a[N]; // packed a1[i], a2[i], a3[i], 0 
__m128 b[N]; // packed b1[i], b2[i], b3[i], 0

for (int i = 0; i < N; i++)
{
    __m128 gt_mask = _mm_cmpgt_ps(x, a[i]);
    __m128 lt_mask = _mm_cmplt_ps(x, b[i]);
    __m128 mask = _mm_and_ps(gt_mask, lt_mask);
    if (_mm_movemask_epi8 (_mm_castps_si128(mask)) == 0xfff0)
    {
        // do something with i
    }
}

这有效,并且相当快。问题是,有没有更有效的方法来做到这一点?特别是,如果有一个寄存器包含 SSE 或 AVX 比较浮点数(放置0xffff0x0000放入该插槽)的结果,那么所有比较的结果如何(例如)与 -ed 或 or-ed 在一起,一般来说? PMOVMSKB(或相应的内在)是_mm_movemask执行此操作的标准方法吗?

另外,如何在上面的代码中使用 AVX 256 位寄存器而不是 SSE?

编辑:

使用 VPTEST(来自 _mm_test* 内在)对版本进行测试和基准测试,如下所示。

__m128 x; // x1, x2, x3, 0
__m128 a[N]; // packed a1[i], a2[i], a3[i], 0
__m128 b[N]; // packed b1[i], b2[i], b3[i], 0
__m128i ref_mask = _mm_set_epi32(0xffff, 0xffff, 0xffff, 0x0000);

for (int i = 0; i < N; i++)
{
    __m128 gt_mask = _mm_cmpgt_ps(x, a[i]);
    __m128 lt_mask = _mm_cmplt_ps(x, b[i]);
    __m128 mask = _mm_and_ps(gt_mask, lt_mask);
    if (_mm_testc_si128(_mm_castps_si128(mask), ref_mask))
    {
        // do stuff with i
    }
}

这也有效,而且速度很快。对此(Intel i7-2630QM、Windows 7、cygwin 1.7、cygwin gcc 4.5.3 或 mingw x86_64 gcc 4.5.3、N=8)进行基准测试表明,这与 64 位上的上述代码速度相同(小于 0.1%) . 内部循环的任一版本平均运行大约 6.8 个时钟,这些数据都在缓存中,并且比较总是返回 false。

有趣的是,在 32 位上,_mm_test 版本的运行速度慢了大约 10%。事实证明,编译器在循环展开后会溢出掩码,并且必须重新读取它们;这可能是不必要的,并且可以在手动编码组装中避免。

选择哪种方法?似乎没有令人信服的理由更VPTEST喜欢VMOVMSKPS. 实际上,有一个更喜欢的理由VMOVMSKPS,即它释放了一个 xmm 寄存器,否则该寄存器会被掩码占用。

4

2 回答 2

12

如果您正在使用浮点数,您通常希望使用MOVMSKPS(和相应的 AVX 指令VMOVMSKPS)而不是PMOVMSKB.

除此之外,是的,这是执行此操作的一种标准方式;您还可以使用PTEST( VPTEST) 根据 SSE 或 AVX AND 或 ANDNOT 的结果直接更新条件标志。

于 2012-11-21T13:23:58.983 回答
2

要解决您的编辑版本:

如果您要直接对 的结果进行分支PTEST,则使用它比使用MOVMSKPSGP reg 更快,然后对其执行 aTEST以设置分支指令的标志。在 AMD CPU 上,在向量域和整数域之间移动数据非常慢(5 到 10 个周期延迟,具体取决于 CPU 型号)。

至于需要额外的注册PTEST,您通常不需要。您可以使用与两个 args 相同的值,就像使用常规的非向量TEST指令一样。(测试foo & foo与测试相同foo)。

在您的情况下,您确实需要检查是否设置了所有矢量元素。如果您反转比较,然后将结果组合在一起(因此您正在测试!(x1 < a1[i]) || !(x2 < a2[i]) || ...),您将拥有需要测试全零而不是全零的向量。但是处理低元素还是有问题的。如果您需要保存一个寄存器以避免需要PTEST/的向量掩码,您可以在执行 a并将其分支为全零VTESTPS之前将向量右移 4 个字节。PTEST

AVX 引入VTESTPS,我猜这避免了可能的 float -> int 绕过延迟。但是,如果您使用任何 int-domain 指令来生成测试输入,那么您不妨使用(V)PTEST. (我知道您使用的是内在函数,但与助记符相比,它们很难输入和查看。)

于 2015-06-09T02:27:18.833 回答