C语言 为什么__m128会导致float x/y/z联合中的对齐问题?

9q78igpj  于 2023-06-05  发布在  其他
关注(0)|答案(1)|浏览(217)

我以前从来没有遇到过这个问题,至少我没有意识到...但是我正在我的一些代码中进行一些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内存分配器),但它仍然会因访问冲突而崩溃,这进一步增加了我的脱发。

3qpi33ja

3qpi33ja1#

alignof(your_union)包含__m128成员时,alignof(your_union)是16,因此编译器将使用movapsmovdqa,因为您已经向它们承诺数据是对齐的。否则alignof(your_union)只有4(从float继承而来,所以它们将使用没有对齐要求的movupsmovdqu
这仍然是未定义对齐的行为,正如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 posvec vel成员继承了alignof(T),而sizeof(T)必须是alignof(T)的倍数(所以数组可以工作)。

相关问题