如何解决 AVX load/store 操作的 32 字节对齐问题?
How to solve the 32-byte-alignment issue for AVX load/store operations?
我在使用 ymm
寄存器时遇到对齐问题,一些代码片段对我来说似乎没问题。这是一个最小的工作示例:
#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);
}
我是不是在做什么傻事?解决方法是什么?
内存管理有两个内部函数。
_mm_malloc 像标准 malloc 一样运行,但它需要一个额外的参数来指定所需的对齐方式。在这种情况下,32 字节对齐。使用此分配方法时,必须通过相应的 _mm_free 调用释放内存。
float *a = static_cast<float*>(_mm_malloc(sizeof(float) * ss , 32));
...
_mm_free(a);
是的,您可以对未对齐的 loads/stores(). If the compiler doesn't 、AVX _mm256_loadu
/storeu
使用 _mm256_loadu_ps
/ storeu
恰好对齐与对齐所需的 load/store 一样快,因此在方便时 对齐数据 仍然可以为通常 运行 的函数提供两全其美的功能在对齐的数据上,但让硬件处理它们不处理的罕见情况。(而不是总是 运行 额外的指令来检查东西)。
对齐对于 512 位 AVX-512 向量尤其重要,例如 SKX 上的速度为 15% 到 20%,即使在您认为 L3 / DRAM 带宽成为瓶颈的大型阵列上,相比之下只有几个百分点用于大型阵列的 AVX2 CPU。 (如果您的数据在 L2 或特别是 L1d 缓存中很热,那么对于现代 CPU 上的 AVX2 仍然很重要,特别是如果您可以接近每个时钟最大化 2 个负载 and/or 1 个存储。缓存行拆分成本约为吞吐量资源的两倍,加上临时需要一个行拆分缓冲区。)
标准分配器通常只与 alignof(max_align_t)
对齐,通常是 16B,例如long double
在 x86-64 系统 V ABI 中。但在某些 32 位 ABI 中,它只有 8B,因此它甚至不足以动态分配对齐的 __m128
向量,您需要超越简单地调用 new
或 malloc
.
静态和自动存储很容易:使用alignas(32) float arr[N];
C++17 提供了 aligned 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<__m256>
这样的东西也会崩溃,而不仅仅是 std::vector<int>
,除非你运气好,它恰好对齐 32。
Plain-delete
float
/ int
数组的兼容分配:
不幸的是,auto* arr = new alignas(32) float[numSteps]
并不适用于所有编译器,因为 alignas
适用于变量、成员或 class 声明,但不适用于类型修饰符。 (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[]
and std::align_val_t
的文档
其他选项,与new
/delete
不兼容
动态分配的其他选项大多兼容malloc
/free
,不 new
/delete
:
std::aligned_alloc
: ISO C++17. major downside: size must be a multiple of alignment. This braindead requirement makes it inappropriate for allocating a 64B cache-line aligned array of an unknown number of float
s, for example. Or especially a 2M-aligned array to take advantage of transparent hugepages.
aligned_alloc
was added in ISO C11. It's available in some but not all C++ compilers. As noted on the cppreference page, the C11 version wasn't required to fail when size isn't a multiple of alignment (it's undefined behaviour), so many implementations provided the obvious desired behaviour as an "extension". Discussion is underway to fix this 的 C 版本,但目前我还不能真正推荐 aligned_alloc
作为分配任意大小数组的可移植方式。在实践中,一些实现在 UB / required-to-fail 情况下工作正常,因此它可能是一个很好的不可移植选项。
此外,评论者报告它在 MSVC++ 中不可用。请参阅 best cross-platform method to get aligned memory 以获得 Windows 的可行 #ifdef
。但是据我所知,没有 Windows 对齐分配函数可以生成与标准 free
.
兼容的指针
posix_memalign
:POSIX 2001 的一部分,不是任何 ISO C 或 C++ 标准。笨重 prototype/interface 与 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_free
和 free
是兼容的,但不保证可移植。 (与其他两个不同,它会在 运行 时失败,而不是编译时。)在 Windows 上的 MSVC 上,_mm_malloc
使用 _aligned_malloc
,这与free
;它在实践中崩溃了。
直接使用mmap
or VirtualAlloc
这样的系统调用。适用于大型分配,根据定义,您获得的内存是页面对齐的(4k,甚至可能是 2M 大页面)。 不兼容free
;您当然必须使用需要大小和地址的 munmap
或 VirtualFree
。 (对于大型分配,您通常希望在完成后将内存交还给 OS,而不是管理一个空闲列表;glibc malloc 使用 mmap/munmap 直接用于 malloc/free 个块一定的大小阈值。)
主要优势:您不必处理 C++,C 的脑残拒绝为对齐分配器提供 grow/shrink 便利。如果你想要 space 在分配后再获得 1MiB,你甚至可以使用 Linux 的 mremap(MREMAP_MAYMOVE)
让它在虚拟地址 space 中选择不同的位置(如果需要)对于相同的物理页面,无需复制任何内容。或者,如果它不必移动,则当前使用部分的 TLB 条目保持有效。
并且由于您无论如何都在使用 OS 系统调用(并且知道您正在处理整个页面),因此您可以优先使用 madvise(MADV_HUGEPAGE)
to hint that transparent hugepages,或者不使用,因为这一系列的匿名页面。您还可以使用 mmap
的分配提示,例如对于 OS 预置零页面,或者如果在 hugetlbfs 上映射文件,使用 2M 或 1G 页面。 (如果该内核机制仍然有效)。
并且使用 madvise(MADV_FREE)
,您可以保持它的映射,但让内核在发生内存压力时回收页面,如果发生这种情况,它就像延迟分配的零支持页面一样。所以如果你很快就重用它,你可能不会遇到新的页面错误。但如果你不这样做,你就不会霸占它,当你阅读它时,它就像一个新映射的区域。
alignas()
数组/结构
在 C++11 及更高版本中:使用 alignas(32) float avx_array[1234]
作为 struct/class 成员的第一个成员(或直接在普通数组上),因此该类型的静态和自动存储对象将具有32B对齐。 std::aligned_storage
documentation 有一个这种技术的例子来解释 std::aligned_storage
的作用。
直到 C++17 动态分配存储(如 std::vector<my_class_with_aligned_member_array>
),这才真正起作用,参见 Making std::vector allocate aligned memory。
从 C++17 开始,编译器将为 alignas
对整个类型或其成员强制对齐的类型选择对齐 new
,std::allocator
也会选择对齐new
用于此类,因此在创建此类 std::vector
时无需担心。
最后,最后一个选项太糟糕了,它甚至不在列表中:分配一个更大的缓冲区,然后 p+=31; p&=~31ULL
进行适当的转换。太多的缺点(难以释放,浪费内存)值得讨论,因为对齐分配函数在支持 Intel _mm256_...
内在函数的每个平台上都可用。但是如果你坚持的话,IIRC 甚至还有库函数可以帮助你做到这一点。
使用 _mm_free
而不是 free
的要求可能部分存在于使用此技术在普通旧 malloc
之上实施 _mm_malloc
的可能性。或者对于使用备用空闲列表的对齐分配器。
您需要对齐的分配器。
但没有理由不能将它们捆绑在一起:
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);
}
now mm_ptr<float[], 4>
是指向 4 字节对齐的 float
数组的唯一指针。您通过 make_aligned<float[], 4>(20)
创建它,它创建 20 个 4 字节对齐的浮点数,或 make_aligned<float[20], 4>()
(仅在该语法中的编译时常量)。 make_aligned<float[20],4>
returns mm_ptr<float[],4>
不是 mm_ptr<float[20],4>
.
A mm_ptr<float[], 8>
可以移动构造一个 mm_ptr<float[],4>
但反之则不行,我认为这很好。
mm_ptr<float[]>
可以采用任何对齐方式,但保证 none.
与 std::unique_ptr
一样,每个指针的开销基本上为零。积极的 inline
ing.
可以最大限度地减少代码开销
我在使用 ymm
寄存器时遇到对齐问题,一些代码片段对我来说似乎没问题。这是一个最小的工作示例:
#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);
}
我是不是在做什么傻事?解决方法是什么?
内存管理有两个内部函数。 _mm_malloc 像标准 malloc 一样运行,但它需要一个额外的参数来指定所需的对齐方式。在这种情况下,32 字节对齐。使用此分配方法时,必须通过相应的 _mm_free 调用释放内存。
float *a = static_cast<float*>(_mm_malloc(sizeof(float) * ss , 32));
...
_mm_free(a);
是的,您可以对未对齐的 loads/stores(_mm256_loadu
/storeu
使用 _mm256_loadu_ps
/ storeu
恰好对齐与对齐所需的 load/store 一样快,因此在方便时 对齐数据 仍然可以为通常 运行 的函数提供两全其美的功能在对齐的数据上,但让硬件处理它们不处理的罕见情况。(而不是总是 运行 额外的指令来检查东西)。
对齐对于 512 位 AVX-512 向量尤其重要,例如 SKX 上的速度为 15% 到 20%,即使在您认为 L3 / DRAM 带宽成为瓶颈的大型阵列上,相比之下只有几个百分点用于大型阵列的 AVX2 CPU。 (如果您的数据在 L2 或特别是 L1d 缓存中很热,那么对于现代 CPU 上的 AVX2 仍然很重要,特别是如果您可以接近每个时钟最大化 2 个负载 and/or 1 个存储。缓存行拆分成本约为吞吐量资源的两倍,加上临时需要一个行拆分缓冲区。)
标准分配器通常只与 alignof(max_align_t)
对齐,通常是 16B,例如long double
在 x86-64 系统 V ABI 中。但在某些 32 位 ABI 中,它只有 8B,因此它甚至不足以动态分配对齐的 __m128
向量,您需要超越简单地调用 new
或 malloc
.
静态和自动存储很容易:使用alignas(32) float arr[N];
C++17 提供了 aligned 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<__m256>
这样的东西也会崩溃,而不仅仅是 std::vector<int>
,除非你运气好,它恰好对齐 32。
Plain-delete
float
/ int
数组的兼容分配:
不幸的是,auto* arr = new alignas(32) float[numSteps]
并不适用于所有编译器,因为 alignas
适用于变量、成员或 class 声明,但不适用于类型修饰符。 (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[]
and std::align_val_t
其他选项,与new
/delete
不兼容
动态分配的其他选项大多兼容malloc
/free
,不 new
/delete
:
std::aligned_alloc
: ISO C++17. major downside: size must be a multiple of alignment. This braindead requirement makes it inappropriate for allocating a 64B cache-line aligned array of an unknown number offloat
s, for example. Or especially a 2M-aligned array to take advantage of transparent hugepages.aligned_alloc
was added in ISO C11. It's available in some but not all C++ compilers. As noted on the cppreference page, the C11 version wasn't required to fail when size isn't a multiple of alignment (it's undefined behaviour), so many implementations provided the obvious desired behaviour as an "extension". Discussion is underway to fix this 的 C 版本,但目前我还不能真正推荐aligned_alloc
作为分配任意大小数组的可移植方式。在实践中,一些实现在 UB / required-to-fail 情况下工作正常,因此它可能是一个很好的不可移植选项。此外,评论者报告它在 MSVC++ 中不可用。请参阅 best cross-platform method to get aligned memory 以获得 Windows 的可行
兼容的指针#ifdef
。但是据我所知,没有 Windows 对齐分配函数可以生成与标准free
.posix_memalign
:POSIX 2001 的一部分,不是任何 ISO C 或 C++ 标准。笨重 prototype/interface 与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_free
和free
是兼容的,但不保证可移植。 (与其他两个不同,它会在 运行 时失败,而不是编译时。)在 Windows 上的 MSVC 上,_mm_malloc
使用_aligned_malloc
,这与free
;它在实践中崩溃了。直接使用
mmap
orVirtualAlloc
这样的系统调用。适用于大型分配,根据定义,您获得的内存是页面对齐的(4k,甚至可能是 2M 大页面)。 不兼容free
;您当然必须使用需要大小和地址的munmap
或VirtualFree
。 (对于大型分配,您通常希望在完成后将内存交还给 OS,而不是管理一个空闲列表;glibc malloc 使用 mmap/munmap 直接用于 malloc/free 个块一定的大小阈值。)主要优势:您不必处理 C++,C 的脑残拒绝为对齐分配器提供 grow/shrink 便利。如果你想要 space 在分配后再获得 1MiB,你甚至可以使用 Linux 的
mremap(MREMAP_MAYMOVE)
让它在虚拟地址 space 中选择不同的位置(如果需要)对于相同的物理页面,无需复制任何内容。或者,如果它不必移动,则当前使用部分的 TLB 条目保持有效。并且由于您无论如何都在使用 OS 系统调用(并且知道您正在处理整个页面),因此您可以优先使用
madvise(MADV_HUGEPAGE)
to hint that transparent hugepages,或者不使用,因为这一系列的匿名页面。您还可以使用mmap
的分配提示,例如对于 OS 预置零页面,或者如果在 hugetlbfs 上映射文件,使用 2M 或 1G 页面。 (如果该内核机制仍然有效)。并且使用
madvise(MADV_FREE)
,您可以保持它的映射,但让内核在发生内存压力时回收页面,如果发生这种情况,它就像延迟分配的零支持页面一样。所以如果你很快就重用它,你可能不会遇到新的页面错误。但如果你不这样做,你就不会霸占它,当你阅读它时,它就像一个新映射的区域。
alignas()
数组/结构
在 C++11 及更高版本中:使用 alignas(32) float avx_array[1234]
作为 struct/class 成员的第一个成员(或直接在普通数组上),因此该类型的静态和自动存储对象将具有32B对齐。 std::aligned_storage
documentation 有一个这种技术的例子来解释 std::aligned_storage
的作用。
直到 C++17 动态分配存储(如 std::vector<my_class_with_aligned_member_array>
),这才真正起作用,参见 Making std::vector allocate aligned memory。
从 C++17 开始,编译器将为 alignas
对整个类型或其成员强制对齐的类型选择对齐 new
,std::allocator
也会选择对齐new
用于此类,因此在创建此类 std::vector
时无需担心。
最后,最后一个选项太糟糕了,它甚至不在列表中:分配一个更大的缓冲区,然后 p+=31; p&=~31ULL
进行适当的转换。太多的缺点(难以释放,浪费内存)值得讨论,因为对齐分配函数在支持 Intel _mm256_...
内在函数的每个平台上都可用。但是如果你坚持的话,IIRC 甚至还有库函数可以帮助你做到这一点。
使用 _mm_free
而不是 free
的要求可能部分存在于使用此技术在普通旧 malloc
之上实施 _mm_malloc
的可能性。或者对于使用备用空闲列表的对齐分配器。
您需要对齐的分配器。
但没有理由不能将它们捆绑在一起:
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);
}
now mm_ptr<float[], 4>
是指向 4 字节对齐的 float
数组的唯一指针。您通过 make_aligned<float[], 4>(20)
创建它,它创建 20 个 4 字节对齐的浮点数,或 make_aligned<float[20], 4>()
(仅在该语法中的编译时常量)。 make_aligned<float[20],4>
returns mm_ptr<float[],4>
不是 mm_ptr<float[20],4>
.
A mm_ptr<float[], 8>
可以移动构造一个 mm_ptr<float[],4>
但反之则不行,我认为这很好。
mm_ptr<float[]>
可以采用任何对齐方式,但保证 none.
与 std::unique_ptr
一样,每个指针的开销基本上为零。积极的 inline
ing.