当前位置: 首页 > 知识库问答 >
问题:

带SSE的并行前缀(累计)和

吴伟志
2023-03-14

我正在寻找一些关于如何使用SSE进行并行前缀和的建议。我对在int、float或double数组上执行此操作感兴趣。

我想出了两个解决方案。特殊情况和一般情况。在这两种情况下,解决方案都与OpenMP并行在阵列上运行两次。对于特殊情况,我在两个过程中都使用SSE。对于一般情况,我只在第二遍使用它。

我的主要问题是,在一般情况下,如何在第一次通过时使用SSE?英特尔cpu上的以下链接simd前缀和显示了字节数的改进,但不适用于32位数据类型。

特殊情况称为特殊的原因是它要求数组采用特殊格式。例如,让我们假设数组a浮点数只有16个元素。然后如果数组像这样重新排列(结构数组到数组结构):

a[0] a[1] ...a[15] -> a[0] a[4] a[8] a[12] a[1] a[5] a[9] a[13]...a[3] a[7] a[11] a[15]

SSE垂直总和可用于两个过程。然而,只有当数组已经是特殊格式并且输出可以使用特殊格式时,这才是有效的。否则,必须对输入和输出进行代价高昂的重新排列,这将使其比一般情况慢得多。

也许我应该考虑前缀和的不同算法(例如二叉树)?

一般情况下的代码

void prefix_sum_omp_sse(double a[], double s[], int n) {
    double *suma;
    #pragma omp parallel
    {
        const int ithread = omp_get_thread_num();
        const int nthreads = omp_get_num_threads();
        #pragma omp single
        {
            suma = new double[nthreads + 1];
            suma[0] = 0;
        }
        double sum = 0;
        #pragma omp for schedule(static) nowait //first parallel pass
        for (int i = 0; i<n; i++) {
            sum += a[i];
            s[i] = sum;
        }
        suma[ithread + 1] = sum;
        #pragma omp barrier
        #pragma omp single
        {
            double tmp = 0;
            for (int i = 0; i<(nthreads + 1); i++) {
                tmp += suma[i];
                suma[i] = tmp;
            }
        }
        __m128d offset = _mm_set1_pd(suma[ithread]);
        #pragma omp for schedule(static) //second parallel pass with SSE as well
        for (int i = 0; i<n/4; i++) {       
            __m128d tmp1 = _mm_load_pd(&s[4*i]);
            tmp1 = _mm_add_pd(tmp1, offset);    
            __m128d tmp2 = _mm_load_pd(&s[4*i+2]);
            tmp2 = _mm_add_pd(tmp2, offset);
            _mm_store_pd(&s[4*i], tmp1);
            _mm_store_pd(&s[4*i+2], tmp2);
        }
    }
    delete[] suma;
}

共有1个答案

公西永嘉
2023-03-14

这是我第一次回答自己的问题,但这似乎很合适。基于hirschhornsalz对intel cpu上16字节simd前缀和的前缀和的回答,我提出了一种在4、8和16个32位字的第一遍使用simd的解决方案

一般理论如下。对于n个单词的顺序扫描,需要添加(n-1扫描n个单词,从之前扫描的一组单词中再添加一个)。但是,使用SIMD时,n个字可以在对数加法和相等数量的移位加上一次加法中进行扫描,并从上一次SIMD扫描中进行广播。因此,对于某些值n,SIMD方法将获胜。

让我们看看使用SSE、AVX和AVX-512的32位单词:

4 32-bit words (SSE):      2 shifts, 3 adds, 1 broadcast       sequential: 4 adds
8 32-bit words (AVX):      3 shifts, 4 adds, 1 broadcast       sequential: 8 adds
16 32 bit-words (AVX-512): 4 shifts, 5 adds, 1 broadcast       sequential: 16 adds

基于此,在AVX-512之前,SIMD似乎对32位字的扫描没有用处。这还假设移位和广播只能在一条指令中完成。这对SSE来说是正确的,但对AVX来说不是这样,甚至可能对AVX2来说也不是这样。

无论如何,我将一些工作和测试的代码放在一起,这些代码使用SSE进行前缀和。

inline __m128 scan_SSE(__m128 x) {
    x = _mm_add_ps(x, _mm_castsi128_ps(_mm_slli_si128(_mm_castps_si128(x), 4))); 
    x = _mm_add_ps(x, _mm_castsi128_ps(_mm_slli_si128(_mm_castps_si128(x), 8)));
    return x;
}

void prefix_sum_SSE(float *a, float *s, const int n) {
__m128 offset = _mm_setzero_ps();
for (int i = 0; i < n; i+=4) {
    __m128 x = _mm_load_ps(&a[i]);
    __m128 out = scan_SSE(x);
    out = _mm_add_ps(out, offset);
    _mm_store_ps(&s[i], out);
    offset = _mm_shuffle_ps(out, out, _MM_SHUFFLE(3, 3, 3, 3)); 
}

请注意,scan\u SSE功能有两个加法(\u mm\u add\u ps)和两个移位(\u mm\u slli\u si128)。强制转换只用于使编译器满意,而不会转换为指令。然后在prefix\u sum\u SSE中的数组主循环中,使用另一个加法和一个随机数。这总共是6次运算,而顺序和只有4次加法。

以下是AVX的工作解决方案:

inline __m256 scan_AVX(__m256 x) {
    __m256 t0, t1;
    //shift1_AVX + add
    t0 = _mm256_permute_ps(x, _MM_SHUFFLE(2, 1, 0, 3));
    t1 = _mm256_permute2f128_ps(t0, t0, 41);
    x = _mm256_add_ps(x, _mm256_blend_ps(t0, t1, 0x11));
    //shift2_AVX + add
    t0 = _mm256_permute_ps(x, _MM_SHUFFLE(1, 0, 3, 2));
    t1 = _mm256_permute2f128_ps(t0, t0, 41);
    x = _mm256_add_ps(x, _mm256_blend_ps(t0, t1, 0x33));
    //shift3_AVX + add
    x = _mm256_add_ps(x,_mm256_permute2f128_ps(x, x, 41));
    return x;
}

void prefix_sum_AVX(float *a, float *s, const int n) {
    __m256 offset = _mm256_setzero_ps();
    for (int i = 0; i < n; i += 8) {
        __m256 x = _mm256_loadu_ps(&a[i]);
        __m256 out = scan_AVX(x);
        out = _mm256_add_ps(out, offset);
        _mm256_storeu_ps(&s[i], out);
        //broadcast last element
        __m256 t0 = _mm256_permute2f128_ps(out, out, 0x11);
        offset = _mm256_permute_ps(t0, 0xff);
    }   
}

三班制需要7个内在要素。广播需要两个内在因素。所以加上4个附加项,这就是13个内部变量。对于AVX2,移位只需要5个内部变量,因此总共需要11个内部变量。顺序和只需要8个加法。因此,AVX和AVX2可能都不适用于第一次通过。

编辑:

因此,我最终对此进行了基准测试,结果出乎意料。SSE和AVX代码的速度都是以下顺序代码的两倍:

void scan(float a[], float s[], int n) {
    float sum = 0;
    for (int i = 0; i<n; i++) {
        sum += a[i];
        s[i] = sum;
    }
}

我想这是由于教学水平的平行性。

所以这回答了我自己的问题。在一般情况下,我成功地将SIMD用于pass1。当我在我的4核常春藤桥系统上将其与OpenMP结合时,512k浮动的总速度约为7。

 类似资料:
  • 问题内容: 我正在和熊猫一起工作,但是我没有太多经验。我有以下DataFrame: 而且我需要计算前11行的累积总和。如果先前的数量少于11,则将剩余的数量假定为0。 我试过了: 但是,这并没有实现我想要的,但是这正在旋转累积总和的结果。我该如何实现? 问题答案: 呼叫与和和:

  • 问题内容: 如何删除所有都有前缀的表? 注意:需要在phpMyAdmin中执行 问题答案: 您不能仅使用单个MySQL命令来完成此操作,但是可以使用MySQL为您构造该语句: 在MySQL Shell中或通过PHPMyAdmin,使用以下查询 这将生成一个DROP语句,然后您可以复制并执行该语句以删除表。 编辑:此处免责声明- 上面生成的语句将删除所有带有该前缀的数据库中的所有表。如果要将其限制为

  • 问题内容: 我有一个名为“ seeder”的软件包: 现在我想用MyFunc前缀调用所有函数 我想要这样的东西: 这个输出: EDIT1 :在此示例中,parentKey是在循环中更改的字符串变量 但是GC说: 使用没有选择器的包播种机 问题答案: 您无法通过函数名称获得函数,而这正是您想要做的。原因是,如果Go工具可以检测到未显式引用某个函数(因此无法访问该函数),则该函数甚至可能无法编译为可执

  • 我对spring boot和创建我的第一个应用程序非常陌生。创建数据源时,我使用了带有前缀的@ConfigurationProperties和要从Application.Property中读取的属性。 但是,这个设置似乎对我不起作用,我的程序没有运行。 我的pom.xml文件包含: 我的存储库类: 我的主要类: 请让我知道如果我需要提供任何其他信息以及。

  • 问题内容: 说我有一段距离。 我想从x到达总和达到10的索引,在这种情况下,idx = [4,9]。 因此,满足条件后,cumsum重新启动。 我可以使用循环来完成此操作,但是对于大型阵列而言,循环速度很慢,我想知道是否可以用某种方式来执行。 问题答案: 这是一个带有numba和数组初始化的代码- 时机 包括并使用同一篇文章中的基准测试设置- Numba:追加与数组初始化 为了更仔细地了解数组初始

  • 我正在尝试利用Laravel的登录页路径。例如,默认情况下,Laravel会将您带到欢迎页面,并且斜杠后没有url文本。 在我的欢迎页面中,我使用以下条件根据路线名称应用样式。 然而,此代码不工作。如何正确查看欢迎页面的路线? 编辑:使用“请求”而不是“路由”使其工作。然而,为了保持一致性,我想知道是否也可以使用“路线”来完成。