如何在GCC中将SIMD整型向量转换为浮点型?

7

我正在为一个项目使用GCC SIMD向量扩展,一切工作得相当顺利,但是转换操作会重置向量的所有分量。

手册指出:

可以从一个向量类型转换到另一个向量类型,前提是它们具有相同的大小(事实上,您还可以将向量与具有相同大小的其他数据类型进行转换)。

以下是一个简单的示例:

#include <stdio.h>

typedef int int4 __attribute__ (( vector_size( sizeof( int ) * 4 ) ));
typedef float float4 __attribute__ (( vector_size( sizeof( float ) * 4 ) ));

int main()
{
    int4 i = { 1 , 2 , 3 , 4 };
    float4 f = { 0.1 , 0.2 , 0.3 , 0.4 };

    printf( "%i %i %i %i\n" , i[0] , i[1] , i[2] , i[3] );
    printf( "%f %f %f %f\n" , f[0] , f[1] , f[2] , f[3] );

    f = ( float4 )i;

    printf( "%f %f %f %f\n" , f[0] , f[1] , f[2] , f[3] );
}

使用gcc cast.c -O3 -o cast编译并在我的机器上运行,我得到了以下结果:

1 2 3 4
0.100000 0.200000 0.300000 0.400000
0.000000 0.000000 0.000000 0.000000 <-- no no no

我不是汇编大师,但我在这里看到了一些字节移动:

[...]
400454:       f2 0f 10 1d 1c 02 00    movsd  0x21c(%rip),%xmm3
40045b:       00 
40045c:       bf 49 06 40 00          mov    $0x400649,%edi
400461:       f2 0f 10 15 17 02 00    movsd  0x217(%rip),%xmm2
400468:       00 
400469:       b8 04 00 00 00          mov    $0x4,%eax
40046e:       f2 0f 10 0d 12 02 00    movsd  0x212(%rip),%xmm1
400475:       00 
400476:       f2 0f 10 05 12 02 00    movsd  0x212(%rip),%xmm0
40047d:       00 
40047e:       48 83 c4 08             add    $0x8,%rsp
400482:       e9 59 ff ff ff          jmpq   4003e0 
我怀疑这个标量的向量等效物:
*( int * )&float_value = int_value;

你怎么解释这种行为?


3
是的,看起来正在发生位运算转换(或者更确切地说,根本没有进行转换)。因此,您将获得4个非规范化浮点数,而不是实际值的转换。 - Mysticial
1
这就是向量转换的定义(其他任何方式都会完全疯狂,并且会使标准向量编程习惯非常难写)。如果您想要实际进行转换,您可能需要使用某种内置函数,例如 _mm_cvtepi32_ps(当然,这会破坏您的向量代码的良好架构独立性,这也很烦人;一种常见的方法是使用一个翻译头文件,定义一个可移植的“内置函数”集)。 - Stephen Canon
我理解你的观点,但问题是:这种_cast_何时会有用? - cYrus
3
信不信由你,我实际上比起值转换更经常使用这种(按位)转换。 - Mysticial
@user877329 在新版本的GCC中,你需要进行强制类型转换和内置函数调用(这是GCC开发人员的低级错误,但事实就是如此):_mm_cvtepi32_ps((__m128i)x) - Stephen Canon
显示剩余2条评论
3个回答

9
那就是向量强制转换的定义(其他方式完全不合理,会使标准向量编程惯用语非常难写)。如果您想要实际进行转换,您可能需要使用某种内在功能,例如_mm_cvtepi32_ps(当然,这会破坏您的向量代码的良好架构独立性,这也很让人烦恼;一种常见的方法是使用翻译头文件,该文件定义了一组可移植的“内在功能”)。
为什么这很有用? 有各种各样的原因,但以下是最大的原因:
在向量代码中,你几乎永远不想分支。相反,如果您需要有条件地执行某些操作,则通过逐个地评估条件两侧,并使用掩码逐个选择适当的结果。这些掩码向量通常具有整数类型,而您的数据向量通常是浮点数。你想使用逻辑运算将两者结合起来。如果向量强制转换只是重新解释位,那么这种极其常见的惯用语就最自然不过了。
当然,可以避免此类情况或任何其他常见的向量惯用语之一,但“向量是一个位包”的观点非常普遍,反映了大多数向量程序员的思维方式。

2
你可以直接遍历元素,将int类型转换为float类型。
float4 cast(int4 x) {
    float4 y;
    for(int i=0; i<4; i++) y[i] = x[i];
    return y;
}

GCC、Clang和ICC都会为此生成一条指令cvtdq2ps xmm0, xmm0

https://godbolt.org/g/KU1aPg


2
事实上,在您的情况下,甚至没有生成单个向量指令,也没有在运行时执行任何类型转换。由于开启了-O3开关,所有操作都是在编译时完成的。这四条MOVSD指令实际上是将预转换的参数加载到printf中。根据SysV AMD64 ABI,浮点参数传递在XMM寄存器中。您已经反汇编的部分是(使用-S编译得到的汇编代码):
    movsd   .LC6(%rip), %xmm3
    movl    $.LC5, %edi
    movsd   .LC7(%rip), %xmm2
    movl    $4, %eax
    movsd   .LC8(%rip), %xmm1
    movsd   .LC9(%rip), %xmm0
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    jmp     printf
    .cfi_endproc

.LC5 标记格式字符串:

.LC5:
    .string "%f %f %f %f\n"

指向格式字符串的指针属于INTEGER类,因此通过RDI寄存器传递(在VA空间的前4 GiB中的某个位置,通过向RDI的低部发出32位移动,可以节省一些代码字节)。寄存器RAX(使用EAX可节省代码字节)加载了通过XMM寄存器传递的参数数量(再次根据SysV AMD64 ABI用于调用具有可变参数数量的函数)。所有四个MOVSD(MOVe Scalar Double-precision)将对应的参数移动到XMM寄存器中。例如,.LC9标记了两个双字:
    .align 8
.LC9:
    .long   0
    .long   916455424

这两个数形成了64位的quadword 0x36A0000000000000,在64位IEEE 754表示中恰好为2-149。在非规格化的32位IEEE 754中,它看起来像是0x00000001,因此确实是整数1的无转换(但由于printf需要double参数,它仍然被预转换为双精度)。第二个参数是:

    .align 8
.LC8:
    .long   0
    .long   917504000

这是0x36B0000000000000或64位IEEE 754中的2-148,以及非规格化32位IEEE 754中的0x00000002。其他两个参数也是一样。

请注意,上面的代码没有使用单个堆栈变量 - 它仅使用预先计算的常量。这是由于使用了非常高的优化级别(-O3)。如果您使用较低的优化级别(-O2或更低)进行编译,则会发生实际的运行时转换。然后发出以下代码来执行类型转换:

    movaps  -16(%rbp), %xmm0
    movaps  %xmm0, -32(%rbp)

这只是将四个整数值移动到浮点向量的相应槽中,因此没有任何转换。然后对于每个元素,执行一些SSE mumbo-jumbo以将其从单精度转换为双精度(printf所期望的)。

    movss   -20(%rbp), %xmm0
    unpcklps        %xmm0, %xmm0
    cvtps2pd        %xmm0, %xmm3

(为什么不直接使用CVTSS2SD超出了我对SSE指令集的理解)

(说明:该段内容是关于IT技术中SSE指令集的翻译)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接