C# 在x86-x64环境不同的浮点”bug”解析

最近一个项目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。呃,想通了,因为浮点运算的误差,-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;

呃,好像x86、x64都是这个合理的结果-202014160。奇了个怪了。

合理的运算结果,应该是-202014160,正确的运算结果是-202014162,合理性是浮点精度不够造成的(文后解释了合理性)。若是用两个double相乘可得正确且合理的运算结果。//就别纠结我用的“正确、合理”这两个词是否恰当了。问题是为何C#下X64和X86结果不一致?

用C++同样的代码,X86,X64(DEBUG下,这个后面会说)下得到一致的结果-202014160,容易理解且也是合理的。原因何在?看下编译后生成的代码(截取关键部分)

//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的精度),并说明如果硬件支持可扩展浮点精度的话,那么所有的浮点运算都将用此精度进行以提高效率,举个例子x*y/z, x*y的值可能都在double的能力范围之外了,但真实情况可能除以z后又能把结果拉回到double范围内,这样的话,用了FPU的结果就会得到一个准确的double值,而非FPU的就是无穷大之类的了。

即产生如上的结果原因是,两个浮点数相乘在非FPU的情况下,用了32位计算产生的结果导致结果存在误差,而FPU是用了80位进行计算的,所以得到的结果是精度很高的,体现在本文的案例上就是个位数上的2。 那么怎么避免这种情况呢?【update 这里说的解决方案是希望产生合理的结果,而不是正确的结果,即希望产生的结果是-202014160,求别吐槽我不知道直接用double就能得到正确且合理的结果了。】

对于C++来说有解决方案即禁用可扩展精度,VS2012中C++,编译选项可以设置(代码生成中)可选,/fp:[precise | fast | strict],本例中Release x86下用precise 或者 strict将得到合理的结果(-202014160),fast将产生正确的结果(-202014162), fast debug/release下结果也不一样哦(release下才优化了)。X64下各个结果可以大家自己去测试下(Debug/Release),分别看看VS编译后产生的中间代码长什么样。

但对于C#来说,目前还没找到解决方案。

所以大家在写代码的时候得保证实际运行环境/测试环境/开发环境的一致性(包括OS架构啊、编译选项等)啊,不然莫名其妙的问题会产生(本文就是开发环境与运行环境不一致导致的问题,纠结了好久才发现是这个原因);遇到涉及浮点运算的时候别忘了有可能是这个原因产生的;另外,float/double混用的情况得特别注意。

Reference:

[1] C# Language Specification Floating point types
[2] Are floating-point numbers consistent in C#? Can they be?
[3] The FPU Instruction Set

附80838.0f * -2499.0f = -202014160.0浮点运算过程的说明

32位浮点数在计算机中的表示方式为:1位符号位(s)-8位指数位(E)-23位有效数字(M)。
32位Float = (-1)^s * (1+m) * 2^(e-127), 其中e是实际转换成1.xxxxx*2^e的指数,m是前面的xxxxx(节约1位)

80838.0f = 1 0011 1011 1100 0110.0= 1.00111011110001100*2^16
有效位M = 0011 1011 1100 0110 0000 000
指数位E = 16 + 127 = 143 =  10001111
内部表示 80838.0 =  0 [1000 1111] [0011 1011 1100 0110 0000 000]
= 0100 0111 1001 1101 1110 0011 0000 0000
= 47 9d e3 00 //实际调试时看到的内存值 可能是00 e3 9d 47是因为调试环境用了小端表示法法:低位字节排内存低地址端,高位排内存高地址

-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

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
再转成double后还是-202014160.

如果是FPU的话,上面的有效位结果不会被截断,即
FPU结果 = -1.1000 0001 0100 1111 1011 101001 *2^27
= -11000 0001 0100 1111 1011 1010010
= -202014162

全文完,若本文有纰漏之处欢迎指正。

时间: 2024-09-20 13:33:56

C# 在x86-x64环境不同的浮点”bug”解析的相关文章

C#因x86-x64环境不同引发的浮点bug

float p3x = 80838.0f; float p2y = -2499.0f; double v321 = p3x * p2y; Console.WriteLine(v321); 很简单吧,马上笔算下结果为-202014162,没问题,难道C#没有产生这样的结果?不可能吧,开启VisualStudio,copy代码试试,果然结果是-202014162.就这样完了么?显然没有!你把编译时的选项从AnyCPU改成x64试试~(服务器环境正是64位滴哦!!)结果居然边成了-202014160,

32位-IOCP在x64环境下报错:内存访问冲突

问题描述 IOCP在x64环境下报错:内存访问冲突 一个IOCP的例子程序,WIN32平台能够正常运行,但是在配置管理器里把平台从WIN32改成x64之后,就会在 WSARecv(PerHandleData->socket, &(PerIoData->databuff), 1, &RecvBytes, &Flags, &(PerIoData->overlapped), NULL); 语句处报错. 但是我同时还要连接mysql数据库,当时安装的是x64的,所

c-如何编译x86 x64 都能用的dll

问题描述 如何编译x86 x64 都能用的dll 在网上下到过x86 x64都能用的sqlite3.dll 但是自己编译只能编译出只支持一种的 这个怎么编译啊 源码是C写的 解决方案 根本上讲32位和64位不能互相调用,除非通过一些封装手段. 解决方案二: dll没法互相调用,需要保持版本一致. 只能要exe来包装dll调用. 解决方案三: dll没法互相调用,需要保持版本一致. 只能要exe来包装dll调用. 解决方案四: 可以在源代码层面共享.或者使用进程外dllhttp://stackov

FC-FCoE Driver for RHEL 5.6/5.7 (x86/x64) V-8.03.07.09.5.6-k

下载 : http://filedownloads.qlogic.com FC-FCoE Driver for RHEL 5.6/5.7 (x86/x64) V-8.03.07.09.5.6-k 安装 : 4.1 Building the Driver for RHEL 5.x Linux 1. In the directory that contains the source driver file,     qla2xxx-src-x.xx.xx.xx.xx.xx-k.gz, issue t

x64环境下,把内嵌汇编的汇编单独放在.asm文件中

问题描述 x64环境下,把内嵌汇编的汇编单独放在.asm文件中 void GDIRender::YUV_TO_RGB24(unsigned char *puc_y int stride_y unsigned char *puc_u unsigned char *puc_v int stride_uv unsigned char *puc_out int width_y int height_yint stride_out) { int y horiz_count;unsigned char *p

[Tool]利用Advanced Installer建立x86/x64在一起的安装程式

原文 [Tool]利用Advanced Installer建立x86/x64在一起的安装程式 之前使用InstallShield做安装程式时,如果要将程式放在Program Files的话,需要分别针对x86及x64做一份安装程式,详细可参考「[InsallShield]x64无法设定安装目录为C:\Program Files? 」 . 一般来说,大多数的系统不会针对x86 or x64 分别去处理,而同一分安装内容,却要区分x86 or x64 的安装程式,还蛮累人的. 这时,就可以使用Adv

IBM Windows Server 2008 Standard X86/X64中文版下载

IBM Windows Server 2008 Standard X86/X64中文版下载 IBM 随即附带标准版系统Win2008标准中文版&http://www.aliyun.com/zixun/aggregation/37954.html">nbsp; OEM版系统 ======================================== 文件名称: IBM Win2008 Standard X64.iso文件大小: 3344982016 字节修改时间: 2010年1

NVIDIA发布Linux版X86/X64 v304.64显卡驱动

NVIDIA发布Linux版X86/X64 v304.64http://www.aliyun.com/zixun/aggregation/36046.html">显卡驱动 版本 304.64 Certified 发布日期 2012.11.06 操作系统 Linux 语言 Chinese (Simplified) 文件大小 37.5 MB   发布重点: 新增了对下列 GPU 的支持: VGX K1VGX K2 修正了在某些笔记本配置上背光控制功能退化的问题. 修正了在近期发布的 Linux

Windows x86/ x64 Ring3层注入Dll总结_win服务器

0x01.前言 提到Dll的注入,立马能够想到的方法就有很多,比如利用远程线程.Apc等等,这里我对Ring3层的Dll注入学习做一个总结吧. 我把注入的方法分成六类,分别是:1.创建新线程.2.设置线程上下背景文,修改寄存器.3.插入Apc队列.4.修改注册表.5.挂钩窗口消息.6.远程手动实现LoadLibrary. 那么下面就开始学习之旅吧! 0x02.预备工作 在涉及到注入的程序中,提升程序的权限自然是必不可少的,这里我提供了两个封装的函数,都可以用于提权.第一个是通过权限令牌来调整权限