向量的数据如何对齐?

61
如果我想使用SSE处理std :: vector 中的数据,则需要16字节对齐。我如何实现这一点?我需要编写自己的分配器吗?还是默认的分配器已经对齐到16字节边界?

在C++11中,有一个aligned_storage。也许还有一个aligned_allocator?让我检查一下。 - Xeo
2
可能是SSE和C++容器的重复问题。 - Paul R
2
请查看C++ STL数据结构对齐,算法向量化 - mattia.penati
8个回答

49

C++标准要求分配函数(malloc()operator new())为任何标准类型分配内存,使其对齐合适。由于这些函数不接受对齐需求作为参数,实际上意味着所有分配的对齐方式相同,并且是具有最大对齐需求的标准类型的对齐方式,通常是long double和/或long long(请参见boost max_align union)。

向量指令(例如SSE和AVX)具有比标准C++分配函数提供的更强的对齐要求(128位访问需要16字节对齐,256位访问需要32字节对齐)。可以使用posix_memalign()memalign()来满足此类对齐要求更强的分配。


在C++17中,分配函数接受一种名为std::align_val_t的附加参数。

可以像这样使用它:

#include <immintrin.h>
#include <memory>
#include <new>

int main() {
    std::unique_ptr<__m256i[]> arr{new(std::align_val_t{alignof(__m256i)}) __m256i[32]};
}

此外,在C++17中,标准分配器已更新为遵守类型的对齐方式,因此您可以简单地执行以下操作:

#include <immintrin.h>
#include <vector>

int main() {
    std::vector<__m256i> arr2(32);
}

或者(不涉及堆分配,且在C++11中受支持):

#include <immintrin.h>
#include <array>

int main() {
    std::array<__m256i, 32> arr3;
}

精彩的答案,喜欢它! - Paul Sanders
new(std::align_val_t{alignof(__m256i)}) 中的 (std::align_val_t{alignof(__m256i)}) 部分是多余的。如果数组类型具有比不对齐的 operator new[] 保证更严格的对齐要求,新表达式将调用带有正确 align_val_t 参数的 operator new[] 重载。显式指定它有点误导人,因为不保证比给定数组类型更严格的对齐方式能够正常工作。 - user17732522
1
@user17732522,您是否想要在这个2011年的答案中添加您的C++17补充? - Maxim Egorushkin
1
啊,我只是在摆弄我的aligned_allocator类,没有意识到C++17会尊重类型对齐。谢谢。 - Robinson

28

在使用std::容器(如vector)时,您应该使用自定义分配器。以下是一个比较好的实现,虽然作者已经无从考证,但我在使用一段时间后发现它似乎很有效(根据编译器/平台可能需要将_aligned_malloc更改为_mm_malloc):

#ifndef ALIGNMENT_ALLOCATOR_H
#define ALIGNMENT_ALLOCATOR_H

#include <stdlib.h>
#include <malloc.h>

template <typename T, std::size_t N = 16>
class AlignmentAllocator {
public:
  typedef T value_type;
  typedef std::size_t size_type;
  typedef std::ptrdiff_t difference_type;

  typedef T * pointer;
  typedef const T * const_pointer;

  typedef T & reference;
  typedef const T & const_reference;

  public:
  inline AlignmentAllocator () throw () { }

  template <typename T2>
  inline AlignmentAllocator (const AlignmentAllocator<T2, N> &) throw () { }

  inline ~AlignmentAllocator () throw () { }

  inline pointer adress (reference r) {
    return &r;
  }

  inline const_pointer adress (const_reference r) const {
    return &r;
  }

  inline pointer allocate (size_type n) {
     return (pointer)_aligned_malloc(n*sizeof(value_type), N);
  }

  inline void deallocate (pointer p, size_type) {
    _aligned_free (p);
  }

  inline void construct (pointer p, const value_type & wert) {
     new (p) value_type (wert);
  }

  inline void destroy (pointer p) {
    p->~value_type ();
  }

  inline size_type max_size () const throw () {
    return size_type (-1) / sizeof (value_type);
  }

  template <typename T2>
  struct rebind {
    typedef AlignmentAllocator<T2, N> other;
  };

  bool operator!=(const AlignmentAllocator<T,N>& other) const  {
    return !(*this == other);
  }

  // Returns true if and only if storage allocated from *this
  // can be deallocated from other, and vice versa.
  // Always returns true for stateless allocators.
  bool operator==(const AlignmentAllocator<T,N>& other) const {
    return true;
  }
};

#endif

使用方法如下(如果需要,将16更改为其他对齐方式):

std::vector<T, AlignmentAllocator<T, 16> > bla;

然而,这只确保了std::vector使用的内存块是16字节对齐的。如果sizeof(T)不是16的倍数,则某些元素将不会对齐。根据您的数据类型,这可能不是一个问题。如果Tint(4个字节),则仅加载索引为4的倍数的元素。如果是double(8个字节),则仅加载2的倍数等。

真正的问题是如果您使用类作为T,在这种情况下,您将不得不在类本身中指定对齐要求(再次取决于编译器,这可能是不同的;以下示例适用于GCC):

class __attribute__ ((aligned (16))) Foo {
    __attribute__ ((aligned (16))) double u[2];
};

我们快要完成了!如果您使用的是Visual C++(至少2010版本),则由于std::vector::resize,您将无法使用已指定对齐方式的类的std::vector。

编译时,如果出现以下错误:

C:\Program Files\Microsoft Visual Studio 10.0\VC\include\vector(870):
error C2719: '_Val': formal parameter with __declspec(align('16')) won't be aligned

您需要修改stl::vector头文件

  1. 找到vector头文件[C:\ Program Files \ Microsoft Visual Studio 10.0 \ VC \ include \ vector]
  2. 找到void resize(_Ty _Val)方法[VC2010的第870行]
  3. 将其更改为void resize(const _Ty& _Val)

错误:在此作用域中未声明“_aligned_malloc”。 - thang
1
请注意,最后提出的“hack”可能会引入有关所引用对象生命周期的真实错误。例如,使用传值参数时vector<T> v(1); v.resize(v[0]);是合法的。但是在更改为引用后,它可能会出现问题。 - Ben Voigt

22

不必像之前建议的那样编写自己的分配器,您可以像这样为std::vector使用boost::alignment::aligned_allocator:

#include <vector>
#include <boost/align/aligned_allocator.hpp>

template <typename T>
using aligned_vector = std::vector<T, boost::alignment::aligned_allocator<T, 16>>;

3

编写自己的内存分配器。 allocatedeallocate 是重要的函数。以下是一个例子:

pointer allocate( size_type size, const void * pBuff = 0 )
{
    char * p;

    int difference;

    if( size > ( INT_MAX - 16 ) )
        return NULL;

    p = (char*)malloc( size + 16 );

    if( !p )
        return NULL;

    difference = ( (-(int)p - 1 ) & 15 ) + 1;

    p += difference;
    p[ -1 ] = (char)difference;

    return (T*)p;
}

void deallocate( pointer p, size_type num )
{
    char * pBuffer = (char*)p;

    free( (void*)(((char*)p) - pBuffer[ -1 ] ) );
}

5
你的代码在 64 位平台上可能不会正常工作。你应该使用 intptr_t(它保证具有指针大小)代替 int,并移除 INT_MAX(尺寸很可能是无符号的)。 - Christian Rau
@Christian,这只是解决问题的一个想法。我可以用C/C ++更好地解释,而其他人只是在评论。这就是我写那个的原因。Fred是唯一知道他将如何解决它的人。我希望这会指引他朝着正确的方向。 - user1090249
5
我理解你的论点,只需要进行一些简单的修改即可将此代码变得更加健壮,而不会让它变得更加复杂。但是,你得到了我的支持(+1)。 - Christian Rau

2

简短回答:

如果 sizeof(T)*vector.size() > 16,那么是的。
假设您的向量使用普通分配器

注意:只要 alignof(std::max_align_t) >= 16,因为这是最大对齐。

长回答:

更新于2017年8月25日新标准 n4659

如果它对于任何大于16的东西都是对齐的,那么它也正确地对齐了16。

6.11 对齐(第4/5段)

对齐方式表示为 std::size_t 类型的值。有效对齐方式仅包括基本类型的 alignof 表达式返回的那些值以及一个额外的实现定义的值集,该集合可以为空。每个对齐值都必须是正的二的幂。

对齐方式具有从较弱到较强或更严格的对齐方式的顺序。更严格的对齐方式具有更大的对齐值。满足对齐要求的地址也满足任何较弱的有效对齐要求。

new和new[]返回对齐的值,以便对象根据其大小正确对齐:

8.3.4 New(第17段)

[注意:当分配函数返回除null之外的值时,它必须是指向已保留对象空间的存储块的指针。假定存储块已适当对齐并具有所请求的大小。如果对象是数组,则创建的对象的地址不一定与块的地址相同。—注]

请注意,大多数系统都有最大对齐方式。动态分配的内存不需要对齐到大于此值的值。

6.11 对齐(第2段)

基本对齐由小于或等于实现在所有上下文中支持的最大对齐方式表示,该对齐方式等于alignof(std :: max_align_t)(21.2)。当类型用作完整对象的类型和用作子对象的类型时,所需的对齐方式可能不同。

因此,只要您的向量内存分配大于16个字节,它就会在16个字节边界上正确对齐。


6
然而,大多数实现会因使用vmovaps加载/存储需要32字节对齐的内存而导致std::vector<__m256>出现段错误。 SIMD向量不被视为基本类型,因此,在现有的x86 C++实现中,new不会返回足够对齐以容纳它们的内存。 在一些实现中(特别是32位),new只返回8字节对齐的内存,即使是std::vector<__m128>也会出错。 - Peter Cordes
3
sizeof(T)*vector.size()并不相关。首先,T可以是结构体类型;其次,vector.size()与内存对齐方式无关。除了实现细节(例如,通常会分配新的整个页面),这两者之间没有任何联系。原帖中想要实现的是(例如)使用16字节对齐的std::vector<float>,但大多数实现都不保证在没有自定义分配器的情况下实现这一点。(std::vector<__m128>也不支持这种用例,但这不是通常的用法。) - Peter Cordes
2
@PeterCordes 给你加了一个警告。现在你可以使用 aligned_storage 来使标准容器与其他值对齐。参见:http://en.cppreference.com/w/cpp/types/aligned_storage - Martin York
5
“Err, std::aligned_storage只是一个缓冲区。那里的示例在其上实现了一个容器(使用存储数组作为成员数组,而不是动态分配)。没有明显的方法可以让标准容器用它来做任何事情。 “示例实现”说它可以基于alignas构建,但这对于动态存储并没有帮助。” - Peter Cordes
2
顺便提一下,std::vector<__m256> 在正确的实现中确实可以使用 -std=gnu++17-std=c++17。只有在 C++14 及更早版本中才会出现问题,而我 2017 年的早期评论可能仅基于尝试了没有 -std=gnu++17g++,如果当时支持的话。但是,您将不得不使用 vector<__m256> 而不是其他代码可以轻松访问的 vector<float>,并且它的大小不是 8 个浮点数的倍数。 - Peter Cordes
显示剩余7条评论

1

这是对一个过时但重要的问题的现代回答。

像其他人所说,编写自己的Allocator类[模板]是首选。自C++11以来直到C++17,实现大多受标准限制,只能使用alignas和放置new。C++17取消了C11的aligned_alloc,这很方便。此外,C++17的std::pmr命名空间(头文件<memory_resource>)引入了polymorphic_allocator类模板和memory_resource抽象接口,受Boost启发,用于多态分配。除了允许真正通用的动态代码之外,在某些情况下已经显示出速度提升;在这种情况下,您的SIMD代码将表现得更好。


-2

我非常确定这在std::vector上不起作用:它只会对齐控制块,而不是指向的.data()。你链接的文档没有提到std::vectorvector<,所以我认为任何提到单词vector的地方都是在谈论SIMD向量,而不是C++的std::vector<T> - Peter Cordes

-4
标准规定newnew[]返回对于任何数据类型都对齐的数据,这应该包括SSE。无论MSVC是否遵循该规则是另一个问题。

1
@Fred有提到过MSVC吗? - Xeo
8
“which should include SSE” - 但通常没有。据我所知,无论是Windows还是Linux,只保证对已分配内存的8字节对齐,MSVC和glibc都没有采取任何措施增加对齐方式。我认为这种模棱两可的说法是,由于SSE操作是非标准的,实现者可以选择任何行为方式,包括在非16字节对齐的内存上执行SSE操作是未定义的。你可以将扩展的SSE类型存储在8字节对齐的内存中,因此对标准进行了口头服务,但实际上必须理解为不适用于非标准类型。 - Steve Jessop
1
真的适用于任何类型吗?所以,如果我自己想出一个需要4096字节对齐的数据类型,也会得到支持吗?当然,这个例子是垃圾的,但我希望你能看到任何有点不合适。我猜它是适用于任何标准类型,我非常确定SSE类型不属于其中,因为C++标准没有提到SSE。 - Christian Rau
@ChristianRau:它意味着实现对类型所施加的任何对齐要求。是的,您可以编写一个库,它接受char*MYTHING*指针参数,并检查该指针是否为4096对齐,并在不对齐时中止、抛出或执行未定义的操作。这并不意味着charMYTHING在标准的意义上具有4096对齐要求。我认为标准确实打算通过mallocnew来满足实现所施加的任何对齐要求,但实现者认为这样会浪费空间,因此不切实际。 - Steve Jessop
x86-64 SystemV ABI 具有alignof(long double) = 16,这是Intel针对80位x87优化手册的建议。在64位Linux(使用glibc)上,malloc/new确实会为您提供16字节对齐的内存。但这对于AVX或AVX512并没有帮助。(正如您所说,64位Windows和32位任何操作系统只提供8字节对齐的内存。) - Peter Cordes
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接