【问题标题】:Multiplying vector by constant using SSE使用 SSE 将向量乘以常数
【发布时间】:2011-07-13 12:50:46
【问题描述】:

我有一些在 4D 向量上运行的代码,我目前正在尝试将其转换为使用 SSE。我在 64b linux 上同时使用 clang 和 gcc。
仅对向量进行操作都可以很好地掌握。但现在有一部分我必须将整个向量乘以一个常数 - 像这样:

float y[4];
float a1 =   25.0/216.0;  

for(j=0; j<4; j++){  
    y[j] = a1 * x[j];  
} 

到这样的事情:

float4 y;
float a1 =   25.0/216.0;  

y = a1 * x;  

在哪里:

typedef double v4sf __attribute__ ((vector_size(4*sizeof(float)))); 

typedef union float4{
    v4sf v;
    float x,y,z,w;
} float4;

这当然行不通,因为我正在尝试对不兼容的数据类型进行乘法运算。
现在,我可以这样做:
float4 a1 = (v4sf){25.0/216.0, 25.0/216.0, 25.0/216.0, 25.0/216.0} 但只是让我觉得很傻,即使我写了一个宏来做到这一点。 另外,我很确定这不会产生非常高效的代码。

谷歌搜索没有得到明确的答案(见Load constant floats into SSE registers)。

那么,将整个向量乘以同一个常数的最佳方法是什么?

【问题讨论】:

  • 我认为你需要在上面s/double/float/g - SSE 向量可以容纳 4 个浮点数或 2 个双精度数。

标签: c gcc clang sse simd


【解决方案1】:

只需使用内在函数并让编译器处理它,例如

__m128 vb = _mm_set_ps(1.0f, 2.0f, 3.0f, 4.0f); // vb = { 1.0, 2.0, 3.0, 4.0 }
__m128 va = _mm_set1_ps(25.0f / 216.0f); // va = { 25.0f / 216.0f, 25.0f / 216.0f, 25.0f / 216.0f, 25.0f / 216.0f }
__m128 vc = _mm_mul_ps(va, vb); // vc = va * vb

如果您查看生成的代码,它应该非常高效 - 25.0f / 16.0f 值将在编译时计算,_mm_set1_ps 生成通常会生成相当有效的代码来喷溅矢量。

另请注意,您通常只初始化一次常量向量,例如 va,然后进入将执行大部分实际工作的循环,因此它往往不是性能关键。

【讨论】:

  • 那么我如何访问__m128 的单个元素?我应该使用像v4sf 这样的联合吗?
  • @Emanuel:您通常不想这样做 - SIMD 的全部意义在于使用例如顺序访问大量连续元素。 _mm_load_ps_mm_store_ps 加上循环中需要的任何逻辑/算术 SIMD 操作。但在极少数情况下,您需要操纵单个元素,那么可以,使用联合。
  • @Emanuel:尝试使用 SIMD 操作来执行此操作,而不是提取标量值并对其进行测试,否则您可能会遇到性能瓶颈。例如。使用 SIMD 比较并屏蔽您不感兴趣的元素。如果您在新问题中发布标量代码,那么我和其他人也许可以指导您对其进行矢量化。
  • @PaulR,我刚刚意识到您不久前回答了这个问题。从那时起,GCC 可能发生了很多变化,但我今天刚刚发布了我认为对 GCC 来说更好的答案。
  • @Jedi:在这种情况下,“喷溅”意味着将向量的所有元素设置为相同的值,通常是将一个元素复制到所有其他元素。该术语来自 AltiVec(PowerPC SIMD 架构),它有一个vec_splat 指令/内在函数。
【解决方案2】:

没有理由为此必须使用内在函数。 OP只想进行广播。这与 SIMD 添加一样基本的 SIMD 操作。任何体面的 SIMD 库/扩展都必须支持广播。 Agner Fog 的矢量类确实如此,OpenCL 如此,GCC documention 清楚地表明它确实如此。

a = b + 1;    /* a = b + {1,1,1,1}; */
a = 2 * b;    /* a = {2,2,2,2} * b; */

下面的代码编译得很好

#include <stdio.h>
int main() {     
    typedef float float4 __attribute__ ((vector_size (16)));

    float4 x = {1,2,3,4};
    float4 y = (25.0f/216.0f)*x;
    printf("%f %f %f %f\n", y[0], y[1], y[2], y[3]);
    //0.115741 0.231481 0.347222 0.462963
}

您可以在http://coliru.stacked-crooked.com/a/de79cca2fb5d4b11查看结果

将该代码与内部代码进行比较,就可以清楚地看出哪一个更具可读性。它不仅更具可读性,而且更容易移植到例如ARM 霓虹灯。它看起来也非常类似于 OpenCL C 代码。

【讨论】:

  • 另一方面,为了移植到其他编译器,例如ICC、MSVC、旧版本的 gcc 等,内在函数是更好的选择,即使它们的可读性较差 - 选择取决于您的特定应用程序和可移植性要求。
  • Well Clang 支持 GCC 扩展(以及其他)。但我认为最好的办法是使用 Agner Fog 的矢量类。它适用于所有这些编译器。然后你也会得到更简洁的代码。太糟糕了 C/C++ 目前还没有采用 simd 类型(例如 OpenCL 中的 float4)作为基本类型。它应该是一个基本类型,就像 float 和 double 一样,因为现在大多数硬件都支持它作为基本类型。
  • 除了编译器支持之外,您还需要考虑许多重要的内在函数不能很好地映射到更通用的模型,例如_mm_madd_epi16。如果您正在做相对简单直接的事情,特别是如果它只是浮点/双精度,那么很快某种抽象层或编译器扩展可能是一个好方法,但这不是万能药 - 有很多用例使用内在函数 (甚至 asm) 是更好的选择。
  • 是的,我完全同意这一点。虽然到目前为止,Vector 类无法完成我想要的事情的情况很少,但mm_madd_ep16 就是其中之一。
  • Agner Fog's VCL 与 Intel 内在函数兼容,无需额外转换,只需在 Vec8s 上使用 _mm_madd_epi16 并将结果分配给另一个 Vec8s;它具有运算符重载和复制构造函数,可以在没有语法痛苦的情况下完成这项工作。使用 GNU C 本机向量时,您可能需要显式转换为 __m128i,但 __m128i 是根据(long long)本机向量定义的,因此应该是可能的。
【解决方案3】:

这可能不是最好的方法,但这是我在 SSE 涉足时采用的方法。

float4 scale(const float s, const float4 a)
{
  v4sf sv = { s, s, s, 0.0f };
  float4 r = { .v = __builtin_ia32_mulps(sv, a.v) };
  return r;
}

float4 y;
float a1;

y = scale(a1, y);

【讨论】:

    猜你喜欢
    • 2012-02-23
    • 2011-09-30
    • 2021-03-25
    • 1970-01-01
    • 1970-01-01
    • 2019-04-03
    • 2016-02-22
    • 2011-07-05
    • 1970-01-01
    相关资源
    最近更新 更多