如何解决 AVX 加载/存储操作的 32 字节对齐问题?

Posted

技术标签:

【中文标题】如何解决 AVX 加载/存储操作的 32 字节对齐问题?【英文标题】:How to solve the 32-byte-alignment issue for AVX load/store operations? 【发布时间】:2022-01-01 09:27:37 【问题描述】:

我在使用ymm 寄存器时遇到对齐问题,其中一些sn-ps 代码对我来说似乎很好。这是一个最小的工作示例:

#include <iostream> 
#include <immintrin.h>

inline void ones(float *a)

     __m256 out_aligned = _mm256_set1_ps(1.0f);
     _mm256_store_ps(a,out_aligned);


int main()

     size_t ss = 8;
     float *a = new float[ss];
     ones(a);

     delete [] a;

     std::cout << "All Good!" << std::endl;
     return 0;

当然,sizeof(float) 在我的架构 (Intel(R) Xeon(R) CPU E5-2650 v2 @ 2.60GHz) 上是 4,我正在使用 -O3 -march=native 标志编译 gcc。当然,错误会随着未对齐的内存访问而消失,即指定_mm256_storeu_ps。我在xmm 寄存器上也没有这个问题,即

inline void ones_sse(float *a)

     __m128 out_aligned = _mm_set1_ps(1.0f);
     _mm_store_ps(a,out_aligned);

我做了什么傻事吗?解决方法是什么?

【问题讨论】:

有点跑题了,但记得在删除使用new [] 分配的内容时使用delete [] 你试过 _mm_malloc 而不是 new 吗? 我想一个简单的总结应该是 因为 new/malloc 在 x64 上返回 16 字节对齐的指针; SSE 就够了,但是 AVX 需要 32 字节对齐. 相关:***.com/questions/12055822/…(解决 16 字节 SSE 对齐,但答案很容易适应 32 字节 AVX 对齐)。 也许这也很有趣:***.com/questions/16376942/… 【参考方案1】:

内存管理有两个内在函数。 _mm_malloc 像标准 malloc 一样运行,但它需要一个额外的参数来指定所需的对齐方式。在这种情况下,32 字节对齐。当使用这种分配方式时,内存必须通过相应的_mm_free调用来释放。

float *a = static_cast<float*>(_mm_malloc(sizeof(float) * ss , 32));
...
_mm_free(a);

【讨论】:

【参考方案2】:

是的,您可以使用 _mm256_loadu_ps / storeu 进行未对齐的加载/存储 (AVX: data alignment: store crash, storeu, load, loadu doesn't)。如果编译器不do a bad job (cough GCC default tuning),AVX _mm256_loadu/storeu 恰好对齐的数据与需要对齐的加载/存储一样快,所以仍然在方便时对齐数据对于通常在对齐数据上运行但让硬件处理它们不这样做的罕见情况的功能,为您提供两全其美的功能。 (而不是总是运行额外的指令来检查东西)。

对齐对于 512 位 AVX-512 向量尤其重要,例如 SKX 上 15% 到 20% 的速度,即使在您预计 L3/DRAM 带宽会成为瓶颈的大型阵列上,而 AVX2 CPU 的速度只有百分之几.


标准分配器通常只与alignof(max_align_t)对齐,通常为 16B,例如long double 在 x86-64 System V ABI 中。但在某些 32 位 ABI 中,它只有 8B,因此对于对齐的__m128 向量的动态分配甚至不够,您需要超越简单地调用newmalloc

静态和自动存储很容易:使用alignas(32) float arr[N];

C++17 为对齐的动态分配提供对齐的new。如果某个类型的alignof 大于标准对齐方式,则使用对齐的operator new/operator delete。所以new __m256[N] 只能在 C++17 中工作(如果编译器支持这个 C++17 特性;请检查 __cpp_aligned_new 特性宏)。实际上,GCC / clang / MSVC / ICX 支持它,ICC 2021 不支持。

如果没有 C++17 的特性,即使像 std::vector&lt;__m256&gt; 这样的东西也会损坏,而不仅仅是 std::vector&lt;int&gt;,除非你很幸运并且它恰好是 32 对齐的。


Plain-delete 兼容分配一个float / int 数组:

不幸的是,auto* arr = new alignas(32) float[numSteps] 不适用于所有编译器,因为alignas 适用于变量、成员或类声明,但不适用于类型修饰符。 (GCC 接受 using vfloat = alignas(32) float;,因此这确实为您提供了一个与 GCC 上的普通 delete 兼容的对齐新版本。

解决方法是包装在结构中 (struct alignas(32) s float v; ; new s[numSteps];) 或将对齐作为放置参数传递 (new (std::align_val_t(32)) float[numSteps];),在以后的情况下,请务必调用匹配对齐的 operator delete

请参阅 new/new[]std::align_val_t 的文档


其他选项,与new/delete不兼容

动态分配的其他选项大多兼容malloc/freenew/delete

std::aligned_alloc:ISO C++17。 主要缺点:尺寸必须是对齐的倍数。例如,这种无脑的要求使其不适用于分配未知数量的floats 的 64B 高速缓存行对齐数组。或者特别是一个 2M 对齐的数组来利用 transparent hugepages。

在 ISO C11 中添加了 aligned_alloc 的 C 版本。它在一些但不是所有的 C++ 编译器中可用。正如 cppreference 页面所指出的,当大小不是对齐的倍数(这是未定义的行为)时,C11 版本不需要失败,因此许多实现提供了明显的所需行为作为“扩展”。 Discussion is underway to fix this,但现在我不能真正推荐 aligned_alloc 作为分配任意大小数组的可移植方式。在实践中,一些实现在 UB / required-to-fail 情况下运行良好,因此它可能是一个很好的非便携选项。

此外,评论者报告它在 MSVC++ 中不可用。请参阅 best cross-platform method to get aligned memory 了解适用于 Windows 的可行 #ifdef。但是 AFAIK 没有 Windows 对齐分配函数可以产生与标准 free 兼容的指针。

posix_memalign:POSIX 2001 的一部分,不是任何 ISO C 或 C++ 标准。与aligned_alloc 相比,原型/界面笨拙。我已经看到 gcc 生成指针的重新加载,因为它不确定存储到缓冲区中没有修改指针。 (posix_memalign 传递了指针的地址,从而避免了转义分析。)因此,如果您使用它,请将指针复制到另一个尚未将其地址传递到函数外部的 C++ 变量中。

#include <stdlib.h>
int posix_memalign(void **memptr, size_t alignment, size_t size);  // POSIX 2001
void *aligned_alloc(size_t alignment, size_t size);                // C11 (and ISO C++17)

_mm_malloc:在_mm_whatever_ps 可用的任何平台上都可用,但您不能将指针从它传递给free。在许多 C 和 C++ 实现中,_mm_freefree 是兼容的,但不能保证可移植。 (与其他两个不同,它将在运行时失败,而不是编译时失败。)在 Windows 上的 MSVC 上,_mm_malloc 使用 _aligned_malloc,它与 free 不兼容;它在实践中崩溃了。

直接使用mmapVirtualAlloc等系统调用。适用于大型分配,并且您获得的内存根据定义是页面对齐的(4k,甚至可能是 2M 大页面)。 不兼容free;您当然必须使用需要大小和地址的munmapVirtualFree。 (对于大型分配,您通常希望在完成后将内存交还给操作系统,而不是管理空闲列表;glibc malloc 直接使用 mmap/munmap 来处理超过一定大小阈值的 malloc/free 块。)

主要优势:您不必处理 C++ 和 C 的脑死拒绝为对齐的分配器提供增长/收缩设施。如果您在分配后需要另外 1MiB 的空间,您甚至可以使用 Linux 的 mremap(MREMAP_MAYMOVE) 让它为相同的物理页面在虚拟地址空间(如果需要)中选择不同的位置,而无需复制任何内容。或者,如果它不必移动,则当前使用部分的 TLB 条目保持有效。

而且由于您无论如何都在使用操作系统系统调用(并且知道您正在处理整个页面),您可以使用madvise(MADV_HUGEPAGE) 来暗示transparent hugepages 是首选,或者它们不是,对于这个范围匿名页面。您还可以通过mmap 使用分配提示,例如让操作系统预置零页,或者如果在hugetlbfs上映射文件,则使用2M或1G页。 (如果该内核机制仍然有效)。

使用madvise(MADV_FREE),您可以保留它的映射,但让内核在内存压力发生时回收页面,如果发生这种情况,它就像延迟分配的零支持页面一样。因此,如果您很快重用它,您可能不会遭受新的页面错误。但如果你不这样做,你就不会占用它,当你阅读它时,它就像一个新映射的区域。


alignas() 带有数组/结构

在 C++11 及更高版本中:使用 alignas(32) float avx_array[1234] 作为结构/类成员的第一个成员(或直接在普通数组上),因此该类型的静态和自动存储对象将具有 32B 对齐。 std::aligned_storage documentation 有一个这种技术的例子来解释 std::aligned_storage 的作用。

在 C++17 之前,对于动态分配的存储(如 std::vector&lt;my_class_with_aligned_member_array&gt;),这实际上并不适用,请参阅 Making std::vector allocate aligned memory。

从 C++17 开始,编译器将为 alignas 在整个类型或其成员上强制对齐的类型选择对齐的new,同样std::allocator 将为此类类型选择对齐的new,所以创建此类类型的std::vector 时无需担心。


最后,最后一个选项太糟糕了,它甚至都不是列表的一部分:分配一个更大的缓冲区并使用适当的转换执行p+=31; p&amp;=~31ULL。太多的缺点(难以释放,浪费内存)值得讨论,因为每个支持 Intel _mm256_... 内在函数的平台上都有对齐分配函数。但如果你坚持的话,IIRC 甚至还有一些库函数可以帮助你做到这一点。

使用_mm_free 而不是free 的要求可能存在部分是因为使用这种技术在普通的旧malloc 之上实现_mm_malloc 的可能性。或者对于使用备用空闲列表的对齐分配器。

【讨论】:

@Useless:如果您将 _mm_whatever 内在函数用于 SSE / AVX / 其他指令,您还将有 _mm_malloc 可用。如果将对齐的分配与未对齐的分配分开不是问题,或者您可以在程序中的任何地方使用_mm_malloc / _mm_free,并且不要与任何分配或释放任何内容的库进行交互,那么这是一个也是有效的选项。 @PeterCordes aligned_alloc 在我看来是最好的。对于应该使用哪一个,是否有任何普遍共识? 既然你提到了C++17:alignas+dynamic allocation 终于固定在那里了。 @matejk:我不确定您是否必须编写自己的分配器,或者是否已经有可以自定义的模板分配器。就动态分配的对齐支持或公开有效的realloccalloc 以供 std::vector 使用而言,我对 C++ 完全不感兴趣。它的糟糕程度也很荒谬,直到 C++17 new __m256[] 才能正常工作。我不明白 WTF 很难将对齐作为模板参数成为类型的一部分。甚至 C 也缺少可移植的对齐 realloccalloc,AFAIK。 在 C++17 中,alignas 可以正常工作。您只需说new T 类型,由alignas 强制对齐大于__STDCPP_DEFAULT_NEW_ALIGNMENT__,并调用operator new 的对齐形式。 std::allocator 也有所不同,并在需要时调用对齐 operator new【参考方案3】:

您需要对齐的分配器。

但没有理由不能将它们捆绑起来:

template<class T, size_t align>
struct aligned_free 
  void operator()(T* t)const
    ASSERT(!(uint_ptr(t) % align));
    _mm_free(t);
  
  aligned_free() = default;
  aligned_free(aligned_free const&) = default;
  aligned_free(aligned_free&&) = default;
  // allow assignment from things that are
  // more aligned than we are:
  template<size_t o,
    std::enable_if_t< !(o % align) >* = nullptr
  >
  aligned_free( aligned_free<T, o> ) 
;
template<class T>
struct aligned_free<T[]>:aligned_free<T>;

template<class T, size_t align=1>
using mm_ptr = std::unique_ptr< T, aligned_free<T, align> >;
template<class T, size_t align>
struct aligned_make;
template<class T, size_t align>
struct aligned_make<T[],align> 
  mm_ptr<T, align> operator()(size_t N)const 
    return mm_ptr<T, align>(static_cast<T*>(_mm_malloc(sizeof(T)*N, align)));
  
;
template<class T, size_t align>
struct aligned_make 
  mm_ptr<T, align> operator()()const 
    return aligned_make<T[],align>(1);
  
;
template<class T, size_t N, size_t align>
struct aligned_make<T[N], align> 
  mm_ptr<T, align> operator()()const 
    return aligned_make<T[],align>(N);
  
:
// T[N] and T versions:
template<class T, size_t align>
auto make_aligned()
-> std::result_of_t<aligned_make<T,align>()>

  return aligned_make<T,align>();

// T[] version:
template<class T, size_t align>
auto make_aligned(size_t N)
-> std::result_of_t<aligned_make<T,align>(size_t)>

  return aligned_make<T,align>(N);

现在mm_ptr&lt;float[], 4&gt; 是一个指向floats 数组的唯一指针,该数组是4 字节对齐的。您可以通过 make_aligned&lt;float[], 4&gt;(20) 创建它,它创建 20 个 4 字节对齐的浮点数,或 make_aligned&lt;float[20], 4&gt;()(仅在该语法中的编译时常量)。 make_aligned&lt;float[20],4&gt; 返回 mm_ptr&lt;float[],4&gt; 而不是 mm_ptr&lt;float[20],4&gt;

mm_ptr&lt;float[], 8&gt; 可以移动构造mm_ptr&lt;float[],4&gt;,但反之则不行,我认为这很好。

mm_ptr&lt;float[]&gt; 可以采用任何对齐方式,但保证没有。

开销与std::unique_ptr 一样,每个指针基本上为零。积极的inlineing 可以最大限度地减少代码开销。

【讨论】:

@romeric 从多到少

以上是关于如何解决 AVX 加载/存储操作的 32 字节对齐问题?的主要内容,如果未能解决你的问题,请参考以下文章

GCC __attribute__ 在 32 字节处对齐的 AVX 矢量化代码中的段错误

阵列中的 AVX 对齐

SSE 向量重新对齐?

使用 AVX 的有符号/无符号整数的最小值

SSE / AVX 对齐内存上的 valarray

C语言-字节对齐