是什么区别decimal,float并double在.NET?
什么时候会有人使用其中一种?
不,这不是另一个"为什么是(1/3.0)*3!= 1"的问题.
我最近一直在读关于漂浮点的事情; 具体而言,相同的计算如何在不同的体系结构或优化设置上给出不同的结果.
对于存储重放的视频游戏来说,这是一个问题,或者是对等网络(而不是服务器 - 客户端),它依赖于每次运行程序时产生完全相同结果的所有客户端 - 一个小的差异浮点计算可能导致不同机器(甚至是同一台机器上)的游戏状态截然不同!
甚至在"跟随" IEEE-754的处理器中也会发生这种情况,这主要是因为某些处理器(即x86)使用双倍扩展精度.也就是说,它们使用80位寄存器进行所有计算,然后截断为64位或32位,导致与使用64位或32位进行计算的机器不同的舍入结果.
我在网上看到过这个问题的几种解决方案,但都是针对C++,而不是C#:
double使用_controlfp_s(Windows),_FPU_SETCW(Linux?)或fpsetprec(BSD)禁用双扩展精度模式(以便所有计算使用IEEE-754 64位).float和double完全.decimal可以用于此目的,但会慢得多,并且没有任何System.Math库函数支持它.那么,这在C#中是否也是一个问题? 如果我只打算支持Windows(而不是Mono)怎么办?
如果是,有没有办法强制我的程序以正常的双精度运行?
如果没有,是否有任何库可以帮助保持浮点计算的一致性?
我有以下简单的代码:
int speed1 = (int)(6.2f * 10);
float tmp = 6.2f * 10;
int speed2 = (int)tmp;
Run Code Online (Sandbox Code Playgroud)
speed1和speed2应该具有相同的值,但事实上,我有:
speed1 = 61
speed2 = 62
Run Code Online (Sandbox Code Playgroud)
我知道我应该使用Math.Round而不是cast,但我想了解为什么值不同.
我查看了生成的字节码,但除了存储和加载外,操作码是相同的.
我也在java中尝试了相同的代码,我正确地获得了62和62.
有人可以解释一下吗?
编辑: 在实际代码中,它不是直接6.2f*10而是函数调用*常量.我有以下字节码:
速度1:
IL_01b3: ldloc.s V_8
IL_01b5: callvirt instance float32 myPackage.MyClass::getSpeed()
IL_01ba: ldc.r4 10.
IL_01bf: mul
IL_01c0: conv.i4
IL_01c1: stloc.s V_9
Run Code Online (Sandbox Code Playgroud)
速度2:
IL_01c3: ldloc.s V_8
IL_01c5: callvirt instance float32 myPackage.MyClass::getSpeed()
IL_01ca: ldc.r4 10.
IL_01cf: mul
IL_01d0: stloc.s V_10
IL_01d2: ldloc.s V_10
IL_01d4: conv.i4
IL_01d5: stloc.s V_11
Run Code Online (Sandbox Code Playgroud)
我们可以看到操作数是浮点数,唯一的区别是stloc/ldloc
至于虚拟机,我尝试使用Mono/Win7,Mono/MacOS和.NET/Windows,结果相同
我一直在阅读很多关于.NET中浮点确定性的内容,即确保具有相同输入的相同代码将在不同的机器上提供相同的结果.由于.NET缺少Java的fpstrict和MSVC的fp:strict等选项,因此似乎一致认为使用纯托管代码无法绕过这个问题.C#游戏AI Wars已经决定使用定点数学,但这是一个麻烦的解决方案.
主要问题似乎是CLR允许中间结果存在于FPU寄存器中,这些寄存器具有比类型的原始精度更高的精度,从而导致不可预测的更高精度结果.CLR工程师David Notario撰写的MSDN文章解释了以下内容:
请注意,对于当前规范,它仍然是提供"可预测性"的语言选择.在每次FP操作之后,该语言可以插入conv.r4或conv.r8指令以获得"可预测的"行为. 显然,这非常昂贵,不同的语言有不同的妥协.例如,C#什么都不做,如果你想缩小,你必须手动插入(浮点)和(双)强制转换.
这表明,只需为每个表达式和计算浮点数的子表达式插入显式强制转换,就可以实现浮点确定性.有人可能会在float周围编写一个包装器类型来自动执行此任务.这将是一个简单而理想的解决方案!
然而,其他评论表明它并非如此简单.Eric Lippert最近表示(强调我的):
在某些版本的运行时中,显式转换为float会产生与不这样做不同的结果.当你明确地转换为float时,C#编译器会给运行时提供一个提示,说"如果碰巧使用这个优化,就把这个东西从超高精度模式中取出".
这对运行时的"提示"是什么?C#规范是否规定显式转换为float会导致在IL中插入conv.r4?CLR规范是否规定conv.r4指令会使值缩小到其原始大小?只有当这两者都成立时,我们才能依靠显式转换来提供浮点"可预测性",正如David Notario所解释的那样.
最后,即使我们确实能够将所有中间结果强制转换为类型的原生大小,这是否足以保证跨机器的可重复性,还是有其他因素如FPU/SSE运行时设置?
我知道浮点数具有精度,精度后的数字不可靠.
但是如果用于计算数字的等式是相同的呢?我可以假设结果也一样吗?
例如,我们有两个浮点数x和y.我们可以假设x/y机器1的结果与机器2的结果完全相同吗?IE ==比较将返回true
这不是一个现实世界的例子!请不要建议使用decimal或其他东西.
我只是问这个,因为我真的想知道为什么会这样.
我最近再次与Jon Skeet一起看到了令人敬畏的Tekpub网络广播C#4.0.
在第7集- 小数和浮点数它真的很奇怪,甚至我们的 Chuck Norris of Programming(又名Jon Skeet)对我的问题没有真正的答案.只有一个可能.
MyTestMethod()失败并MyTestMethod2()通过?[Test]
public void MyTestMethod()
{
double d = 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
d += 0.1d;
Console.WriteLine("d = " + d);
Assert.AreEqual(d, 1.0d);
}
Run Code Online (Sandbox Code Playgroud)
这导致了
d = 1
预期:0.99999999999999989d但是:1.0d …
输出以下代码:
var a = 0.1;
var count = 1;
while (a > 0)
{
if (count == 323)
{
var isZeroA = (a * 0.1) == 0;
var b = a * 0.1;
var isZeroB = b == 0;
Console.WriteLine("IsZeroA: {0}, IsZeroB: {1}", isZeroA, isZeroB);
}
a *= 0.1;
++count;
}
Run Code Online (Sandbox Code Playgroud)
是
奇怪的是,当我if (count == 323)在调试并(a * 0.1) == 0在Visual Studio Watch窗口中放置表达式之后放置断点时,它会报告表达式true.
有谁知道为什么表达式a * 0.1不为零,但是当分配给变量时b,则b为零?
这里的代码是直截了当的,但我不明白结果:
float percent = 0.69f;
int firstInt = (int)(percent*100f);
float tempFloat = percent*100f;
int secondInt = (int)tempFloat;
Debug.Log(firstInt + " " + secondInt);
Run Code Online (Sandbox Code Playgroud)
为什么是firstInt68但是secondInt69?
当您阅读MSDN时System.Single:
Single符合IEC 60559:1989(IEEE 754)二进制浮点运算标准.
和C#语言规范:
这些
float和double类型使用32位单精度和64位双精度IEEE 754格式表示[...]
然后:
根据IEEE 754算法的规则计算产品.
您很容易得到float类型及其乘法符合IEEE 754 的印象.
IEEE 754的一部分是多重定义.我的意思是,当你有两个float实例时,存在一个且只有一个float是他们的"正确"产品.不允许产品依赖于计算系统的某些"状态"或"设置".
现在,考虑以下简单程序:
using System;
static class Program
{
static void Main()
{
Console.WriteLine("Environment");
Console.WriteLine(Environment.Is64BitOperatingSystem);
Console.WriteLine(Environment.Is64BitProcess);
bool isDebug = false;
#if DEBUG
isDebug = true;
#endif
Console.WriteLine(isDebug);
Console.WriteLine();
float a, b, product, whole;
Console.WriteLine("case .58");
a = 0.58f;
b = 100f;
product = a * b;
whole = 58f;
Console.WriteLine(whole == …Run Code Online (Sandbox Code Playgroud) 在数学上,0.9重复可以显示为等于1.然而,这个问题不是关于无穷大,收敛或这背后的数学.
上述假设可以用C#中的双精度表示,具体如下.
var oneOverNine = 1d / 9d;
var resultTimesNine = oneOverNine * 9d;
Run Code Online (Sandbox Code Playgroud)
使用上面的代码,(resultTimesNine == 1d)计算结果为true.
当使用小数代替时,评估结果为false,但是,我的问题不是关于double和decimal的不同精度.
由于没有类型具有无限精度,如何以及为什么双重维持这样的相等而小数不是?关于oneOverNine变量存储在内存中的方式,上面代码的"行之间"发生了什么?
我有以下代码:
float a = 0.02f * 28f;
double b = (double)a;
double c = (double)(0.02f * 28f);
Console.WriteLine(String.Format(" {0:F20}", b));
Console.WriteLine(String.Format(" {0:F20}", c));
Run Code Online (Sandbox Code Playgroud)
然而,无论是从VS2012还是VS2015(均具有"标准"设置)编译,它都会返回不同的结果
在VS2012
0,56000000238418600000
0,55999998748302500000
Run Code Online (Sandbox Code Playgroud)
在VS2015中:
0,56000000238418600000
0,56000000238418600000
Run Code Online (Sandbox Code Playgroud)
VS2012解散:
float a = 0.02f * 28f;
0000003a mov dword ptr [ebp-40h],3F0F5C29h
double b = (double)a;
00000041 fld dword ptr [ebp-40h]
00000044 fstp qword ptr [ebp-48h]
double c = (double)(0.02f * 28f);
00000047 fld qword ptr ds:[001D34D0h]
0000004d fstp qword ptr [ebp-50h]
Run Code Online (Sandbox Code Playgroud)
VS2015解散:
float a = 0.02f * 28f;
001E2DE2 …Run Code Online (Sandbox Code Playgroud) 我们有一些代码会在某些机器上产生意外结果.我把它缩小到一个简单的例子.在下面的linqpad片段中,方法GetVal和GetVal2实现基本相同,尽管前者还包括对NaN的检查.但是,每个返回的结果都不同(至少在我的机器上).
void Main()
{
var x = Double.MinValue;
var y = Double.MaxValue;
var diff = y/10 - x/10;
Console.WriteLine(GetVal(x,6,diff));
Console.WriteLine(GetVal2(x,6,diff));
}
public static double GetVal(double start, int numSteps, double step)
{
var res = start + numSteps * step;
if (res == Double.NaN)
throw new InvalidOperationException();
return res;
}
public static double GetVal2(double start, int numSteps, double step)
{
return start + numSteps * step;
}
Run Code Online (Sandbox Code Playgroud)
结果
3.59538626972463E+307
Infinity
Run Code Online (Sandbox Code Playgroud)
为什么会发生这种情况,是否有一种避免它的简单方法?与寄存器有关?