【问题标题】:Process unaligned part of a double array, vectorize the rest处理双精度数组的未对齐部分,将其余部分矢量化
【发布时间】:2016-09-28 03:36:59
【问题描述】:

我正在生成 sse/avx 指令,目前我必须使用未对齐的加载和存储。我对浮点/双精度数组进行操作,我永远不会知道它是否会对齐。因此,在对其进行矢量化之前,我希望有一个前循环和可能的后循环,它负责处理未对齐的部分。然后主矢量化循环在对齐的部分上运行。

但是我如何确定数组何时对齐?我可以检查指针值吗?前循环应该什么时候停止,后循环应该什么时候开始?

这是我的简单代码示例:

void func(double * in, double * out, unsigned int size){
    for( as long as in unaligned part ){
        out[i] = do_something_with_array(in[i])
    }
    for( as long as aligned ){
        awesome avx code that loads operates and stores 4 doubles
    }
    for( remaining part of array ){
        out[i] = do_something_with_array(in[i])
    }
 }

编辑: 我一直在考虑。从理论上讲,指向第 i 个元素的指针应该可以被 2、4、16、32 整除(类似于 &a[i]%16==0)(取决于它是否为 double 以及它是 sse 还是 avx)。所以第一个循环应该覆盖不可分割的元素。

实际上我会尝试编译器编译指示和标志,看看编译器会产生什么。如果没有人给出好的答案,我会在周末发布我的解决方案(如果有的话)。

【问题讨论】:

  • 您是否查看过自动矢量化编译器(如gccclang)为处理可能的错位而生成的代码?我怀疑你不太清楚自己在做什么。
  • 是的,将数组衰减为一个指针并对它进行算术运算将帮助您确定它是否在一个很好的对齐地址上。 但是... 检查没有帮助。如果第一个元素的地址没有很好地对齐,那么 all 元素的地址可能是未对齐的,或者至少是许多或大部分元素的地址。一个更好的想法可能是使用动态分配并添加一个初始填充以确保您有一个对齐的“数组”。
  • @JoachimPileborg:我认为问题是关于矢量指令的数据未对齐,在这种情况下,矢量的各个元素是对齐的。
  • @JoachimPileborg - 你所描述的情况不会自然发生,因为数组是根据底层数据类型对齐的 - 所以double 数组无论如何都是 8 字节对齐的。因此,根据 EOF 的评论,它实际上是关于每个单独 SIMD 指令(无论是 128 位还是 256 位或 512 位)中元素的布局。
  • 这是我对这个问题的理解。请注意,在许多情况下,“首选”应改为“必需”——如果您尝试使用未对齐的内存,则会出现大量 AVX 指令段错误。

标签: c++ c x86 vectorization sse


【解决方案1】:

这里有一些示例 C 代码,可以满足您的需求

#include <stdio.h>
#include <x86intrin.h>
#include <inttypes.h>

#define ALIGN 32
#define SIMD_WIDTH (ALIGN/sizeof(double))

int main(void) {
    int n = 17;
    int c = 1;
    double* p = _mm_malloc((n+c) * sizeof *p, ALIGN);
    double* p1 = p+c;
    for(int i=0; i<n; i++) p1[i] = 1.0*i;
    double* p2 = (double*)((uintptr_t)(p1+SIMD_WIDTH-1)&-ALIGN);
    double* p3 = (double*)((uintptr_t)(p1+n)&-ALIGN);
    if(p2>p3) p2 = p3;

    printf("%p %p %p %p\n", p1, p2, p3, p1+n);
    double *t;
    for(t=p1; t<p2; t+=1) {
        printf("a %p %f\n", t, *t);
    }
    puts("");
    for(;t<p3; t+=SIMD_WIDTH) {
        printf("b %p ", t);
        for(int i=0; i<SIMD_WIDTH; i++) printf("%f ", *(t+i));
        puts("");
    }
    puts("");
    for(;t<p1+n; t+=1) {
        printf("c %p %f\n", t, *t);
    }  
}

这会生成一个 32 字节对齐的缓冲区,然后将其大小偏移一倍,因此它不再是 32 字节对齐的。它循环标量值直到达到 32 字节对齐,循环遍历 32 字节对齐的值,然后最后以另一个标量循环结束任何不是 SIMD 宽度倍数的剩余值。


我认为这种优化只对 Nehalem 之前的 Intel x86 处理器真正有意义。由于 Nehalem,未对齐的加载和存储的延迟和吞吐量与对齐的加载和存储相同。此外,由于 Nehalem,高速缓存行拆分的成本很小。

SSE 有一个微妙的点,因为 Nehalem 在未对齐的加载和存储中不能与其他操作折叠。因此,自 Nehalem 以来,对齐的加载和存储并没有被 SSE 淘汰。因此,原则上,即使使用 Nehalem,这种优化也会产生影响,但在实践中,我认为很少有这种情况会发生。

但是,对于 AVX,未对齐的加载和存储可以折叠,因此对齐的加载和存储指令已过时。


I looked into this with GCC, MSVC, and Clang。 GCC如果它不能假设一个指针是对齐的,例如16 字节的 SSE 然后它将生成类似于上述代码的代码以达到 16 字节对齐以避免向量化时缓存行拆分。

Clang 和 MSVC 不这样做,因此它们会受到缓存行拆分的影响。但是,执行此操作的额外代码成本弥补了缓存行拆分的成本,这可能解释了为什么 Clang 和 MSVC 不担心它。

唯一的例外是在 Nahalem 之前。在这种情况下,当指针未对齐时,GCC 比 Clang 和 MSVC 快得多。如果指针是对齐的并且 Clang 知道它,那么它将使用对齐的加载和存储,并且像 GCC 一样快速。 MSVC 向量化仍然使用未对齐的存储和加载,因此即使指针是 16 字节对齐的,在 Nahalem 之前也很慢。


这是一个我认为使用指针差异更清晰的版本

#include <stdio.h>
#include <x86intrin.h>
#include <inttypes.h>

#define ALIGN 32
#define SIMD_WIDTH (ALIGN/sizeof(double))

int main(void) {
    int n = 17, c =1;

    double* p = _mm_malloc((n+c) * sizeof *p, ALIGN);
    double* p1 = p+c;
    for(int i=0; i<n; i++) p1[i] = 1.0*i;
    double* p2 = (double*)((uintptr_t)(p1+SIMD_WIDTH-1)&-ALIGN);
    double* p3 = (double*)((uintptr_t)(p1+n)&-ALIGN);
    int n1 = p2-p1, n2 = p3-p2;
    if(n1>n2) n1=n2;
    printf("%d %d %d\n", n1, n2, n);

    int i;
    for(i=0; i<n1; i++) {
        printf("a %p %f\n", &p1[i], p1[i]);
    }
    puts("");
    for(;i<n2; i+=SIMD_WIDTH) {
        printf("b %p ", &p1[i]);
        for(int j=0; j<SIMD_WIDTH; j++) printf("%f ", p1[i+j]);
        puts("");
    }
    puts("");
    for(;i<n; i++) {
        printf("c %p %f\n", &p1[i], p1[i]);
    }  
}

【讨论】:

  • 我一定会尝试的。在我看来这很合理。对您的优化讨论的评论。 Afaik 如果您想使用流指令,则需要对齐的加载/存储,例如非临时存储。
  • @hr0m:没错,NT 存储仅在对齐版本中可用。通常你不想要这些,因为它们会强制从缓存中驱逐目标。如果您稍后要重用数据,这很糟糕。 NT 加载在当前微架构上的普通(回写)内存上没有任何不同。在当前的 CPU 上,NT 加载仅在从 WC 内存(例如视频 RAM)加载时才有用。
  • 您没有提到 OP 的主要复杂性:如果输入和输出具有不同的对齐方式,则进行对齐加载和存储的唯一方法是 palignr。 (在 Nehalem 及更高版本上不值得)。
  • @PeterCordes:是的,我知道缓存阻塞。还有比这更好的技术。我有理由使用 NT 商店。这是一个玩具示例(甚至 jacobi 也是)。我要处理的是一组不同的算法,我不能到处应用缓存阻塞。
  • @PeterCordes,我想我的意思是误解了你的意思。您的意思是 enhanced rep mov 仅对大型 memcpy 有用,而 OP 没有这样做,因此对于大型数组非临时存储仍然是正确的选择(假设 OP 的算法受内存带宽限制)。我从未使用过enhanced rep mov,只是读过它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-09-12
  • 2015-06-24
  • 2018-01-24
  • 2017-05-25
  • 1970-01-01
  • 2019-11-10
  • 1970-01-01
相关资源
最近更新 更多