我想在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具有收集支持,而AVX512具有分散支持,但是引入索引数组会使SIMD变得更糟。

有关在三角形矩阵上循环以及将i,j转换为线性的信息,请参见algorithm of addressing a triangle matrices memory using assembly。 (您可能希望填充索引,以便每行从32字节对齐边界处开始。即,将每行的长度四舍五入为8个float元素的整数倍,即整个AVX向量。这也使遍历a具有AVX向量的矩阵:您可以将垃圾存储到行尾的填充中,而不是让行的最后一个向量从下一行的开始就包含一些元素。)

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



如果您确实需要这样做:

对于大数组,它很好地分解为整个向量,仅在行的末尾变得棘手。

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

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


对于较大的三角形,我们存储的是相同编号的大行程(例如memset),然后是下一个数字的较短行程,以此类推。也就是说,存储整行的0很容易。对于除最后几行以外的所有行,这些游程都大于1个向量元素。

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


同样,在单行中,这是一个简单的矢量化模式。要存储递增的序列,请从{1,2,3,4}向量开始,然后使用{4,4,4,4}_mm_set1_epi32(1)的SIMD加法将其递增。对于256位AVX2向量,使用_mm256_set1_epi32(8)将8元素向量增加8。

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

编译器已经可以相当不错地自动对函数进行矢量化处理,尽管行尾处理比手工处理要复杂得多。 With your code on the Godbolt compiler explorer(使用__restrict告诉编译器输出数组不重叠,使用__builtin_assume_aligned告诉编译器它们对齐),我们得到了这样一个内部循环(来自gcc):

.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)


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

计算下一个外循环迭代的起始向量可以通过以下方法完成:

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


即通过添加所有矢量将{0,0,0,0,...}变为{1,1,1,1,...}。通过添加所有矢量将{1,2,3,4,5,6,7,8}变为{2,3,4,5,6,7,8,9}

10-04 13:28