26

我使用大量 SSE 编译器内在函数编写了一个 3D 矢量类。一切都很好,直到我开始将具有 3D 矢量的类作为 new 的成员。我在发布模式下遇到了奇怪的崩溃,但在调试模式下却没有,反之亦然。

所以我阅读了一些文章,并认为我需要将拥有 3D 矢量类实例的类也对齐到 16 个字节。所以我只是在类前面添加了_MM_ALIGN16__declspec(align(16)),如下所示:

_MM_ALIGN16 struct Sphere
{
    // ....

    Vector3 point;
    float radius
};

起初这似乎解决了这个问题。但是在更改了一些代码之后,我的程序又开始以奇怪的方式崩溃。我在网上搜索了一些,发现了一篇博客文章。我尝试了作者 Ernst Hot 解决问题的方法,它也对我有用。我在我的类中添加了 new 和 delete 运算符,如下所示:

_MM_ALIGN16 struct Sphere
{
    // ....

    void *operator new (unsigned int size)
     { return _mm_malloc(size, 16); }

    void operator delete (void *p)
     { _mm_free(p); }

    Vector3 point;
    float radius
};

Ernst 提到这种方法也可能存在问题,但他只是链接到一个不再存在的论坛,而没有解释为什么它可能存在问题。

所以我的问题是:

  1. 定义运算符有什么问题?

  2. 为什么添加_MM_ALIGN16到类定义中还不够?

  3. 处理 SSE 内在函数带来的对齐问题的最佳方法是什么?

4

3 回答 3

21

首先,您必须注意两种类型的内存分配:

  • 静态分配。要使自动变量正确对齐,您的类型需要正确的对齐规范(例如__declspec(align(16)),__attribute__((aligned(16)))或 your _MM_ALIGN16)。但幸运的是,仅当类型成员(如果有)给出的对齐要求不充分时,您才需要这样做。所以你不需要这个Sphere,因为你Vector3已经正确对齐了。如果你Vector3包含一个__m128成员(这很可能,否则我建议这样做),那么你甚至不需要它Vector3。因此,您通常不必弄乱编译器特定的对齐属性。

  • 动态分配。简单的部分就这么多。问题是,C++ 在最低级别上使用了一个与类型无关的内存分配函数来分配任何动态内存。这只能保证所有标准类型的正确对齐,这可能恰好是 16 个字节,但不能保证。

    为此,您必须重载内置operator new/delete函数以实现您自己的内存分配,并在后台使用对齐的分配函数而不是 good old malloc。重载operator new/delete本身就是一个主题,但并不像起初看起来那么困难(尽管您的示例还不够),您可以在这个出色的常见问题解答中阅读有关它的内容。

    不幸的是,您必须为每种类型的任何成员都需要非标准对齐方式执行此操作,在您的情况下,SphereVector3. 但是你可以做的让它更容易一点,就是为这些运算符创建一个具有适当重载的空基类,然后从这个基类派生所有必要的类。

    大多数人有时会忘记的是,标准分配器std::alocator使用全局operator new分配所有内存,因此您的类型不适用于标准容器(并且std::vector<Vector3>用例并不罕见)。您需要做的是制作自己的标准符合分配器并使用它。但是为了方便和安全,实际上最好只std::allocator针对您的类型进行专门化(也许只是从您的自定义分配器派生它),以便始终使用它,并且您无需在每次使用std::vector. 不幸的是,在这种情况下,您必须再次为每个对齐的类型专门化它,但是一个小的 evil 宏可以帮助解决这个问题。

    此外,您必须使用全局而不是您的自定义来寻找其他事物operator new/delete,例如std::get_temporary_bufferand std::return_temporary_buffer,并在必要时照顾这些事物。

不幸的是,我认为还没有更好的方法来解决这些问题,除非你在一个原生与 16 对齐的平台上并且知道这一点。或者您可能只是重载全局operator new/delete以始终将每个内存块对齐到 16 个字节,并且无需关心包含 SSE 成员的每个类的对齐,但我不知道这种方法的含义。在最坏的情况下,它只会导致浪费内存,但是你通常不会在 C++ 中动态分配小对象(尽管可能对此有不同的看法)std::liststd::map

所以总结一下:

  • 使用类似的东西注意静态内存的正确对齐__declspec(align(16)),但前提是它还没有被任何成员照顾,这通常是这种情况。

  • operator new/delete具有非标准对齐要求的成员的每种类型的重载。

  • 制作一个符合标准的定制分配器,以在对齐类型的标准容器中使用,或者更好的是,专门std::allocator针对每个对齐类型。


最后是一些一般性的建议。在执行许多向量运算时,您通常只能在计算量大的块中从 SSE 中获利。为了简化所有这些对齐问题,特别是关心每个包含 a 的类型的对齐问题,Vector3制作一个特殊的 SSE 向量类型并且只在冗长的计算中使用它可能是一个很好的方法,使用正常的非用于存储和成员变量的 SSE 向量。

于 2012-09-20T12:08:17.777 回答
2

基本上,您需要确保您的向量正确对齐,因为 SIMD 向量类型通常比任何内置类型具有更大的对齐要求。

这需要做以下事情:

  1. 确保Vector3它在堆栈或结构的成员上时正确对齐。这是通过应用于__attribute__((aligned(32)))Vector3(或编译器支持的任何属性)来完成的。请注意,您不需要将属性应用于包含 的结构Vector3,这不是必需的,而且还不够(即无需将其应用于Sphere)。

  2. 在使用堆分配时,确保它Vector3或其封闭结构正确对齐。这是通过使用posix_memalign()(或您的平台的类似函数)而不是使用plain 来完成的,malloc()或者operator new()因为后两者为内置类型(通常为8 或16 字节)对齐内存,这不能保证对于SIMD 类型来说足够。

于 2012-09-20T09:49:08.847 回答
1
  1. 运营商的问题在于它们本身是不够的。它们不会影响您仍然需要的堆栈分配__declspec(align(16))

  2. __declspec(align(16))影响编译器如何将对象放入内存中,当且仅当它有选择时。对于新的对象,编译器别无选择,只能使用operator new.

  3. 理想情况下,使用本机处理它们的编译器。没有理论上的理由说明他们需要区别对待double。否则,请阅读编译器文档以了解解决方法。每个有缺陷的编译器都有自己的一组问题,因此也有自己的一组解决方法。

于 2012-09-20T08:24:03.943 回答