Windows / Intel和iOS / Arm浮点计算的差异

时间:2012-08-06 16:41:05

标签: floating-point arm intel

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])。我专注于值的十六进制版本,因为这些是两个平台上输入的确切值,通过检查两个平台上v1v2的内存位置进行验证。作为参考,通过将十六进制值放入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之间这种简单计算的结果如此不同?

1 个答案:

答案 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中)。