为什么GCC避免向量寄存器的多元素联合?



我注意到GCC在给定SIMD矢量类型和任何其他相同大小和相同对齐类型的非矢量类型的联合时生成非常不同(并且效率较低)的代码。

特别是,在这个Godbolt的例子中可以看到,当一个__m128向量类型与一个非向量类型放在一个联合中,这个联合在两个XMM寄存器中传递(每个参数),然后加载到堆栈中用于addps,而不是在一个XMM寄存器中传递并直接用于addps。另一方面,对于仅包含__m128__m128向量本身的联合的其他两种情况,参数和返回值直接传递到XMM寄存器中,而不使用堆栈。

是什么导致了这种差异?有没有一种方法来"强迫"?GCC通过多元素联合在XMM寄存器?

与联盟:

#include <immintrin.h>
#include <array>
union simd
{
__m128 vec;
alignas(__m128) std::array<float, 4> values; 
};
simd add(simd a, simd b) noexcept
{
simd ret;
ret.vec = _mm_add_ps(a.vec, b.vec);
return ret;
}
add(simd, simd):
movq    QWORD PTR [rsp-40], xmm0
movq    QWORD PTR [rsp-32], xmm1
movq    QWORD PTR [rsp-24], xmm2
movq    QWORD PTR [rsp-16], xmm3
movaps  xmm4, XMMWORD PTR [rsp-24]
addps   xmm4, XMMWORD PTR [rsp-40]
movaps  XMMWORD PTR [rsp-40], xmm4
movq    xmm1, QWORD PTR [rsp-32]
movq    xmm0, QWORD PTR [rsp-40]
ret

没有联盟:

__m128 add(__m128 a, __m128 b) noexcept
{
return _mm_add_ps(a, b);
}
add(float __vector(4), float __vector(4)):
addps   xmm0, xmm1
ret

注意,第二种情况也适用于将__m128向量包装在封闭结构体或联合体中的情况。

正如Homer512所怀疑的那样,答案在于AMD64调用约定。

根据System V AMD64 ABI章节3.2.3,每8个字节接收它自己的参数类(小于8个字节的参数被分组在一起或填充)。

对于在单个向量寄存器中传递的参数,它必须由至少一个SSE类和任意数量的SSEUP类组成。SSE类表示寄存器的低阶64位,而SSEUP类表示寄存器的高阶64位。

例如,__m128和其他向量被视为由SSE和SSEUP类组成的多8字节参数,因此它们在单个寄存器中传递。然后,每个标量float被分配SSE参数类,并在寄存器的较低部分传递。

聚合类型(数组、结构体和类)和联合的参数类是根据它们的组成来确定的。

这样,给定一个联合:

union simd
{
__m128 vec;
float vals[4];
};

__m128 vec向量属于特殊情况规则,并且被分类为SSE+SSEUP,使得它可以通过单个寄存器。到目前为止一切顺利。然而,由于float vals[4]数组包含2(独立)8字节块,和每一个8字节块分配SSE类,数组本身反过来列为SSE + SSE,这并不符合SSE + SSEUP需求,进而迫使它通过使用较低的部分2单独的XMM寄存器,随着"最小公分母",导致联盟本身被视为2传入参数和2寄存器。

简单地说,调用约定将数组视为两个单独的8字节参数,因此必须在两个单独的寄存器中传递它,而独立的__m128被视为单个参数,并在单个寄存器中传递。

奇怪的是,这使得后面的并集
union simd
{
__m128 vec;
float vals[2];
};

实际上,被视为SSE+ SSEUP类,因此在单个寄存器中传递。__m128 vec被视为SSE &SSEUP,而float vec[2]被视为单个SSE类。

不幸的是,似乎没有办法显式地向编译器指定(或提示)参数类。

最新更新