我正在为项目使用GCC SIMD向量扩展,一切都运行得非常好但是强制转换,它们只是重置向量的所有组件。
manual州:
可以从一种矢量类型转换为另一种矢量类型,只要它们具有相同的大小(事实上,您也可以将矢量转换为相同大小的其他数据类型)。
这是一个简单的例子:
#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;
你怎么解释这种行为?
答案 0 :(得分:9)
这就是定义要执行的矢量转换(其他任何东西都会完全疯狂,并且会使标准的矢量编程习惯用起来非常痛苦)。如果你想真正获得转换,你可能想要使用某种类型的内在函数,比如_mm_cvtepi32_ps(这会打破你的矢量代码的漂亮的架构独立性,当然,这也很烦人;一种常见的方法是使用一个定义便携式“内在函数”的翻译标题。)
为什么这有用?各种原因,但这是最大的原因:
在矢量代码中,您几乎不想分支。相反,如果您需要有条件地执行某些操作,则评估条件的两侧,并使用蒙版来选择适当的结果。这些掩码矢量“自然地”具有整数类型,而您的数据矢量通常是浮点数;你想要使用逻辑运算来组合这两者。如果向量转换只是重新解释这些位,这种非常常见的习语是最自然的。
当然,可以解决这个案例,或任何一些其他常见的矢量习语,但“矢量是一个包位”视图是非常常见的,并反映了大多数矢量程序员的想法。
答案 1 :(得分: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中的某个位置,通过发出一个代码字节来保存) 32位移动到RDI
的下半部分。寄存器RAX
(用于保存代码字节的EAX
)加载了XMM寄存器中传递的参数个数(同样根据SysV AMD64 ABI调用具有可变数量参数的函数)。所有四个MOVSD
(MOVe标量双精度)都移动XMM寄存器中的相应参数。 .LC9
例如标记了两个双字:
.align 8
.LC9:
.long 0
.long 916455424
这两个形成64位四字0x36A0000000000000
,在64位IEEE 754表示中恰好是2 -149 。在非规范化的32位IEEE 754中,它看起来像0x00000001
,所以实际上它是整数1
的无转换(但由于printf
期望double
参数,它仍然是预转换的精度加倍)。第二个论点是:
.align 8
.LC8:
.long 0
.long 917504000
这是64位IEEE 754中的0x36B0000000000000
或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指令集的理解)
答案 2 :(得分: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
。