由于当您将项目从 x86 切换到 x64 时,GPU 代码没有任何变化,这一切都与 CPU 上如何执行乘法有关。在 x86 和 x64 模式下处理浮点数之间存在一些细微差别,最大的区别在于,由于任何 x64 CPU 也支持 SSE 和 SSE2,因此它默认用于 Windows 上 64 位模式下的数学运算。
HD4770 GPU 使用单精度浮点单元进行所有计算。另一方面,现代 x64 CPU 有两种处理浮点数的功能单元:
- x87 FPU 以更高的 80 位扩展精度运行
- SSE FPU 以 32 位和 64 位精度运行,与其他 CPU 处理浮点数的方式非常兼容
在 32 位模式下,编译器不假定 SSE 可用,而是生成通常的 x87 FPU 代码来进行数学运算。在这种情况下,像data[i] * data[i] 这样的操作是在内部使用更高的 80 位精度执行的。类if (results[i] == data[i] * data[i])的比较如下:
-
使用
FLD DWORD PTR data[i] 将 data[i] 推送到 x87 FPU 堆栈
-
data[i] * data[i] 使用 FMUL DWORD PTR data[i] 计算
-
使用
FLD DWORD PTR result[i] 将result[i] 推送到x87 FPU 堆栈上
- 使用
FUCOMPP比较两个值
问题来了。 data[i] * data[i] 位于 80 位精度的 x87 FPU 堆栈元素中。 result[i] 来自 32 位精度的 GPU。这两个数字很可能会有所不同,因为data[i] * data[i] 有更多的有效数字,而result[i] 有很多零(80 位精度)!
在 64 位模式下,事情以另一种方式发生。编译器知道您的 CPU 支持 SSE,它使用 SSE 指令进行数学运算。同样的比较语句在x64上的执行方式如下:
-
使用
MOVSS XMM0, DWORD PTR data[i] 将data[i] 加载到SSE 寄存器中
-
data[i] * data[i] 使用 MULSS XMM0, DWORD PTR data[i] 计算
-
result[i] 使用 MOVSS XMM1, DWORD PTR result[i] 加载到另一个 SSE 寄存器中
- 使用
UCOMISS XMM1, XMM0比较两个值
在这种情况下,平方运算以与 GPU 上相同的 32 位单点精度执行。不会生成具有 80 位精度的中间结果。这就是结果相同的原因。
即使没有 GPU 参与,实际测试也很容易。只需运行以下简单程序:
#include <stdlib.h>
#include <stdio.h>
float mysqr(float f)
{
f *= f;
return f;
}
int main (void)
{
int i, n;
float f, f2;
srand(1);
for (i = n = 0; n < 1000000; n++)
{
f = rand()/(float)RAND_MAX;
if (mysqr(f) != f*f) i++;
}
printf("%d of %d squares differ\n", i);
return 0;
}
mysqr 是专门编写的,以便将中间 80 位结果转换为 32 位精度 float。如果在 64 位模式下编译运行,输出为:
0 of 1000000 squares differ
如果在32位模式下编译运行,输出为:
999845 of 1000000 squares differ
原则上您应该能够在 32 位模式下更改浮点模型(项目属性 -> 配置属性 -> C/C++ -> 代码生成 -> 浮点模型)但是这样做不会改变任何事情,因为至少在 VS2010 上,中间结果仍保留在 FPU 中。您可以做的是强制存储和重新加载计算平方,以便在与 GPU 的结果进行比较之前将其舍入到 32 位精度。在上面的简单示例中,这是通过更改来实现的:
if (mysqr(f) != f*f) i++;
到
if (mysqr(f) != (float)(f*f)) i++;
改变后32位代码输出变为:
0 of 1000000 squares differ