C++ 为什么gcc/clang使用两个128位xmm寄存器来传递单个值?

C++ 为什么gcc/clang使用两个128位xmm寄存器来传递单个值?,c++,c,assembly,clang,sse,C++,C,Assembly,Clang,Sse,所以我偶然发现了一些我想理解的东西,因为它让我头疼。我有以下代码: #include <stdio.h> #include <smmintrin.h> typedef union { struct { float x, y, z, w; } v; __m128 m; } vec; vec __attribute__((noinline)) square(vec a) { vec x = { .m = _mm_mul_ps(a.m, a.m) }

所以我偶然发现了一些我想理解的东西,因为它让我头疼。我有以下代码:

#include <stdio.h>
#include <smmintrin.h>

typedef union {
    struct { float x, y, z, w; } v;
    __m128 m;
} vec;

vec __attribute__((noinline)) square(vec a)
{
    vec x = { .m = _mm_mul_ps(a.m, a.m) };
    return x;
}

int main(int argc, char *argv[])
{
    float f = 4.9;
    vec a = (vec){f, f, f, f};
    vec res = square(a); // ?
    printf("%f %f %f %f\n", res.v.x, res.v.y, res.v.z, res.v.w);
    return 0;
}
#包括
#包括
typedef联合{
结构{float x,y,z,w;}v;
__m128米;
}vec;
向量属性((noinline))平方(向量a)
{
vec x={.m={mm\u mul\u ps(a.m,a.m)};
返回x;
}
int main(int argc,char*argv[])
{
浮点数f=4.9;
向量a=(向量){f,f,f};
vec res=平方(a);/?
printf(“%f%f%f%f\n”,res.v.x,res.v.y,res.v.z,res.v.w);
返回0;
}
现在,在我看来,在
main
中调用
square
应该将
a
的值放在
xmm0
中,这样
square
函数就可以执行
mulps xmm0,xmm0
并用它来完成

这不是我使用clang或gcc编译时发生的情况。相反,
a
的前8个字节放在
xmm0
中,接下来的8个字节放在
xmm1
中,这使得
square
的功能更加复杂,因为它需要修补备份

知道为什么吗

注:这是与-O3优化


经过进一步研究,它似乎与联合类型有关。如果函数采用直线_m128,则生成的代码将在单个寄存器(xmm0)中预期值。但是考虑到它们都应该适合xmm0,我不明白为什么在使用
vec
类型时它会被拆分为两个半使用的寄存器。

编辑:在第二次通读时,我不太确定为什么会发生这种情况,但我更确定这就是发生的地方。我认为这个答案不正确,但我还是不说了,因为它可能会有帮助

只为叮当声说话:

这似乎是一个问题,只是编译器启发式的一个不幸的副作用

从clang(文件
CGRecordLayoutBuilder.cpp
,函数)的简要介绍中可以看出,llvm在内部并不表示这样的联合类型,函数的类型也不会根据函数中的用途而改变

clang查看了您的函数,发现它需要16字节的参数作为类型签名,然后使用启发式方法选择它认为最好的类型。它支持
{double,double}
解释而不是
(这将使它在您的情况下获得最大的效率),因为double在对齐方面更为宽松

我对铿锵的内部结构不是专家,所以我可能会大错特错,但看起来并没有一个特别好的方法来解决这个问题。如果您想要优化版本,您可能必须使用指针转换而不是联合来获得它

我怀疑是代码导致了问题:

void CGRecordLowering::lowerUnion() {
    ...
    // Conditionally update our storage type if we've got a new "better" one.
    if (!StorageType ||
        getAlignment(FieldType) >  getAlignment(StorageType) ||
        (getAlignment(FieldType) == getAlignment(StorageType) &&
        getSize(FieldType) > getSize(StorageType)))
      StorageType = FieldType;
    ...
}

编译器只是试图遵循System V应用程序二进制接口AMD64体系结构处理器补充,第3.2.3节参数传递中指定的调用约定

有关要点如下:

我们首先定义一些类来对参数进行分类。这个
类与AMD64寄存器类相对应,定义如下:
SSE类由适合向量寄存器的类型组成。
SSEUP类由适合向量寄存器的类型组成,并且可以
将以其高位字节传递和返回。
每个参数的大小被四舍五入到八字节。
基本类型被指定为其自然类:
float、double、_Decimal32、_Decimal64和uu m64类型的参数是
在课堂上。
聚合(结构和数组)和联合类型的分类
工作内容如下:
如果骨料的大小超过一个八字节,则每个八字节
单独分类。

应用上述规则意味着嵌入式结构的
x,y
z,w
对被分别归类为
SSE
类,这反过来意味着它们必须在两个单独的寄存器中传递。在这种情况下,
m
成员的存在没有任何效果,您甚至可以删除它。

您是否使用过其他命令行选项?总是值得尝试打开/关闭,看看它如何影响生成的代码。我会尝试,有什么建议吗?有这么多的开关。我不熟悉
clang
,但在GCC上有很多特定于x86的
x86,许多与SSE有关。还有,优化和调试标志呢?老实说,我很惊讶它没有使用四个寄存器,因为这就是它传递四个浮点的方式。由于您使用的是gcc,我建议您使用,而不是带有四个浮点数的结构。如果您使用一个直的uum128,甚至是一个包含单个uum128的结构,那么它将按照ABI在xmm0中传递它。但我认为ABI坚持认为工会应该通过谈判(尽管我很可能错了)。这似乎是答案。我想第一点可能会优先,因为它适合向量寄存器。有趣。