【问题标题】:C++ SSE Intrinsics: Storing results in variables [closed]C ++ SSE Intrinsics:将结果存储在变量中[关闭]
【发布时间】:2019-03-18 12:25:41
【问题描述】:

我无法理解如何使用 SSE 内在函数将一些 SIMD 计算的结果存储回“正常变量”。例如 _mm_store_ps 内在函数在“Intel Intrinsics Guide”中描述如下:

void _mm_store_ps (float* mem_addr, __m128 a)

存储 128 位(由 4 个打包单精度(32 位)组成) 浮点元素)从 a 到内存。 mem_addr 必须对齐 在 16 字节边界上或一般保护异常可能是 生成。

第一个参数是一个指针,指向一个大小为 32 位的浮点数。但描述指出,内在函数会将 128 位从 a 复制到目标 mem_addr。

  • mem_addr 是否需要为 4 个浮点数的数组?
  • 如何仅访问 a 中的特定 32 位元素并将其存储在单个浮点数中?
  • 我在概念上缺少什么?
  • 还有比 _mm_store_ps 内在函数更好的选择吗?

这是一个简单的结构,其中 doSomething() 将 1 加到结构的 x/y 上。缺少的是关于如何将结果存储回 x/y 的部分,而仅使用较高的 32 位宽元素 2 和 3,而未使用 1 和 0。

struct vec2 {
   union {
         struct {
            float data[2];
         };
         struct {
            float x, y;
         };
      };

   void doSomething() {
      __m128 v1 = _mm_setr_ps(x, y, 0, 0);
      __m128 v2 = _mm_setr_ps(1, 1, 0, 0);
      __m128 result = _mm_add_ps(v1, v2);
      // ?? How to store results in x,y ??
   }
}

【问题讨论】:

  • 使用 _mm_store_sd 对向量的低半部分进行 64 位存储。或_mm_storel_pi (movlps)。除了_mm_setr,您可以使用_mm_load_sd((float*)&vec.x) 进行零扩展为128 位向量的64 位加载。
  • mem_addr 不需要声明为float[],但需要正确对齐,这可以通过_mm_mallocaligned_malloc 等对齐分配来完成,尽管@987654332 @ 应该已经分配给 alignof(std::max_align_t) 字节。如果数据不是动态分配的,那么应该使用alignas 关键字(例如在vec2 类型的data 字段上)。

标签: c++ sse simd intrinsics


【解决方案1】:

这是一个 128 位的加载或存储,所以是的,arg 类似于 float mem[4]。请记住,在 C 中,将数组传递给函数/内部函数与传递指针相同。

英特尔的内在函数有些特殊,因为它们不遵循正常的严格别名规则,至少对于整数而言。 (例如_mm_loadu_si128((const __m128i*)some_pointer) 不违反严格别名,即使它是指向long 的指针。我认为这同样适用于浮动/双重加载/存储内在函数,因此您可以安全地使用它们从/到加载/存储随便你。通常你会使用 _mm_load_ps 来加载单精度 FP 位模式,但通常你会将它们保存在 float 类型的 C 对象中。

如何仅访问 a 中的特定 32 位元素并将其存储在单个浮点数中?

使用矢量洗牌,然后_mm_cvtss_f32 将矢量转换为标量。


加载/存储 64 位

理想情况下,您可以同时操作 2 个向量,或者一个 X 值数组和一个 Y 值数组,因此对于一对向量,您将获得 4 对 XY 坐标的 X 和 Y 值。 (数组结构而不是结构数组)。

但是你可以像这样有效地表达你想要做的事情:

struct vec2 {
    float x,y;
};

void foo(const struct vec2 *in, struct vec2 *out) {
    __m128d tmp = _mm_load_sd( (const double*)in );  //64-bit zero-extending load with MOVSD
    __m128  inv = _mm_castpd_ps(tmp);             // keep the compiler happy
    __m128  result = _mm_add_ps(inv,  _mm_setr_ps(1, 1, 0, 0) );

    _mm_storel_pi( out, result );
}

GCC 8.2 compiles it like this (on Godbolt),对于 x86-64 System V,奇怪的是使用 movq 而不是 movsd 进行负载。 gcc 6.3 使用movsd

foo(vec2 const*, vec2*):
        movq    xmm0, QWORD PTR [rdi]           # 64-bit integer load
        addps   xmm0, XMMWORD PTR .LC0[rip]     # packed 128-bit float add
        movlps  QWORD PTR [rsi], xmm0           # 64-bit store
        ret

对于向量低半部分的 64 位存储(2 个floats 或 1 个double),您可以使用_mm_store_sd。或者更好的_mm_storel_pi (movlps)。不幸的是,它的内在函数需要 __m64* arg 而不是 float*,但这只是英特尔内在函数的设计怪癖。它们通常需要类型转换。

请注意,我使用 _mm_load_sd((const double*)&(in->x)) 代替 _mm_setr 进行 64 位加载,该加载将零扩展为 128 位向量。您不想要 movlps 加载,因为它会合并到现有向量中。这将对之前的任何值产生错误的依赖,并花费额外的 ALU uop。

【讨论】:

  • 感谢您的详细解释并花时间这样做。在_mm_cvtss_f32之前也没有想过要洗牌。
  • 另请注意,asm 中的浮点标量只是 XMM 寄存器底部的值,与通常在 GP 整数寄存器中的标量 int 不同。所以_mm_cvtss_f32 是免费的,而_mm_cvtsi128_si32 通常是movd eax, xmm0(或直接存储到内存中,具体取决于您对 int 所做的操作。)
  • 还相关:Is `reinterpret_cast`ing between hardware SIMD vector pointer and the corresponding type an undefined behavior? - 即使像 _mm_load_sd(const double*) 这样的内在函数也应该是严格混叠安全的,即您可以使用它来加载两个浮点数。上次我测试时,它实际上在大多数编译器中都是安全的。
猜你喜欢
  • 2021-06-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多