如何使用GNU C Vector Extensions加载/存储双精度数组



我使用GNU C向量扩展,而不是英特尔的_mm_* intrinsic。

我想做与英特尔的_m256_loadu_pd内在相同的事情。一个接一个赋值很慢:gcc生成的代码有4条加载指令,而不是一个单独的vmovupd (_m256_loadu_pd确实生成)。

typedef double vector __attribute__((vector_size(4 * sizeof(double))));
int main(int argc, char **argv) {
    double a[4] = {1.0, 2.0, 3.0, 4.0};
    vector v;
    /* I currently do this */
    v[0] = a[0];
    v[1] = a[1];
    v[2] = a[2];
    v[3] = a[3];
}

我想要这样的东西:

v = (vector)(a);

v = *((vector*)(a));

,但都不起作用。第一个以"无法将值转换为向量"失败,而第二个则导致段错误。

update:我看到你使用的是GNU C的原生向量语法,而不是Intel的intrinsic。您是否为了可移植到非x86而避免使用Intel的固有特性?gcc目前在编译使用GNU C向量的代码时做得很差,超出了目标机器所支持的范围。(您可能希望它只使用两个128b向量并分别对每个向量进行操作,但显然情况比这更糟。)

无论如何,这个答案展示了如何使用Intel x86 intrinsic将数据加载到GNU C向量语法类型


首先,如果你想学习什么可以编译成好的代码,看看编译器的输出小于-O2是浪费时间。您的main()将在-O2下优化为ret

除此之外,每次给一个vector对象赋值会得到不好的asm也就不足为奇了。


旁白:一般人会称其为v4df (vector of 4 Double Float)或其他类型,而不是vector,所以当他们在c++ std::vector中使用它时不会发疯。对于单精度,v8sf。IIRC, gcc在__m256d内部使用这样的类型名称。

在x86上,Intel的内在类型(如__m256d)是在GNU C向量语法之上实现的(这就是为什么你可以在GNU C中做v1 * v2而不是写_mm256_mul_pd(v1, v2))。您可以自由地从__m256d转换为v4df,就像我在这里所做的那样。

我已经在函数中包装了这两种相同的方法,所以我们可以看看它们的asm。请注意,我们没有从同一个函数中定义的数组中加载,因此编译器不会将其优化掉。

我把它们放在Godbolt编译器资源管理器上,这样你就可以用各种编译选项和编译器版本查看asm。

typedef double v4df __attribute__((vector_size(4 * sizeof(double))));
#include <immintrin.h>
// note the return types.  gcc6.1 compiles with no warnings, even at -Wall -Wextra
v4df load_4_doubles_intel(const double *p) { return _mm256_loadu_pd(p); }
    vmovupd ymm0, YMMWORD PTR [rdi]   # tmp89,* p
    ret
v4df avx_constant() { return _mm256_setr_pd( 1.0, 2.0, 3.0, 4.0 ); }
    vmovapd ymm0, YMMWORD PTR .LC0[rip]
    ret

如果_mm_set* intrinsic的参数不是编译时常量,编译器将尽其所能编写有效的代码,将所有元素放入单个vector中。通常最好这样做,而不是编写将存储到tmp数组并从中加载的C语言,因为这并不总是最好的策略。多个窄存储转发失败,转发到宽负载时,除了通常的存储转发延迟外,还需要额外的10个周期(IIRC)的延迟。如果您的double已经在寄存器中,通常最好只是将它们洗牌在一起。


参见是否有可能将浮点数直接转换为__m128,如果它们是16字节分配?获取将单个标量转化为向量的各种本征函数的列表。x86标签wiki有Intel手册的链接,以及它们的内在查找器。


加载/存储没有Intel intrinsic的GNU C vector:

我不知道你"应该"怎么做。这个Q&A建议转换一个指向你想要加载的内存的指针,并使用像typedef char __attribute__ ((vector_size (16),aligned (1))) unaligned_byte16;这样的向量类型(注意aligned(1)属性)。

你从*(v4df *)a得到一个段错误,因为a可能没有在32字节的边界上对齐,但是你使用的是一个假设自然对齐的向量类型。(就像__m256d一样,如果你对指向它的指针解引用,而不是使用load/store内部函数向编译器传达对齐信息。)

你可以在x86上使用gcc的等效内部函数:__builtin_ia32_loadupd256 (https://gcc.gnu.org/onlinedocs/gcc/x86-Built-in-Functions.html#x86-Built-in-Functions)。

比如:

typedef double v4df __attribute__((vector_size(4 * sizeof(double))));
void vector_copy(double *a, v4df *v)
{
    *v = __builtin_ia32_loadupd256(a);
}

如果您不需要获取a的副本,则使用指针代替(参见示例中的v_ptr)。如果需要拷贝,使用memmove(参见v_copy)

#include <stdio.h>
#include <string.h>
typedef double vector __attribute__((vector_size(4 * sizeof(double))));
int main(int argc, char **argv) {
  double a[4] = {1.0, 2.0, 3.0, 4.0};
  vector *v_ptr;
  vector v_copy;
  v_ptr = (vector*)&a;
  memmove(&v_copy, a, sizeof(a));
  printf("a[0] = %f // v[0] = %f // v_copy[0] = %fn", a[0], (*v_ptr)[0], v_copy[0]);
  printf("a[2] = %f // v[2] = %f // v_copy[0] = %fn", a[2], (*v_ptr)[2], v_copy[2]);
  return 0;
}
输出:

a[0] = 1.000000 // v[0] = 1.000000 // v_copy[0] = 1.000000
a[2] = 3.000000 // v[2] = 3.000000 // v_copy[0] = 3.000000

最新更新