我以前从来没有遇到过这个问题,至少我没有意识到...但是在我的一些代码中,我正在进行一些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;
}

我不明白为什么浮点型x/y/z和浮点型[4]的并集与未对齐的内存地址一起工作,但是浮点型x/y/z和__m128的并集会产生访问冲突. 我知道__m128类型上有一些额外的对齐规范,但总体联合大小没有变化,而且它也是16字节对齐的,所以这有什么关系呢?

我确实理解内存对齐的重要性,但更奇怪的是,我在分配令人不快的未对齐内存的代码中添加了aligned_malloc(我在代码中使用了片/区域内存分配器),但它仍然因访问冲突而崩溃,这进一步加剧了我的脱发.

推荐答案

alignof(your_union)包含__m128成员时是16,因此编译器将使用movapsmovdqa,因为您已经向他们promise 数据是对齐的.否则,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)对齐,这绝对是一个偶数,在x86-64中通常是16.

通常,即使您使用_mm_store_ps,MSVC也只使用未对齐的movdqu/movups加载/存储.(但需要对齐的内部函数将允许它将加载合并到用于addps xmm0, [rcx]等非AVX指令的内存源操作数中).

但显然,MSVC对待聚集体的方式与对待__m128*岁的迪夫的方式不同.

所以你的类型是alignof(T) == 16,因此你的代码有UB对齐,所以它可以并且确实编译成出错的ASM.


顺便说一句,我不建议使用这种联合;尤其是对于函数args/返回值,因为作为聚合的一部分可能会使调用约定对它的处理效率降低.(在MSVC上,如果没有内联,则必须使用vectorcall将其传递到寄存器中,但x86-64 System V通常会在向量正则中传递向量参数,如果它们不是联合的一部分.)

使用__m128个向量和编写帮助器函数,以标量形式输入/输出数据.

理想情况下,不要使用1个SIMD向量来保存1个几何向量,这是一种反模式,因为它会导致大量的洗牌.最好有x数组、y数组和z数组,这样您就可以加载3个数据向量并并行处理4个向量,而不会出现混乱.(数组 struct 而不是 struct 数组).见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位置以及接下来的任何位置.有时,您可能不得不使用额外的指令来清零高位元素,但很可能大多数时候,您可能会以不关心它的方式洗牌.

实际上,当您有__m128个成员时,struct particle是48个字节,因为它继承了vec posvec vel个成员的alignof(T),而sizeof(T)必须是alignof(T)的倍数(所以数组是有效的).

C++相关问答推荐

C中的ATONE会扰乱SEN/CLUTE GMS应用程序中的其他字符串

有效地计算由一组点构成的等边三角形和等腰三角形的数量

ATmega328P EEPROM未写入

将uintptr_t添加到指针是否对称?

#定义SSL_CONNECTION_NO_CONST

在C中包装两个数组?

C-try 将整数和 struct 数组存储到二进制文件中

Valgrind用net_pton()抱怨

c程序,让用户输入两类数字,并给出输出用户输入多少个数字

unions 的原子成员是个好主意吗?

可以';t从A9G模块拨打电话

为什么这个代码的最后一次迭代不能正常工作?

从系统派生线程调用CRT

如何在Rust中处理C的longjmp情况?

程序打印一些随机空行

中位数和众数不正确

memcmp 是否保证按顺序比较字节?

OpenGL 中的非渐变 colored颜色 变化

初始化动态分配的布尔二维数组的最佳方法是什么?

如何让 unlinkat(dir_fd, ".", AT_REMOVEDIR) 工作?