【问题标题】:Loading data for GCC's vector extensions为 GCC 的向量扩展加载数据
【发布时间】:2012-03-08 06:06:34
【问题描述】:

GCC 的 vector extensions 提供了一种不错的、合理可移植的方式来访问不同硬件架构上的一些 SIMD 指令,而无需诉诸 hardware specific intrinsics(或自动矢量化)。

一个真正的用例是计算一个简单的加法校验和。不清楚的一件事是如何安全地将数据加载到向量中。

typedef char v16qi __attribute__ ((vector_size(16)));

static uint8_t checksum(uint8_t *buf, size_t size)
{
    assert(size%16 == 0);
    uint8_t sum = 0;

    vec16qi vec = {0};
    for (size_t i=0; i<(size/16); i++)
    {
        // XXX: Yuck! Is there a better way?
        vec += *((v16qi*) buf+i*16);
    }

    // Sum up the vector
    sum = vec[0] + vec[1] + vec[2] + vec[3] + vec[4] + vec[5] + vec[6] + vec[7] + vec[8] + vec[9] + vec[10] + vec[11] + vec[12] + vec[13] + vec[14] + vec[15];

    return sum;
}

将指针转换为向量类型似乎可行,但我担心如果 SIMD 硬件期望向量类型正确对齐,这可能会以可怕的方式爆炸。

我想到的唯一其他选择是使用临时向量并显式加载值(通过 memcpy 或元素分配),但在测试这个抵消大部分加速获得使用 SIMD 指令。理想情况下,我认为这类似于通用的 __builtin_load() 函数,但似乎不存在。

将数据加载到有对齐问题风险的向量中的更安全方法是什么?

【问题讨论】:

  • 在 GCC x86_64 上的未对齐内存上运行此命令将在 CPU 尝试将未对齐内存加载到 SSE 寄存器时导致 SIGSEGV。一个合理的选择似乎是要么仅校验和对齐内存,要么使用普通循环对字节求和,直到第一个 16 字节边界。
  • 在您当前的代码中,如果编译器知道输入(但总和不好),加载数据实际上编译得很好:godbolt.org/g/DeR3Qv。不知道输入就不太好了:godbolt.org/g/LxEkhp

标签: gcc checksum vectorization simd


【解决方案1】:

编辑(感谢 Peter Cordes)您可以投射指针:

typedef char v16qi __attribute__ ((vector_size (16), aligned (16)));

v16qi vec = *(v16qi*)&buf[i]; // load
*(v16qi*)(buf + i) = vec; // store whole vector

这编译为 vmovdqa 以加载和 vmovups 以存储。如果不知道数据是否对齐,则设置aligned (1) 以生成vmovdqu。 (godbolt)

请注意,还有几个专用的内置函数用于加载和卸载这些寄存器(Edit 2):

v16qi vec = _mm_loadu_si128((__m128i*)&buf[i]); // _mm_load_si128 for aligned
_mm_storeu_si128((__m128i*)&buf[i]), vec); // _mm_store_si128 for aligned

似乎有必要使用-flax-vector-conversions 来从chars 到v16qi 这个函数。

另请参阅:C - How to access elements of vector using GCC SSE vector extension
另见:SSE loading ints into __m128

(提示:谷歌最好的短语是“gcc loading __m128i”。)

【讨论】:

  • 显然,将未对齐数据加载到 GNU C 向量中的推荐方法是在声明向量类型时使用 aligned(1) 属性,并将指针转换为该未对齐向量类型。例如typedef char __attribute__ ((vector_size (16),aligned (1))) unaligned_byte16;。请参阅the end of my answer here,以及 Marc Glisse 的 cmets。
  • 要提取,我认为你应该使用vec[0]。据我了解,将标量指针混叠到向量类型是 not 好的。它适用于char*,因为char* 是特殊的,并且允许为任何东西起别名。将int* 转换为v4si* 甚至不算作别名,因为v4si 是根据int 定义的。 Intel 内在函数类型 (__m128i) 也可以别名为其他东西,因为有一个额外的属性:typedef long long __m128i __attribute__ ((__vector_size__ (16), __may_alias__)); 如果没有 may_alias,您将无法安全地使用 v4si ivec = *(v4si)short_pointer。我之前把它漏掉了
  • 查看 /usr/lib/gcc/x86_64-linux-gnu/5/include/emmintrin.h 或您的 gcc 副本保留该标头的任何位置。
  • 回复:extract,刚刚意识到我所拥有的是移动一个字节,暂时改回 memcpy,仍在挖掘...感谢提示
  • 这个关于如何正确地将数据输入/输出 GNU C 矢量扩展的问题似乎真的需要一个教程,或者更长的规范答案。我可能会写一个,但我没有在我写的任何代码中使用它们,除了在 Godbolt 上的实验。
【解决方案2】:

您可以使用初始化程序来加载值,即做

const vec16qi e = { buf[0], buf[1], ... , buf[15] }

并希望 GCC 将其转换为 SSE 加载指令。不过,我会用反汇编程序验证这一点;-)。此外,为了获得更好的性能,您尝试使buf 16 字节对齐,并通过aligned 属性通知该编译器。如果您可以保证输入缓冲区将对齐,请按字节处理它,直到达到 16 字节边界。

【讨论】:

  • 我不认为对齐 buf 是必要的。如果我们正在处理指针,它会是。
  • @user1095108 您希望编译器将其转换为 SSE 加载指令,相当于 e = *buf(但您不能这样写,因为类型不匹配)。所以你实际上在这里处理指针。如果编译器可以推断出 buf 是 16 字节对齐的,那么它就可以使用对齐加载,这(至少在 ivy-bridge 之前)比未对齐加载要快。
  • 不,根据我的经验,如果您将 buf 转换为 vec16qi,您将需要处理指针。
  • @user1095108 我想你误会了我。你当然不是在处理指针,严格来说。但是您正在加载一个值(实际上是 16 个值)指向 buf, which is *exactly* what dereferencing a pointer (of type vec16qi) would do. Now, since we're not *strictly speaking* dereferencing buf`,指针没有必须 对齐以确保正确性。 但是它可能仍然会在性能上产生巨大的差异 - 在某些 CPU 上确实如此。假设编译器甚至将其转换为 SSE 加载指令。
  • 在我的机器上,我只在直接处理指针时看到对齐问题,而不是在将指针加载到向量中时取消引用它们。
猜你喜欢
  • 2013-09-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-04-12
  • 1970-01-01
  • 2013-11-03
  • 1970-01-01
  • 2015-03-04
相关资源
最近更新 更多