在Autodesk TinkerBox的更新中,我遇到了在Windows上运行的内部开发版本与在iOS最终目标上运行的版本之间的意外浮点计算差异(以下info基于在iPad1上运行的调试版本。)
我们使用Chipmunk来满足我们的物理需求。这绝不是唯一能解决这个问题的计算方法,但这是我正在分析的一个特别的计算方法:
static inline cpFloat
cpvcross(const cpVect &v1, const cpVect &v2)
{
return v1.x*v2.y - v1.y*v2.x;
}
我正在查看的特定情况为v1
为(0xC0A7BC40 [-5.241729736328125],0xC0E84C80 [-7.25933837890625])和v2
为(0x428848FB [68.14253997802734],0x42BCBE40 [94.37158203125])。我专注于值的十六进制版本,因为这些是两个平台上输入的确切值,通过检查两个平台上v1
和v2
的内存位置进行验证。作为参考,通过将十六进制值放入this site来抓取括号中的浮点值。
在Windows上,结果为0xBA15F8E8 [-0.0005720988847315311],在iOS上,结果为0xBA100000 [-0.00054931640625]。当然,差异很小,但在考虑百分比时并不是真的,并且随着时间的推移它会累积以显示物理行为的偏差。 (请不要建议使用双打。当然,它会减慢游戏速度,而不是使用双打也不是问题。:))
作为参考,这是两个平台上的调试版本,代码编译为:
Windows
static inline cpFloat
cpvcross(const cpVect &v1, const cpVect &v2)
{
01324790 push ebp
01324791 mov ebp,esp
01324793 sub esp,0C4h
01324799 push ebx
0132479A push esi
0132479B push edi
0132479C lea edi,[ebp-0C4h]
013247A2 mov ecx,31h
013247A7 mov eax,0CCCCCCCCh
013247AC rep stos dword ptr es:[edi]
return v1.x*v2.y - v1.y*v2.x;
013247AE mov eax,dword ptr [v1]
013247B1 fld dword ptr [eax]
013247B3 mov ecx,dword ptr [v2]
013247B6 fmul dword ptr [ecx+4]
013247B9 mov edx,dword ptr [v1]
013247BC fld dword ptr [edx+4]
013247BF mov eax,dword ptr [v2]
013247C2 fmul dword ptr [eax]
013247C4 fsubp st(1),st
013247C6 fstp dword ptr [ebp-0C4h]
013247CC fld dword ptr [ebp-0C4h]
}
013247D2 pop edi
013247D3 pop esi
013247D4 pop ebx
013247D5 mov esp,ebp
013247D7 pop ebp
013247D8 ret
iOS
invent`cpvcross at cpVect.h:63:
0x94a8: sub sp, sp, #8
0x94ac: str r0, [sp, #4]
0x94b0: str r1, [sp]
0x94b4: ldr r0, [sp, #4]
0x94b8: vldr s0, [r1]
0x94bc: vldr s1, [r1, #4]
0x94c0: vldr s2, [r0]
0x94c4: vldr s3, [r0, #4]
0x94c8: vmul.f32 s1, s2, s1
0x94cc: vmul.f32 s0, s3, s0
0x94d0: vsub.f32 s0, s1, s0
0x94d4: vmov r0, s0
0x94d8: add sp, sp, #8
0x94dc: bx lr
尽管我可以说,这些计算是相同的,假设每条指令都是相同地计算操作数的结果。由于某种原因(Visual Studio允许),Xcode不允许我逐步指令,因此我无法缩小哪些指令与英特尔FP单元相比有偏差。
那么,为什么两个CPU之间这种简单计算的结果如此不同?
答案 0 :(得分:2)
您将看到使用不同浮点精度进行计算的结果。
在x86代码中,计算在FPU寄存器中完成,扩展精度(80位),而NEON代码使用浮点数(32位)。显然,乘法和减法期间的额外精度允许x86代码在ARM代码丢失时保留更多位。
使用_controlfp函数可以告诉FPU对所有计算使用特定的精度。我使用MSDN中的示例创建了一个小程序,并且能够获得与ARM代码相同的结果:
#include <stdio.h>
typedef float cpFloat;
struct cpVect {cpFloat x, y;};
struct cpVectI {unsigned int x, y;};
union cpv {cpVectI i; cpVect f;};
union cfi { float f; unsigned int i;};
cpFloat cpvcross(const cpVect &v1, const cpVect &v2)
{
return v1.x*v2.y - v1.y*v2.x;
}
#include <float.h>
#pragma fenv_access (on)
void main(void)
{
cpv v1, v2;
cfi fi;
v1.i.x = 0xC0A7BC40;
v1.i.y = 0xC0E84C80;
v2.i.x = 0x428848FB;
v2.i.y = 0x42BCBE40;
unsigned int control_word_x87;
// Show original x87 control word and do calculation.
__control87_2(0, 0, &control_word_x87, 0);
printf( "Original: 0x%.4x\n", control_word_x87 );
fi.f = cpvcross(v1.f, v2.f);
printf("Result: %g (0x%08X)\n", fi.f, fi.i);
// Set precision to 24 bits and recalculate.
__control87_2(_PC_24, MCW_PC, &control_word_x87, 0);
printf( "24-bit: 0x%.4x\n", control_word_x87);
fi.f = cpvcross(v1.f, v2.f);
printf("Result: %g (0x%08X)\n", fi.f, fi.i);
// Restore default precision-control bits and recalculate.
__control87_2( _CW_DEFAULT, MCW_PC, &control_word_x87, 0);
printf( "Default: 0x%.4x\n", control_word_x87 );
fi.f = cpvcross(v1.f, v2.f);
printf("Result: %g (0x%08X)\n", fi.f, fi.i);
}
这是输出:
Original: 0x9001f
Result: -0.000572099 (0xBA15F8E8)
24-bit: 0xa001f
Result: -0.000549316 (0xBA100000)
Default: 0x9001f
Result: -0.000572099 (0xBA15F8E8)
使用此功能并调用外部库时要小心;某些代码可能依赖于默认设置,如果您在背后更改它们将会中断。
另一种选择可能是切换到使用特定精度的SSE intrinsics。不幸的是,/arch:SSE2
似乎没有将SSE2用于浮点(至少在VS2010中)。