由于当您将项目从 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])
如下进行:
data[i]
使用FLD DWORD PTR data[i]
data[i] * data[i]
计算使用FMUL DWORD PTR data[i]
result[i]
使用推入 x87 FPU 堆栈FLD DWORD PTR result[i]
- 两个值都使用比较
FUCOMPP
问题来了。data[i] * data[i]
以 80 位精度驻留在 x87 FPU 堆栈元素中。result[i]
来自 GPU 的 32 位精度。这两个数字很可能会有所不同,因为data[i] * data[i]
有更多的有效数字而result[i]
有很多零(以 80 位精度)!
在 64 位模式下,事情以另一种方式发生。编译器知道您的 CPU 具有 SSE 功能,并且它使用 SSE 指令进行数学运算。同样的比较语句在 x64 上以如下方式执行:
data[i]
使用加载到 SSE 寄存器中MOVSS XMM0, DWORD PTR data[i]
data[i] * data[i]
计算使用MULSS XMM0, DWORD PTR data[i]
result[i]
使用加载到另一个 SSE 寄存器MOVSS XMM1, DWORD PTR result[i]
- 两个值都使用比较
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 位模式下更改浮点模型(Project properties -> Configuration Properties -> C/C++ -> Code Generation -> Floating Point Model)但是这样做不会改变任何事情,因为至少在 VS2010 中间结果仍保存在 FPU 中。您可以做的是强制存储和重新加载计算平方,以便在将其与 GPU 的结果进行比较之前将其舍入到 32 位精度。在上面的简单示例中,这是通过更改:
if (mysqr(f) != f*f) i++;
至
if (mysqr(f) != (float)(f*f)) i++;
更改后32位代码输出变为:
0 of 1000000 squares differ