1

在对Autodesk TinkerBox的更新中,我在 Windows 上运行的仅限内部开发版本与在我们的最终目标 iOS 上运行的版本之间遇到了意外的浮点计算差异(以下信息基于调试构建在 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.371582032])。我专注于值的十六进制版本,因为它们是两个平台上输入的确切值,通过检查两个平台上的内存位置来v1验证v2。作为参考,括号中的浮点值是从将十六进制值放入此站点中获取的。

在 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 之间会如此不同呢?

4

1 回答 1

2

您将看到使用不同浮点精度进行计算的结果。

在 x86 代码中,计算是在扩展精度(80 位)的 FPU 寄存器中完成的,而 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 内在函数。不幸的是,/arch:SSE2似乎没有将 SSE2 用于浮点(至少在 VS2010 中)。

于 2012-08-06T17:27:31.177 回答