我以前从来没有遇到过这个问题,至少我没有意识到...但是我正在我的一些代码中进行一些SIMD向量优化,我遇到了一些对齐问题。
下面是我在MSVC(Visual Studio 2022)上重现问题的一些最小代码:
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <xmmintrin.h>
_declspec(align(16)) typedef union
{
struct { float x, y, z; };
#if 0
// This works:
float v[4];
#else
// This does not:
__m128 v;
#endif
} vec;
typedef struct
{
vec pos;
vec vel;
float radius;
} particle;
int main(int argc, char **argv)
{
particle *particles=malloc(sizeof(particle)*10);
if(particles==NULL)
return -1;
// intentionally misalign the pointer
((uint8_t *)particles)+=3;
printf("misalignment: %lld\n", (uintptr_t)particles%16);
particles[0].pos=(vec){ 1.0f, 2.0f, 3.0f };
particles[0].vel=(vec){ 4.0f, 5.0f, 6.0f };
printf("pos: %f %f %f\nvel: %f %f %f\n",
particles[0].pos.x, particles[0].pos.y, particles[0].pos.z,
particles[0].vel.x, particles[0].vel.y, particles[0].vel.z);
return 0;
}
我不明白为什么float x/y/z和float[4]的联合可以处理未对齐的内存地址,但是float x/y/z和__m128
的联合会产生访问冲突。我知道__m128
类型有一些额外的对齐规范,但总体联合大小没有改变,而且它也是16字节对齐的,所以这有什么关系呢?
我确实理解内存对齐的重要性,但额外奇怪的部分是,我在代码中添加了一个aligned_malloc
,它正在分配令人不快的未对齐内存(我在代码中使用了slab/zone内存分配器),但它仍然会因访问冲突而崩溃,这进一步增加了我的脱发。
1条答案
按热度按时间3qpi33ja1#
当
alignof(your_union)
包含__m128
成员时,alignof(your_union)
是16,因此编译器将使用movaps
或movdqa
,因为您已经向它们承诺数据是对齐的。否则alignof(your_union)
只有4(从float
继承而来,所以它们将使用没有对齐要求的movups
或movdqu
。这仍然是未定义对齐的行为,正如
gcc -fsanitize=undefined
会告诉你的,因为你使用的地址甚至没有按4对齐。https://godbolt.org/z/6GxebxT7r显示MSVC正在为您的代码使用
movdqa
存储,如movdqa [rbx+19], xmm2
,其中RBX保存malloc返回值。这肯定是错误的,因为malloc
返回值由alignof(max_align_t)
对齐,alignof(max_align_t)
肯定是偶数,在x86-64中通常是16。MSVC通常只使用未对齐的
movdqu
/movups
加载/存储,即使您使用_mm_store_ps
。(但是需要对齐的intrinsic将允许它将加载折叠到非AVX指令(如addps xmm0, [rcx]
)的内存源操作数中)。但显然MSVC对待聚合的方式与
__m128*
的deref不同。所以你的类型有
alignof(T) == 16
,因此你的代码有对齐UB,所以它可以编译成错误的asm。顺便说一句,我不建议使用这个联盟;特别是对于函数args / return值,因为作为聚合的一部分可能会使调用约定处理它的效率降低。(在MSVC上,如果它没有内联,你必须使用
vectorcall
来让它在寄存器中传递,但是x86-64 System V通常在vector regs中传递vector args,如果它们不是union的一部分。使用
__m128
vectors并编写helper函数来将数据作为标量输入/输出。理想情况下,不要使用1个SIMD向量来保存1个几何向量,这是一种反模式,因为它会导致大量的 Shuffle 。最好有x数组、y数组和z数组,这样你就可以加载3个数据向量,并行处理4个向量,而不会出现混乱。(Struct-of-Arrays而不是Array-of-Structs)。参见https://stackoverflow.com/tags/sse/info,尤其是https://deplinenoise.wordpress.com/2015/03/06/slides-simd-at-insomniac-games-gdc-2015/
如果你真的想这样做,你仍然可以改进它。您的
struct particle
是您定义的36字节,有两个浪费的32位浮点插槽。它可以是32字节:xyz, radius, xyz, zeroed padding
,因此您可以使用alignof(particle) == 16
,而无需将大小增加到48字节,以便能够有效地加载它(永远不会跨越缓存行边界)。半径将沿着_mm_load_ps(&particle->pos_x)
加载为高垃圾,它将获得x,y,z位置以及接下来的任何内容。有时候可能需要使用额外的指令来将高位元素置零,但大多数情况下,您可能会以不关心高位元素的方式进行 Shuffle 。实际上,当你有一个
__m128
成员时,你的struct particle
是48字节,因为它从它的vec pos
和vec vel
成员继承了alignof(T)
,而sizeof(T)
必须是alignof(T)
的倍数(所以数组可以工作)。