码农唐磊 程序猿石头 程序员
本文为 6 年前的旧文整理重发,由于最开始是 workdpress 的程序,后改成静态 blog 过程当中,致使格式等混乱,这篇年久失修旧文可文末点击原文访问。windows
背景就简单点儿说,当初一个项目 C# 编写,涉及浮点运算,前因后果省去,直接看以下代码。(为何有这个问题产生,是由于当初线上产生了很诡异的问题,和本地调试效果不一致。)服务器
float p3x = 80838.0f; float p2y = -2499.0f; double v321 = p3x * p2y; Console.WriteLine(v321);
很简单吧,立刻笔算下结果为 -202014162,没问题,难道C#没有产生这样的结果?不可能吧,开启 VisualStudio,copy代码试试,果真结果是-202014162。就这样完了么?显然没有!把编译时的选项从AnyCPU改为x64试试~(服务器环境正是64位滴哦!!)结果竟然变成了-202014160,对没错,就是-202014160。细想一下,由于浮点运算的偏差,-202014160 这个结果是合理的。嗯,再试试C++。// 测试环境Intel(R) i7-3770 CPU, windows OS 64. Visual Studio 2012 默认设置。架构
float p3x = 80838.0f; float p2y = -2499.0f; double v321 = p3x * p2y; std::cout.precision(15); std::cout << v321 << std::endl;
呃,好像x8六、x64都是这个合理的结果 -202014160。奇了个怪了。其实上面这段C++代码在不一样的平台下的结果以下:ide
补充说明:当初这篇文章投稿到酷壳,著名程序员左耳朵耗子那边,这部分结果数据来自耗子叔对文章作的部分调整。(由于当初行文没抓住重点,还引来了很多吐槽)
合理的运算结果,应该是-202014160,正确的运算结果是-202014162,合理性是浮点精度不够形成的(后文解释了合理性)。如果用两个double相乘可得正确且合理的运算结果。// 就别纠结我用的“正确、合理”这两个词是否恰当了。问题是为什么C#下X64和X86结果不一致?学习
为什么 80838.0f * -2499.0f = -202014160.0 是合理的?测试
32位浮点数在计算机中的表示方式为:1位符号位(s)-8位指数位(E)-23位有效数字(M),即:
其中E是实际转换成1.xxxxx*2^E的指数,M是去掉 1 后的前面的xxxxx(节约1位)。调试
80838.0 如何表达? 0 = 1 0011 1011 1100 0110.0(二进制) = 1.0011 1011 1100 0110 0*2^16 有效位M = 0011 1011 1100 0110 0000 000(一共 23 位) 指数位E = 16 + 127 = 143 = 10001111 内部表示 80838.0 = 0 [10001111] [0011 1011 1100 0110 0000 000] = 0100 0111 1001 1101 1110 0011 0000 0000 = 47 9d e3 00 //实际调试时看到的内存值 多是00 e3 9d 47是由于调试环境用了小端表示法法:低位字节排内存低地址端,高位排内存高地址
2. -2499.0 如何表达? -2499.0 = -100111000011.0 = -1.001110000110 * 2^11 有效位M = 0011 1000 0110 0000 0000 000 指数位E = 11+127=138= 10001010 符号位s = 1 内部表示-2499.0 = 1 [10001010] [0011 1000 0110 0000 0000 000] =1100 0101 0001 1100 0011 0000 0000 0000 =c5 1c 30 00
3. 如何计算 80838.0 * -2499.0 = ? 指数 e = 11+16 = 27 则指数位 E = e + 127 = 154 = 10011010 有效位相乘结果为 1.1000 0001 0100 1111 1011 1010 01 (能够本身动手实际算下),实际中只能有23位,后面的被截断即1000 0001 0100 1111 1011 1010 01,相乘结果内部表示=1[10011010][1000 0001 0100 1111 1011 101] = 1100 1101 0100 0000 1010 0111 1101 1101 = cd 40 a7 dd 结果 = -1.1000 0001 0100 1111 1011 101 *2^27 = -11000 0001 0100 1111 1011 1010000 = -202014160
经过上面得知,32 位浮点数,-202014160 就是合理的结果,彻底能解释清楚。但若是有效数字更长的话, 上面的就不会被截断。code
4. 正确的结果-202014162怎么得来? 有效位相乘结果为 1.1000 0001 0100 1111 1011 1010 01 即结果 = -1.1000 0001 0100 1111 1011 101001 *2^27 = -11000 0001 0100 1111 1011 101001 = -202014162
上面部分解释了两种结果的来源,但貌似没从根本回到为何?用C++一样的代码,X86,X64(DEBUG下,这个后面会说)下获得一致的结果-202014160,容易理解且也是合理的。缘由何在?看下编译后生成的代码(截取关键部分)blog
//C# x86 下 ...... float p3x = 80838.0f; 0000003b mov dword ptr [ebp-40h],479DE300h float p2y = -2499.0f; 00000042 mov dword ptr [ebp-44h],0C51C3000h double v321 = p3x * p2y; 00000049 fld dword ptr [ebp-40h] 0000004c fmul dword ptr [ebp-44h] 0000004f fstp qword ptr [ebp-4Ch] ....... //C# X64下 ...... float p3x = 80838.0f; 00000045 movss xmm0,dword ptr [00000098h] 0000004d movss dword ptr [rbp+3Ch],xmm0 float p2y = -2499.0f; 00000052 movss xmm0,dword ptr [000000A0h] 0000005a movss dword ptr [rbp+38h],xmm0 double v321 = p3x * p2y; 0000005f movss xmm0,dword ptr [rbp+38h] 00000064 mulss xmm0,dword ptr [rbp+3Ch] 00000069 cvtss2sd xmm0,xmm0 0000006d movsd mmword ptr [rbp+30h],xmm0 ......
C++ x86 / x64下都生成了相似的代码(这也就是为什么 C++ x86/x64与C#x64结果一致)即都用了先用浮点乘起来(mulss),而后转成double(cvtss2sd)。从上面的汇编代码能够看出 C# X86生成代码用的指令fld/fmul/fstp等。其中fld/fmul/fstp等指令是由FPU(float point unit)浮点运算处理器作的,FPU在进行浮点运算时,用了80位的寄存器作相关浮点运算,而后再根据是float/double截取成32位或64位。非FPU的状况是用了SSE中128位寄存器(float实际只用了其中的32位,计算时也是以32位计算的),这就是致使上述问题产生的最终缘由。
浮点运算标准IEEE-754 推荐标准实现者提供浮点可扩展精度格式(Extended precision),Intel x86处理器有FPU(float point unit)浮点运算处理器支持这种扩展。C#的浮点是支持该标准的,其中其官方文档也提到了浮点运算可能会产生比返回类型更高精度的值(正如上面的返回值精度就超过了float的精度),并说明若是硬件支持可扩展浮点精度的话,那么全部的浮点运算都将用此精度进行以提升效率,举个例子xy/z, xy的值可能都在double的能力范围以外了,但真实状况可能除以z后又能把结果拉回到double范围内,这样的话,用了FPU的结果就会获得一个准确的double值,而非FPU的就是无穷大之类的了。
即产生如上的结果缘由是,两个浮点数相乘在非FPU的状况下,用了32位计算产生的结果致使结果存在偏差,而FPU是用了80位进行计算的,因此获得的结果是精度很高的,体如今本文的案例上就是个位数上的2。因此你们在写代码的时候得保证明际运行环境/测试环境/开发环境的一致性(包括OS架构啊、编译选项等)啊,否则莫名其妙的问题会产生(本文就是开发环境与运行环境不一致致使的问题,纠结了很久才发现是这个缘由);遇到涉及浮点运算的时候别忘了有多是这个缘由产生的;另外,float/double混用的状况得特别注意。
总结一下,本文经过分析以前遇到的一个疑难杂症带着你们一块回顾或者学习了一下计算机内部浮点数的表达,解决了疑问。有时候可能须要跟进到硬件底层,固然随着硬件技术的发展,可能之前理所固然的东西在新硬件的状况下也会有所不一样(例如文中提到的 FPU 也有更高端的技术来替换了,本人对于硬件这块了解很少,感兴趣能够查阅更多材料,阅读原文有更多参考资料)。
老规矩,若是有帮助(对你身边的其余人有帮助也行呀),写篇文章不容易,但愿亲多多帮忙“在看”,转发分享支持。