【问题标题】:Converting to and from __m256i and std::vector<uint32_t>与 __m256i 和 std::vector<uint32_t> 相互转换
【发布时间】:2019-11-05 18:11:23
【问题描述】:

我想在__m256i 实例和std::vector&lt;uint32_t&gt; 实例之间进行转换(正好包含8 个元素)。

到目前为止,我想出了这个:

using vu32 = std::vector<uint32_t>;

__m256i v2v(const vu32& in) {
    assert(in.size() == 8);
    return _mm256_loadu_si256(reinterpret_cast<const __m256i*>(in.data()));
}

vu32 v2v(__m256i in) {
    vu32 out(8);
    _mm256_storeu_si256(reinterpret_cast<__m256i*>(out.data()), in);
    return out;
}

安全吗?

有没有更惯用的方法呢?

【问题讨论】:

  • 如果vector 有固定长度,为什么不用std::array&lt;uint32_t, 8&gt; 代替呢?这可能有点惯用语,并且关于安全性的语言律师较少,因为该类只是普通数组的包装器。另外,您不会有 std::vector 通常会带来的动态分配开销(我不知道有任何实现像许多 std::strings 那样具有小向量优化)。
  • @JasonR - 我同意,但作为外部约束,我必须为此使用std::vector。总体上同意效率,尽管vector 在可以使用移动的情况下确实有一些优势。
  • 很公平。从实际的角度来看,我认为您的示例代码没有任何问题。我敢肯定,在 C++ 标准语言方面更出色的人可以权衡它是否完全按照标准定义,但我以前无数次使用过非常相似的结构。
  • @PeterCordes - SIMD 向量和vector 彼此有很大关系,因为它们都是给定类型的 N 个元素的连续存储。 SIMD 往往有一个固定的 N 并且类型更灵活(例如,对于整数内容,您基本上可以将类型从操作更改为操作,或者使用提供的无操作转换在整数和 FP 域之间进行转换)。

标签: c++ intel simd intrinsics avx2


【解决方案1】:

首先,SIMD 向量和std::vector 基本上没有任何关系。我知道已经知道这一点,但未来的读者应该仔细考虑这是否真的是他们想要做的事情。


很安全; .data() 必须返回一个可以在任何有效索引处读取或写入的指针。考虑到真正的std::vector 库的实现细节,这在实践中肯定是安全的。就纸上标准而言,我很确定摘要中的内容。

从 cmets 看来,您似乎担心严格混叠 UB。

通过may_alias指针类型(包括char*__m256i*)读/写其他对象没问题。 memcpy(&amp;a, &amp;b, sizeof(a)) 是通过char* 修改a 的对象表示的常见示例。 memcpy 本身并没有什么特别之处。由于 char* 别名特殊情况,这是明确定义的。

may_alias 是一个 GNU C 扩展,它允许您定义除 char 以外的类型,这些类型可以像 char* 那样使用别名。 GNU C 对 __m128 / __m256i 的定义是根据 GNU C 本机向量,如 typedef long long __m256i __attribute((vector_size(32), may_alias)); 其他 C++ 实现(如 MSVC)以不同方式定义 __m256i,但 英特尔内在 API 保证别名向量指针在 char* / memcpy 的任何情况下,其他类型都是合法的。

另见Is `reinterpret_cast`ing between hardware vector pointer and the corresponding type an undefined behavior?

另外:SSE: Difference between _mm_load/store vs. using direct pointer access - loadu / storeu 就像在取消引用之前强制转换矢量类型的 aligned(1) 版本。所以所有关于指针和别名的推理都适用于将指针传递给_mm_storeu,而不仅仅是直接取消引用。


惯用语;可以肯定,这看起来像非常惯用的 C++。我可能仍然使用带有内在函数的 C 风格强制转换,只是因为 reinterpret 的阅读时间太长,而且整数向量设计不佳的内在函数 API 到处都需要它。也许 si256 load/loadu 和 store/storeu 的模板化包装函数是合适的,它可以从任何指针类型转换为 __m256i*const __m256i*


我可能更喜欢将__m256i 元素传递给out 的构造函数的东西,以阻止愚蠢的编译器可能将内存归零然后存储向量。但希望这不会发生。

实际上 gcc 和 clang 在存储向量之前会优化死存储到零 8 个元素。任何使用vector(begin, end) 迭代器构造函数的尝试都会使事情变得更糟,在将in 存储/重新加载到堆栈(大约new)之上用于异常处理的额外代码,然后将其存储到新分配的记忆。

查看on the Godbolt compiler explorer 的一些尝试,注意它们保存/恢复r13 而@Bee 的版本没有,以及通过函数在正常路径之外生成的额外代码。这在-fno-exceptions 中消失了,但是它们与@Bee 的版本相同,而不是更好。所以使用问题中的代码;它的编译效果至少与我的任何与众不同的尝试一样。


如果可以在不更改模板类型的情况下,我可能更喜欢做一些事情来分配新的std::vector&lt;uint32_t&gt; 并分配 32 字节对齐的内存。我不确定这是否可能。

即使我们可以在实践中使初始分配保持一致,而无需更改类型以使其成为将来使用的编译时保证,这也可能会有所帮助。将未对齐处理留给硬件的 AVX 代码将受益于没有缓存行拆分。

但我认为,如果不破解 std::vector 的自定义构造函数,该构造函数使用对齐的 new 进行初始分配,假设它与常规 delete 兼容,这也是不可能的。

如果您可以在代码中的任何位置使用std::vector&lt;uint32_t, some_aligned_allocator&gt;,那么可能值得这样做。但如果您必须将其传递给使用普通 vector&lt;uint32_t&gt; 的代码,则可能不值得麻烦。

可能对您的编译器撒谎,因为该类型与常规 std::vector&lt;uint32_t&gt; 二进制兼容(但不兼容源代码),在对齐的 new/delete 与普通 new/delete 兼容的系统上.但我不建议这样做。

【讨论】:

  • 我的意思不是“惯用的”v2v 函数提供的功能本身是否惯用,而是考虑到您是否需要这些功能(例如,由于外部要求),显示的实现在 SIMD 内在函数方面是否是惯用的。
  • AFAIK 无法在不更改分配器的情况下为 std::vector 获得对齐的内存,这会更改其类型。
  • 关于“可能更喜欢某事......”你的意思是like this?令人惊讶的是,gcc 在这种情况下似乎会生成更糟糕的代码,显然它还必须处理出现异常的可能性。大概是因为它必须在从迭代器复制值之前进行分配,并且一些迭代器可能会抛出(但这个不能)。 clang 做对了,似乎在这两种情况下都能生成类似的代码。
  • @BeeOnRope:我认为你的版本有严格别名的 UB,但是我没想到异常处理开销。我在 Godbolt 上做了一些不同的尝试,除非你使用 -fno-exceptions,否则它们都会变得更糟。如果它们使用 2 个单独的 128 位加载/存储,那么它们是相等的,或者更糟。或者如果它保存另一个 vzeroupper 可能会更好,但我们正在查看此函数的非内联版本。
  • 呵呵,我认为你的版本可能有严格的别名UB。矢量类型是特殊的 wrt 别名(“可能别名”),但我不清楚具体是哪种方式。原始版本将store 内在函数inside 写入向量数据,我认为这始终是犹太教。 pass-it-to-the-initializer 版本将向量类型重新解释为uint32_t 数组,这对我来说不太清楚。我不确定在任何情况下是否存在任何别名问题,因为实际上没有两个对象存在别名。有重新解释,但没有别名。
猜你喜欢
  • 2023-03-07
  • 2018-12-13
  • 2023-03-23
  • 1970-01-01
  • 2022-01-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-20
相关资源
最近更新 更多