微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

使用 std::vector<Vec8d> 是好是坏性能方面

如何解决使用 std::vector<Vec8d> 是好是坏性能方面

我正在使用 Agner Fog 的 vectorclass 库在我的应用程序中使用 SIMD 指令(特别是 AVX)。由于最好使用 struct-of-array 数据结构来轻松使用 SIMD,因此我经常使用:

std::vector<Vec8d> some_var;

甚至

struct some_struct {
    std::vector<Vec8d> a;
    std::vector<Vec8d> b;
}

考虑到 std::vector 内部 Vec8d* 数组实际上可能未对齐,我想知道这是否很糟糕(性能方面甚至完全错误?)?

解决方法

我通常会使用 vector<double> 和标准 SIMD 加载/存储内部函数来访问数据。这避免将界面和所有接触它的代码绑定到特定的 SIMD 矢量宽度和包装库。您仍然可以将大小填充为 8 个双精度的倍数,因此您不必在循环中包含清理处理。

但是,您可能希望为该 vector<double> 使用自定义分配器,以便您可以使用它来对齐双打。不幸的是,即使该分配器的底层内存分配与 new/delete 兼容,它也将具有与 vector<double> 不同的 C++ 类型,因此如果您在其他地方使用它,则无法自由地将其分配/移动到此类容器。

我担心如果您确实想要访问向量的单个 double 元素,那么执行 Vec8vec[i][j] 可能会导致更糟糕的 asm(例如 SIMD 负载然后从 VCL 的 operator[]) 进行洗牌或存储/重新加载而不是 vecdouble[i*8 + j](大概只是一个 vmovsd),特别是如果这意味着您需要编写一个嵌套循环,否则您不会这样做需要一个。

avec.load (&doublevec[8]); 应该生成(几乎或完全)与 avec = Vec8vec[1]; 相同的 asm。如果数据在内存中,编译器将需要使用加载指令来加载它。它有什么“类型”并不重要;类型是 C++ 的东西,而不是 asm 的东西; SIMD 向量只是对内存中某些字节的重新解释。


但如果这是说服 C++17 编译器将动态数组对齐 64 的最简单方法,那么可能值得考虑。如果/当移植到 ARM NEON 或 SVE 时,仍然令人讨厌并且会导致未来的痛苦,因为 Agner 的 VCL 仅在我最后检查时包装 x86 SIMD。甚至移植到 AVX2 也会很糟糕。

更好的方法可能是自定义分配器(我认为 Boost 已经编写了一些),您可以将其用作 std::vector<double,aligned_allocator<64>> 之类的第二个模板参数。如果您想传递它并将其分配给其他 std::vector<double>,这也与 vector<> 类型不兼容,但至少它没有专门绑定到 AVX512。

如果你没有使用 C++17 编译器(所以 std::vector 不尊重 alignof(T) > alignof(max_align_t) ie 16),那么甚至不要考虑这个;当 GCC 和 Clang 等编译器使用 vmovapd(需要对齐)来存储 __m512d 时,它会出错。

您需要对齐数据;与当前 AVX512 CPU (Skylake-X) 上的 AVX2 相比,64 字节对齐与 AVX512 的差异更大。

MSVC(我认为 ICC)出于某种原因选择始终使用未对齐的加载/存储指令(即使使用传统 SSE 指令将加载折叠到内存源操作数中除外,因此需要 16 字节对齐),即使在编译时对齐时也是如此保证存在。我想这就是它碰巧适合你的原因。

对于 SoA 数据布局,您可能希望为所有数组共享一个通用大小,并使用 aligned_alloc(与 free 兼容,而不是 delete)或类似的东西来管理大小对于 double * 成员。不幸的是,没有支持aligned_realloc 的标准对齐分配器,因此您总是必须进行复制,即使在您的阵列后面有可用的虚拟地址空间,一个非蹩脚的API 可以让您的阵列增长而无需复制。谢谢,C++。

,

std::vector 将在 C++17 下正确对齐,无论如何矢量类库都需要它。这将正常工作。 std::vector 模板相对高效。其他几个标准容器模板效率非常低,因为它们被实现为具有大量动态内存分配和取消分配的链表。

如果在编译时就知道数组的大小,或者如果你有一个合理的数组大小上限,那么制作一个老式的 C 数组可能更有效。

const int arraysize = 0x100;
alignas(64) double myarray[arraysize];  // AVX-512 benefits a lot from alignment
...
Vec8d a;
for (int i=0; i < arraysize/a.size(); i += a.size()) {
    a.load(myarray+i);
    // do your calculations here

}

如果在编译时不知道数组大小,那么您可以简单地分配自己的数组:

Vec8d * mydynamicarray = new Vec8d[mysize];

将内存分配包装在容器类中是一个很好的做法,它使用一个析构函数来清理分配:

~myContainerClass() {
    if (mydynamicarray != 0) delete[] mydynamicarray;
}
,

这取决于您打算如何使用 some_struct,而不是使用两个向量,您可能更喜欢每个成员一个:

struct alignas(64) some_struct {
    double[8] a;
    double[8] b;
};

std::vector<some_struct> vector_of_struct_of_arrays{};

我发现我的代码在这种布局下通常更清晰,正如前面提到的,如果您由于某种原因无法使用 vectorclass,这允许将来使用不同的库。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。