当然,在增加了x64扩展这个特性之后,FPU在x86兼容处理器中还是存在的。但是同事,SIMD扩展(SSE, SSE2等)已经有了,他们也可以处理浮点数。数字格式依然相同(使用IEEE754标准)。 所以,x86-64编译器通常都使用SIMD指令。可以说这是一个好消息,因为这让我们可以更容易的使用他们。 24.1 简单的例子 输入的浮点数被传入了XMM0-XMM3寄存器,其他的通过栈来传递。 a被传入了XMM0,b则是通过XMM1。 XMM寄存器是128位的(可以参考SIMD22一节),但是我们的类型是double型的,也就意味着只有一半的寄存器会被使用。 函数处理double的结果将保存在XMM0寄存器中。 这是无优化的MSVC编译器的结果: 有一些繁杂,输入参数保存在“shadow space”(影子空间,7.2.1节),但是只有低一半的寄存器,也即只有64位存了这个double的值。 GCC编译器生成了几乎一样的代码。 24.2 通过参数传递浮点型变量 他们通过XMM0-XMM3的低一半寄存器传递。 在Intel和AMD的手册中(见14章和1章)并没有MOVSDX这个指令,而只有MOVSD一个。所以在x86中有两个指令共享了同一个名字(另一个见B.6.2)。显然,微软的开发者想要避免弄得一团糟,所以他们把它重命名为MOVSDX,它只是会多把一个值载入XMM寄存器的低一半中。 pow()函数从XMM0和XMM1中加载参数,然后返回结果到XMM0中。 然后把值移动到RDX中,因为接下来printf()需要调用这个函数。为什么?老实说我也不知道,也许是因为printf()是一个参数不定的函数? GCC让结果更清晰,printf()的值传入到了XMM0中。顺带一提,这是一个因为printf()才把1写入EAX中的例子。这意味着参数会被传递到向量寄存器中,就像标准需求一样(见21章)。 只有低一半的XMM寄存器会被使用,一组IEEE754格式的数字也会被存在这里。 显然,所有的指令都有SD后缀(标量双精度数),这些操作数是可以用于IEEE754浮点数的,他们存在XMM寄存器的低64位中。 比FPU更简单的是,显然SIMD扩展并不像FPU以前那么混乱,栈寄存器模型也没使用。 如果你像试着将例子中的double替换成float的话,它们还是会使用同样的指令,但是后缀是SS(标量单精度数),例如MOVSS,COMISS,ADDSS等等。 标量(Scalar)代表着SIMD寄存器会包含仅仅一个值,而不是所有的。可以在所有类型的值中生效的指令都被“封装”成同一个名字。 另一个在初学者的编程书中常见的例子是温度转换程序,例如将华氏度转为摄氏度,或者反过来。 我也添加了一个简单的错误处理: 1)我们应该检查用户是否输入了正确的数字 2)我们应该检查摄氏度是否低于-273゜C,因为这比绝对零度还低,学校物理课上的东西应该都还记得。 exit()函数将立即终止程序,而不会回到调用者函数。 关于这个我们可以说的是:
生成的代码几乎一样,但是我发现每个exit()调用之后都有INT 3。 INT 3是一个调试器断点。 可以知道的是exit()是永远不会return的函数之一。所以如果他“返回”了,那么估计发生了什么奇怪的事情,也是时候启动调试器了。 但是MSVC从2012年开始又改成了使用SIMD指令: 当然,SIMD在x86下也是可用的,包括这些浮点数的运算。使用他们计算起来也确实方便点,所以微软编译器使用了他们。 我们也可以注意到 -273 这个值会很早的被载入XMM0。这个没问题,因为编译器并不一定会按照源代码里面的顺序产生代码。 |
sse提供了xmm寄存器,xmm一组8个128位的寄存器,分别名为xmm0-xmm7,sse构架提供对打包单精度浮点数的SIMD支持。
sse提供了两个版本的指令,其一以后缀ps结尾,这组指令对打包单精度浮点值执行类似mmx操作运算,而第二种后缀ss,这些指令对一个量标单精度浮点 值进行运算操作,这些指令不对打包值中的所有浮点值操作,而只对打包值中的低位双字节执行操作,源操作数中剩余的3个值直接传送给结果。
把4个对准的单精度值传送到xmm寄存器或者内存 |
把4个不对准的单精度值传送到xmm寄存器或者内存 |
把1个单精度值传送到内存或者寄存器的低位双字 |
把2个单精度值传送到内存或者寄存器的低四字 |
把2个单精度值传送到内存或者寄存器的高四字 |
把2个单精度值从低四字传送到高四字 |
把2个单精度值从高四字传送到低四字 |
其中对准操作movaps要求数据在内存中对准16字节的边界,以提交效率,否则应使用movups传送数据。
计算打包值的平方根倒数 |
计算两个打包值中的最大值 |
计算两个打包值中的最小值 |
计算两个打包值的按位逻辑与 |
计算两个打包值的按位逻辑非 |
计算两个打包值的按位逻辑或 |
计算两个打包值的按位逻辑异或 |
以上指令都是用两个操作数:源操作数可以是128位内存或者xmm寄存器,目标操作数必须是xmm寄存器。
可以看到,调用加法指令之后,四组和都存储在xmm1寄存器中,gdb查看时由于不知道如何解析xmm1寄存器的内容,因为可能是单精度,也可能是双精度或者不同宽度的整数,所以只能按不同的解析方式全部显示,查看v4_float即四个单精度浮点数的显示。
下面介绍一下sse构架下的比较指令,sse的比较指令单独比较128位打包单精度浮点的每个元素,结果是一个掩码,满足比较条件的结果全为1值,不满足结果的全为0值(量标只对最低的双字执行)。
比较标量值并且设置eflags寄存器 |
比较标量值(包括非法值)并设置eflags寄存器 |
看到这里,仅仅有一个比较指令,并没有说明大小,何为满足条件全1,不满足全0呢,这样说一下指令的使用:
其中多出来的imp是一个无符号整数,这个整数表示的含义就是条件,这个条件值如下表所示:
0 |
如果需要比较两个数是否相等,传imp为0即可作为条件,满足条件结果全1,这是sse的比较方式。这里说明一下条件中的无序,因为是浮点比较,寄存器或内存中的有些值并不符合规定的浮点存储格式,相互比较是没有意义的,称为无序。
除了对浮点数的支持,sse指令集也有指令对mmx提供的功能进行扩展,他们对mmx寄存器中的数据执行操作:
计算打包无符号字节整数的平均值 |
计算打包无符号字整数的平均值 |
把一个字从mmx寄存器复制到通用寄存器 |
把一个字从通用寄存器复制到mmx寄存器 |
计算打包无符号字节整数的最大值 |
计算打包有符号字整数的最大值 |
计算打包无符号字节整数的最小值 |
计算打包有符号字整数的最小值 |
将打包无符号字整数相乘并且存储高位结果 |
计算无符号字节整数的绝对差的总和 |
SSE2 指令集又对 SSE 指令集做了很多扩充,主要对操作双精度浮点数和128位打包整数值执行数学操作,下面介绍SSE2的使用,先来看数据传送指令:
把2个对准的双精度值传送到xmm寄存器或者内存 |
把2个不对准的双精度值传送到xmm寄存器或者内存 |
把2个对准的四字节整数传送到xmm寄存器或者内存 |
把2个不对准的四字节整数传送到xmm寄存器或者内存 |
把1个双精度值传送到内存或者寄存器的低四字 |
把1个双精度值传送到内存或者寄存器的高四字 |
把1个双精度值传送到内存或者寄存器的低四字 |
SSE2指令集提供处理打包双精度浮点数,打包字整数,打包双字整数和打包四字整数值的数学指令,这里列举SSE2的加法指令来说明这一系列指令格式:
将打包双精度浮点值相加 |
将量标双精度浮点值相加 |
将打包带符号字节整数相加 |
将打包带符号字整数相加 |
将打包带符号双字整数相加 |
将打包带符号四字整数相加 |
这里虽然只列举add系列指令,这些选项也存在于乘法和除法操作中(mulpd, mulsd, divpd, divsd等)。
另外同sse指令集,sse2指令集也提供专门的数学操作,sqrt, max, min。
最后我们来看SSE3指令集,SSE3构架并没有提供任何新的数据类型,仅仅添加了几条指令,用于更快的执行标准函数,下面是新指令的列表:
把第一个fpu寄存器的值转换为整数(舍入)并且从fpu堆栈弹出 |
快速从内存加载128位不对准的数据值 |
传送128位值,复制第2个和第4个32位数据元素 |
传送128位值,复制第1个和第3个32位数据元素 |
传送64位值,赋值值,使之成为128位值 |
对于打包单精度浮点数,对第2个和第4个32位执行加法,第1和第3个32位执行减法 |
对于打包单精度浮点数,对第2对64位值执行加法,第1对位执行减法 |
对操作数的相邻的元素执行单精度浮点加法操作 |
对操作数的相邻的元素执行双精度浮点加法操作 |
对操作数的相邻的元素执行单精度浮点减法操作 |
对操作数的相邻的元素执行双精度浮点减法操作 |
SSE指令繁多,这里举得例子却很少,以后我会在此文继续附加一些说明例子,方便理解