不止一次地感觉到,在程序中,不限于任何具体的语言,想用好浮点数是不是一件容易的事。浮点数有一些违反直觉的特征,这里说的浮点数,还只限于IEEE 754标准中的。也就是默认情况下在几乎任何你所使用的程序语言和体系结构中使用的。
有兴趣的可以先猜测一下下面的C代码的输出结果
int main(int argc, char ** argv)
{
float f1= 0;
float f2= -0;
float f3= -f1;
printf("+0: %#08x, -0: %#08x, %#08x\n",
*( (int*)&f1),
*( (int*)&f2),
*( (int*)&f3));
printf("%s", (f1 == f3)?"+0 == -0": "+0 != -0");
return 0;
}
|
* IEEE 754的单精度浮点数占用4个字节,虽与通常情况下的int一样,但表示的数的范围却大得多,但是,深入一想,既然都是32位,它为何能表示“更多”的数呢,答案是它不能,表示的数的范围大,不意味着它能表示的数“更多”,它所表示的实数是离散的,也就是说,它所能够表现的数是个有限的固定的集合,任何它所能表示的两个连续的数之间,都有无穷多个它所不能表示的数。这是本质特征,只要使用一个物理实体来表示无限的概念,都会有这个局限,所以float能表示更大的数,代价是:有些小一些的数,它反而不能精确表示。
Console.WriteLine( uint.MaxValue.ToString() );
Console.WriteLine( float.MaxValue.ToString("F99") );
结果是
4294967295
340282300000000000000000000000000000000
* 再进一步,浮点数表示的数的分布特征是靠近0的小数很密集,靠近是指在X坐标轴上近0点的绝对距离近。远离0的稀疏。
* 众所周知,浮点数比较大小,不能简单地使用==,这是否意味着,由于舍入误差,浮点数在经过一些运算之后,其值变为了数学上的那个精确值的某个邻近值,因而其内部的位存储模式不同了。我认为是的,也就是说,两个浮点数如果用==相比较,结果为false, 它的位存储模式一定不一样?对于非NaN而言,是对的。反过来的命题却不成立:两个浮点数如果其位存储模式不一样,用==比较结果一定为false. 看下面
* 在浮点数中,+0和-0中存储结构上是有区分的,但用==比较的结构却是相同的。
* 对于NaN, 两个NaN,即使其位存储模式一样,以==比较的结果也一定是false. NaN不是一个固定的位模式,虽然+0和-0是,有一个位模式的集合,都是NaA, 但是,即使两个NaN数的位存储模式完全一样,它们相比的结果仍然是不相等。
* 对于两个正无穷呢?(C#)
float f1 = float.PositiveInfinity;
float f2 = float.PositiveInfinity;
Console.WriteLine( (f1 == f2)?"==": "!=" );
|
在数学中,同为无穷,却未必相同。
* 浮点数运算单元判断两个数是否相等不是简单地比较其位存储模式是否一样,而是会检查其数学上的语意,比如+0和-0位存储模式中符号号相反,但其比较结果是一样的。与此相对,两个NaN,位存储模式完全一样,比较结果却是不相等。
* float.ToString("F99")
这个格式字符串的最大可能性是99, 此时输出小数点后99个小数,将99改为100, 结果就莫名其妙了,没有看到.NET的任何文档中描述这一点。
* C#中显示 Single类型的位存储模式:
float f3 = ((float)uint.MaxValue + 2);
IntPtr p1 = Marshal.AllocHGlobal( Marshal.SizeOf( typeof(float) ) );
Marshal.StructureToPtr( f3, p1, false);
uint i = (uint)Marshal.PtrToStructure(p1, typeof(uint) );
Marshal.FreeHGlobal(p1);
Console.WriteLine( i.ToString("X") );
或:
Console.WriteLine( BitConverter.ToUInt32( BitConverter.GetBytes(f3), 0).ToString("X") );
* printf 中 %f 转换指示中名为f, 却“不是”float, 而是double. 也因为这个原因,更好的办法是把float看成是对浮点数的统称, 而不是特指单精度的浮点数。这一点可能最容易造成误解。另外,这一点可以与printf 必需是__cdecl调用协议结合起来理解,printf的原型中通过...声明接受可变个数的参数,这种形式的函数,传递float值仍会进行函数参数类型提升,即把它转换为double, 这在ANSI及C++标准中与固定参数的原型是不同的。
* printf 中浮点数默认输出精度是6, 不知道什么地方看到说是7,自己再确认一下,是6
* sscanf 中却用%f 表示下一个输入是单精度浮点数,用%lf 表示下一个输入是双精度浮点数。用%Lf 表示long double, 注意在printf中%lf 表示的是long double.
* C#中的 string.Format 中指定的"r" 转换指示符,表示round trip, 它只保证把一个浮点数保存成这样的形式,当它用float.Parse 反转回一个float数据时,其内部表示形式仍与原来的float一样。它并不隐含着这样的一个假设:使用“r"的外部形式是对该浮点数的精确表示,比如Single类型的值0.1, 虽然0.1无论如何不能被Single所精确表示,但0.1这种输出形式却是round trip的输出,你不能依赖于round trip生成的浮点数表示形式进行精确的计算。
* 对于C#中的decimal, 它是基于10进制的精确的小数表示法。注意decimal在C#语言的层面上是个primitive类型,CLR中却不是 typeof(decimal).IsPrimitive 返回是false. 在C#中可以这样写:
decimal d = 3.1415926M;
但是,在Form Designer中生成的代码却不是这样, 它使用的是decimal的另一个构造函数形式:
this.m_numeric_up_down.Value = new decimal(new int[]{
20,
0,
0,
0});
这种形式表示的是精确的decimal类型的值: 20
根据MSDN,这4个整数中,前3个整数都是表示结果值的96位的数值部分,最后一个整数中同时编码了符号位和28位的指数位。暗含的底是10.
有人度量过decimal的运算大概比float/double慢40倍。
* printf("A%.0dZ", 0) 输出的结果是 AZ
也就是输出串为空,空不是一个空格,而是啥也没有,虽然看起来不太合理,却是ANSI C的规定。VC 2008编译器也的确是这么实现的。
* printf("%.-3f", 3.14 ); 中精度宽度若指定为负数
* printf("%+d\n", 0); 会输出+0, 尽管在C语言中没有正0这样的概念,因为没有负0. 标准C中对数字前是否有加号是这样说的:如果之后指定了+号(即使同时指定了空格,也是优先使+生效而忽略空格),则对于"非负数"输出一个前导的+号. 注意它没有说正数.
* 尽管对于数字之外的内容, 前导的0看起来并无实际意义, 却仍会对 printf("%030s\n", "_") 这样的内容填充足够的0以补齐总共30个字符的宽度.
* 尽管曾经知道, 却仍会怀疑, %i 与 %d 是否真正是完全一样. 是的, 完全一样, 可以这样来理解这种现状, 早期的C中printf函数没有%i, 但标准C为了与scanf中的指示符保持一致, 加入了%i, 这样printf就有了两个完全相同的转换符. 可以这样来助记这两个单词: i=> integer, d=>decimal
* 经常怀疑, 宽度前面加负号是让输出左对齐, 还是右对齐. 助记: 对于一个负的数字, 负号本身总是出现在数字的左边, "所以"加了负号是让输出内容左对齐. 因此, 没有指定负号时结果相反, 就是右对齐.
* 单个转换符所造成的输出的长度是有限度的, 最大509个字节, 尽管我还从来没有突破过这一限度, 但的确不知道这个限制. 但VC2008却在测试输出宽度为600时能够成功输出.
* 对于整数类型, 精度域表示的是最少输出的数字, 不要担心数字实际值在指定的最少输出字符数中放不下的情形, 如 printf("%.1d\n", 12); 输出仍然是两位数字, 是最少字符数, 不是限定最多字符数.
* 对于字符串类型, 却是限定输出的最多字符数: printf("{%.3s}\n", "1234567" ); 输出是{123}
这个限定是对后面的实际参数 "1234567"所能产生的输出而言, 不是针对该转换符产生的总的输出字符数, 如: printf("{%05s.3}\n", "1234567"); 会输出 {00123}, 从"1234567"中最多取出3个字符, 但最终却要终成5个字符的宽度, 不足的以0补齐.
参考:
1. 深入理解计算机系统
2. What Every Computer Scientist Should Know About Floating Point Arithmetic