Windows/Intel和iOS/Arm在浮点运算中的区别

3
在更新 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.37158203125])。我专注于十六进制版本的值,因为这些是两个平台上都是输入的确切值,通过检查两个平台上 v1v2 的内存位置进行验证。参考资料:方括号中的浮点数值来自将十六进制值放入此网站中获得。

在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个回答

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在VS2010中似乎没有使用SSE2。

好的,这很有前途。我能够在两个平台上复现这个特定的计算结果相同。然而,总体来说,模拟仍然不完全相同。要么是有东西改变了控制字(但我们使用的任何可搜索代码中都没有),要么是有更大的问题。但至少对于这两个平台之间的这种特定差异,谜团已经解开了。 - Jim Buck

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接