c++ - 在 C++ 中用 avx 实现 numpy 的 triu_indices

标签 c++ numpy vectorization sse avx

我想在 c++ 中使用 avx 内在函数实现 numpy.triu_indices(a, 1)(注意第二个参数是 1)。下面的代码片段是我想出的代码的非矢量化版本。这里,a是长度(int),first和second是两个输出数组

void triu_indices(int a, int* first, int* second)
{
    int index = 0;
    for (int i=0; i < a; i++)
    {

        for(int j = i+1; j < a; j++)
        {
            first[index] = i;
            second[index] = j;
            index++;
        }

    }
}

作为示例输出,如果我给 a=4,则

first = [0,0,0,1,1,2]
second = [1,2,3,2,3,3]

现在,我想在 AVX2 中完全实现它(即以矢量化方式)。最终,该函数将运行整个整数数组,该数组将为函数提供变量 a,并输出数组 firstsecond 将存储在两个父数组中。

能否请您给我一些有用的提示(或代码片段),说明如何使用显式 AVX2 内参(即,不依赖于编译器自动矢量化)对该函数进行矢量化?对不起,如果这是一个菜鸟问题,因为我最近开始学习 AVX。

最佳答案

首先,确保您确实需要这样做,并且确实希望将索引数组作为最终结果,而不是作为跟踪三角矩阵中数据的一部分。 AVX2 支持 gather,AVX512 支持 scatter,但引入索引数组让 SIMD 变得更糟。

对于三角矩阵的循环,以及 i,j 到线性,参见 algorithm of addressing a triangle matrices memory using assembly . (您可能想要填充索引,以便每一行都从 32 字节对齐的边界开始。即将每一行的长度四舍五入为 8 个浮点元素的倍数,一个完整的 AVX vector 。这也使得循环更容易带有 AVX vector 的矩阵:您可以将垃圾存储到行末尾的填充中,而不是让行的最后一个 vector 包含下一行开头的一些元素。)

对于线性 -> i,j, the closed-form formula包括 sqrt (也是一个 C++ version ),所以数组查找可能会有用,但实际上你应该完全避免这样做。 (例如,如果以打包格式遍历三角矩阵,请跟踪您在 i、j 和线性矩阵中的位置,以便在找到要查找的元素时不需要查找。)


如果你确实需要这个:

对于大型数组,它可以很好地分解成整个 vector ,仅在行的末尾变得棘手。

对于最后一个角的特殊情况,您可以使用预定义的 vector 常量,其中在 4 或 8 个 int 元素的相同 vector 中有多个三角形行。

first = [0,0,0,1,1,2]

对于较大的三角形,我们存储相同数字的大量运行(如 memset),然后是下一个数字的稍短运行,等等。即存储整行 0s 很简单。对于除最后几行以外的所有行,这些运行都大于 1 个 vector 元素。

second = [1,2,3,2,3,3]

同样,在一行中,它是一个简单的矢量化模式。要存储递增序列,从 {1,2,3,4} 的 vector 开始并使用 SIMD 添加 {4,4,4,4} 来增加它,即 _mm_set1_epi32(1)。对于 256 位 AVX2 vector ,_mm256_set1_epi32(8) 将 8 元素 vector 递增 8。

因此,在最内层的循环中,您只需存储一个不变 vector ,然后在另一个 vector 上使用 _mm256_add_epi32 并将其存储到另一个数组。

编译器已经可以自动-相当不错地向量化您的函数,尽管行尾处理比您手动处理要复杂得多。 With your code on the Godbolt compiler explorer (使用 __restrict 告诉编译器输出数组不重叠,使用 __builtin_assume_aligned 告诉编译器它们是对齐的),我们得到这样的内部循环(来自海湾合作委员会):

.L4:                                       # do {
    movups  XMMWORD PTR [rcx+rax], xmm0      # _mm_store_si128(&second[index], xmm0)
    paddd   xmm0, xmm2                       # _mm_add_epi32
    movups  XMMWORD PTR [r10+rax], xmm1      # _mm_store_si128(&second[index], counter_vec)
    add     rax, 16                          # index += 4  (16 bytes)
    cmp     rax, r9
    jne     .L4                            # }while(index != end_row)

如果我有时间,我可能会写得更详细,包括更好地处理行尾。例如在行尾结束的部分重叠存储通常是好的。或者展开外循环,使内循环具有重复的清理模式。

计算下一个外循环迭代的起始 vector 可以通过类似的方式完成:

vsecond = _mm256_add_epi32(vsecond, _mm256_set1_epi32(1));
vfirst  = _mm256_add_epi32(vfirst, _mm256_set1_epi32(1));

即通过添加全 1 vector 将 {0,0,0,0,...} 变为 {1,1,1,1,...}。并将 {1,2,3,4,5,6,7,8} 转换为 {2,3,4,5,6,7,8,9} 添加一个全部为 1 的 vector 。

关于c++ - 在 C++ 中用 avx 实现 numpy 的 triu_indices,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50522132/

相关文章:

c++ - readdir 在挂载的 cifs 目录上花费很长时间

C++ 数组从 512x512 转换为 16x16x32x32

c++ - 重定向 cout -> std::stringstream,没有看到 EOL

python - 从 numpy 数组中删除某些元素而不破坏结构

python - 两个字符串数组的乘积

matlab - 将一个数组嵌入到另一个数组中

python - 将零重新定位到多维 numpy 数组中最后一个维度的末尾

c++ - 如何用字体计算多字符串的宽度和高度?

python - Cython编译错误-赋值前引用的局部变量

python - JAX vmap 行为