首页 > 解决方案 > 与 __m256i 和 std::vector 相互转换

问题描述

我想在__m256i实例和std::vector<uint32_t>实例之间进行转换(正好包含 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;
}

安全吗?

有没有更惯用的方法来做到这一点?

标签: c++intelsimdintrinsicsavx2

解决方案


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


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

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

may_alias通过指针类型(包括char*or )读/写其他对象__m256i*是可以的。 memcpy(&a, &b, sizeof(a))是修改avia的对象表示的常见示例char*。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将是任何情况下,将向量指针别名到其他类型是合法的。

另请参阅硬件矢量指针和相应类型之间的“重新解释转换”是未定义的行为吗?

另外:SSE:_mm_load/store 与使用直接指针访问之间的区别 - 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)的顶部用于异常处理的额外代码,然后将其存储到新分配的内存中。innew

请参阅Godbolt 编译器资源管理器上的一些尝试,注意它们会保存/恢复@Beer13版本没有的地方,以及通过函数在正常路径之外生成的额外代码。这消失了-fno-exceptions,但它们与@Bee 的版本相同,而不是更好。所以使用问题中的代码;它至少编译了我的任何不同的尝试。


std::vector<uint32_t>如果可以在不更改模板类型的情况下使用 32 字节对齐的内存,我可能还更喜欢做一些事情来获得新分配的内存。我不确定这是否可能。

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

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

如果您可以std::vector<uint32_t, some_aligned_allocator>在代码中的任何地方使用 a ,那可能是值得的。但是,如果您必须将其传递给使用 normal 的代码,则可能不值得麻烦vector<uint32_t>

可能会对编译器撒谎,因为该类型与常规二进制兼容(但不兼容源代码)std::vector<uint32_t>,在对齐的新/删除与普通新/删除兼容的系统上。但我不建议这样做。


推荐阅读