是的,您可以使用 _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 向量的动态分配甚至不够,您需要超越简单地调用new 或malloc。
静态和自动存储很容易:使用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<__m256> 这样的东西也会损坏,而不仅仅是 std::vector<int>,除非你很幸运并且它恰好是 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/free,不new/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_free 和 free 是兼容的,但不能保证可移植。 (与其他两个不同,它将在运行时失败,而不是编译时失败。)在 Windows 上的 MSVC 上,_mm_malloc 使用 _aligned_malloc,它与 free 不兼容;它在实践中崩溃了。
-
直接使用mmap或VirtualAlloc等系统调用。适用于大型分配,并且您获得的内存根据定义是页面对齐的(4k,甚至可能是 2M 大页面)。 不兼容free;您当然必须使用需要大小和地址的munmap 或VirtualFree。 (对于大型分配,您通常希望在完成后将内存交还给操作系统,而不是管理空闲列表;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<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 的可能性。或者对于使用备用空闲列表的对齐分配器。