IEEE754、VAX、IBM浮点型介绍和.NET中互相转换

时间:2023-03-09 13:32:44
IEEE754、VAX、IBM浮点型介绍和.NET中互相转换

【题外话】

最近在做C3D文件的解析,好奇怪的是文件中竟然存储了CPU的类型,原本不以为然,结果后来读取一个文件发现浮点数全部读取错误。查了下发现虽然在上世纪80年代就提出了IEEE754要统一浮点数标准,但是到现在仍然有计算机采用不同方式存储浮点数。在某些非IEEE754标准的计算机产生的二进制文件中,如果拿到其他计算机中读取,如果不进行专门的转换,可能导致数据错误等问题。

【文章索引】

  1. IEEE754标准浮点数字的存储详解
  2. VAX及IBM浮点数字的存储和转换
  3. 双精度浮点数的处理

【一、IEEE754标准浮点数字的存储详解】

对于x86等常见的CPU,都是采用IEEE754存储和计算浮点型的,当然在.NET平台中浮点型也是IEEE754标准的。首先回顾下本科时学过的计算机组成原理,查了下课本发现是如下介绍IEEE754浮点数的存储的(唐朔飞版课本233页):

IEEE754、VAX、IBM浮点型介绍和.NET中互相转换

其中,S为数符,它表示浮点数的正负,但与其有效位(尾数)是分开的。阶码用移码表示,阶码的真值都被加上一个常数(偏移量),如短实数、长实数和临时实数的偏移量用十六进制表示分别为7FH、3FFH和3FFFH。尾数部分通常都是规格化表示,即非“0”的有效位最高位总是1。

以单精度浮点数为例,如果字节查看应该是如下这个样子的,数符占第1字节的第1位,阶码占第1字节的后7位及第二字节的第1位,其余都是尾数。

SEF      S        EEEEEEEE        FFFFFFF        FFFFFFFF        FFFFFFFF
bits
bytes byte1 byte2 byte3 byte4

如果设数符为S,阶码为E,尾数的小数部分为F,那么可以通过位运算得到这三位:

Double S = (byte1 & 0x80) >> ;
Double E = ((byte1 & 0x7F) << ) + ((byte2 & 0x80) >> );
Double F = ((byte2 & 0x7F) << ) + (byte3 << ) + byte4;

由于阶码用移码表示,所以真实的阶码则是E - 0x7F。而尾数由于是规格化的表示,即将尾数规范化为(1.FFFFF……FF)2,但只存小数点之后的部分。由于1 / 2 + 1 / 4 + 1 / 8 + ... + 1 / n = 1 - 1 / 2n,所以可知尾数M(M = 1.0 + F)的范围为1 <= M <= 2 - 1 / 223

所以可通过如下的公式来计算浮点数的值,其中,C是尾数规范化后减去的常量,B是移码的偏移量,可知A、B、C分别为A = 2、B = 0x7F以及C = 1.0。

V = (-)^S * (F + C) * A^(E - B)

可见,浮点数就不存在0的概念了,所以只能用极限小来表示,同时为了表示无穷大,规定E取值范围为0 < E < 0xFF,即-0x7F < (E - B) < 0x80。

所以,当E = 0xFF时,指数最大,规定F = 0时为无穷值,其中又有S = 0为正无穷以及S = 1为负无穷;而F != 0时为无效数字(NaN)。

当E = 0时,指数最小,规定F = 0时为0,其中又有S = 0为正0以及S = 1时为-0。

不过表示非常小的数字,允许当E = 0时非规范化的尾数存在。即当E = 0且F !=0时,V = (-1)^S * F * A^-126。

二进制表示 十六进制表示 含义 十进制表示
0 11111111 00000000000000000000000 7F 80 00 00 正无穷 +∞ 
1 11111111 00000000000000000000000 FF 80 00 00 负无穷 -∞ 
0 00000000 00000000000000000000000 00 00 00 00 +0 0
1 00000000 00000000000000000000000 80 00 00 00 -0 0
0 00000000 00000000000000000000001 00 00 00 01  最小正数  1.401298E-45
0 11111110 11111111111111111111111 7F 7F FF FF 最大值 3.402823E+38
1 11111110 11111111111111111111111 FF 7F FF FF 最小值 -3.402823E+38
0 01111111 00000000000000000000000 3F 80 00 00 +1 1

而二进制小数转十进制小数的计算可以直接按整数的转换来做,然后除以2n即可,n在这里其实就是尾数的长度,为23。

所以,有了以上的这些信息,我们就可以将浮点数字与字节数组相互转换了(本文假定给定的字节数组都是Litten-Endian):

 Single ToSingle(Byte[] data)
{
Double a = 2.0;
Double b = 127.0;
Double c = 1.0;
Double d = -126.0; Byte byte1 = data[];
Byte byte2 = data[];
Byte byte3 = data[];
Byte byte4 = data[]; Double s = (byte1 & 0x80) >> ;
Double e = ((byte1 & 0x7F) << ) + ((byte2 & 0x80) >> );
Double f = ((byte2 & 0x7F) << ) + (byte3 << ) + byte4;
Double m = f / Math.Pow(, ); if (e == 0xFF && f == ) return (s == ? Single.PositiveInfinity : Single.NegativeInfinity);
if (e == 0xFF && f != ) return Single.NaN;
if (e == 0x00 && f == ) return ;
if (e == 0x00 && f != ) return (Single)((s == ? 1.0 : -1.0) * m * Math.Pow(a, d)); return (Single)((s == ? 1.0 : -1.0) * (c + m) * Math.Pow(a, e - b));
} Byte[] GetBytes(Single num)
{
Double a = 2.0;
Double b = 127.0;
Double c = 1.0;
Double d = Math.Log(); Int32 s = (num >= ? : ); Double v = Math.Abs(num);
Int32 e = (Int32)(Math.Log(v) / d + b); Double m = (v / Math.Pow(a, e - b)) - c;
Int32 f = (Int32)(m * Math.Pow(, )); Byte[] data = new Byte[];
data[] = (Byte)((s << ) + ((e & 0xFE) >> ));
data[] = (Byte)(((e & 0x01) << ) + ((f & 0x007F0000) >> ));
data[] = (Byte)((f & 0x0000FF00) >> );
data[] = (Byte)(f & 0x000000FF); return data;
}

上述的浮点数转字节数组不能支持NaN和非规范化的情况,当然也可以自己判断下。当然了,上边说了这么多还是为了介绍下边两种浮点数做铺垫。如果实现系统浮点数与字节数组转换的话,用上边这种方法转换就不如用System.BitConverter来的方便了。

【二、VAX及IBM浮点数字的存储和转换】

首先还是按字节看下VAX和IBM浮点型的存储:

VAX单精度浮点:

SEF         S        EEEEEEEE        FFFFFFF        FFFFFFFF        FFFFFFFF
bits
bytes byte2 byte3 byte0 byte1

IBM单精度浮点:

SEF         S        EEEEEEE        FFFFFFFF        FFFFFFFF        FFFFFFFF
bits
bytes byte1 byte2 byte3 byte4

非常有意思的是,VAX存储的结构并不是按顺序存储的,而是采用了一种叫做Middle-Endian的存储方式来存储(并非字节序):对于四字节而言其顺序就是2301,八字节为23016745,十六字节为23016745AB89EFCD。不过总体来说,VAX浮点型与IEEE754还是很类似的,比如VAX也要进行规范化,但是其规范化为(0.1FFFFF..FF)2,所以上述的C就为0.5,其尾数M的范围即为1/2 <= M <= 1 - 1 / 224;而同时其也并没有规定无穷大,不需要单独为无限大留出最大的阶码,所以上述的B为0x80。

而IBM单精度浮点则与上述两种差别更大。首先其阶码并不是8位,而是7位,由于还是使用移码存储的阶码,所以其减去的不能是127或者128,而是64,所以其与VAX一样,也没有无穷值的表示。除此之外,其也不是以2为底计算阶码的,而是以16为底,并且其没有规范化尾数的要求(当然这也与其以16为底有关),所以不需要对尾数进行加减运算,其范围为1/16 <= M <= 1- 1 / 224

以下是实现VAX浮点字节数组与系统浮点数字相互转化的类:

 using System;

 namespace DotMaysWind.Numerics
{
/// <summary>
/// VAX单精度浮点数字
/// </summary>
/// <remarks>
/// SEF S EEEEEEEE FFFFFFF FFFFFFFF FFFFFFFF
/// bits 1 2 9 10 32
/// bytes byte2 byte1 byte4 byte3
/// </remarks>
public struct VAXSingle
{
#region 常量
private const Int32 LENGTH = ;
private const Double BASE = 2.0;
private const Double EXPONENT_BIAS = 128.0;
private const Double MANTISSA_CONSTANT = 0.5;
private const Double E24 = 16777216.0;
#endregion #region 字段
private Byte[] _data;
#endregion #region 构造方法
/// <summary>
/// 初始化新的VAX单精度浮点数字
/// </summary>
/// <param name="data">VAX单精度浮点数字字节数组</param>
/// <param name="startIndex">数据起始位置</param>
public VAXSingle(Byte[] data, Int32 startIndex)
{
this._data = new Byte[VAXSingle.LENGTH];
Array.Copy(data, startIndex, this._data, , VAXSingle.LENGTH);
} /// <summary>
/// 初始化新的VAX单精度浮点数字
/// </summary>
/// <param name="num">系统标准的单精度浮点数字</param>
public VAXSingle(Single num)
{
Int32 s = (num >= ? : ); Double v = Math.Abs(num);
Int32 e = (Int32)(Math.Log(v) / Math.Log(2.0) + 1.0 + VAXSingle.EXPONENT_BIAS); Double m = (v / Math.Pow(VAXSingle.BASE, e - VAXSingle.EXPONENT_BIAS)) - VAXSingle.MANTISSA_CONSTANT;
Int32 f = (Int32)(m * VAXSingle.E24); this._data = new Byte[VAXSingle.LENGTH];
this._data[] = (Byte)((s << ) + ((e & 0xFE) >> ));
this._data[] = (Byte)(((e & 0x01) << ) + ((f & 0x007F0000) >> ));
this._data[] = (Byte)((f & 0x0000FF00) >> );
this._data[] = (Byte)(f & 0x000000FF);
}
#endregion #region 方法
/// <summary>
/// 获取系统标准的单精度浮点数字
/// </summary>
/// <returns>系统标准的单精度浮点数字</returns>
public Single ToSingle()
{
Byte b1 = this._data[];
Byte b2 = this._data[];
Byte b3 = this._data[];
Byte b4 = this._data[]; Double s = (b1 & 0x80) >> ;
Double e = ((b1 & 0x7F) << ) + ((b2 & 0x80) >> );
Double f = ((b2 & 0x7F) << ) + (b3 << ) + b4;
Double m = f / VAXSingle.E24; if (e == && s == ) return ;
if (e == && s == ) return Single.NaN; return (Single)((s == ? 1.0 : -1.0) * (VAXSingle.MANTISSA_CONSTANT + m) * Math.Pow(VAXSingle.BASE, e - VAXSingle.EXPONENT_BIAS));
} /// <summary>
/// 获取VAX单精度浮点数据字节数组
/// </summary>
/// <returns>字节数组</returns>
public Byte[] ToArray()
{
Byte[] data = new Byte[VAXSingle.LENGTH]; Array.Copy(this._data, data, VAXSingle.LENGTH); return data;
}
#endregion
}
}

以下是实现IBM浮点字节数组与系统浮点数字相互转化的类:

 using System;

 namespace DotMaysWind.Numerics
{
/// <summary>
/// IBM单精度浮点数字
/// </summary>
/// <remarks>
/// SEF S EEEEEEE FFFFFFFF FFFFFFFF FFFFFFFF
/// bits 1 2 8 9 32
/// bytes byte1 byte2 byte3 byte4
/// </remarks>
public struct IBMSingle
{
#region 常量
private const Int32 LENGTH = ;
private const Double BASE = 16.0;
private const Double EXPONENT_BIAS = 64.0;
private const Double E24 = 16777216.0;
#endregion #region 字段
private Byte[] _data;
#endregion #region 构造方法
/// <summary>
/// 初始化新的IBM单精度浮点数字
/// </summary>
/// <param name="data">IBM单精度浮点数字字节数组</param>
/// <param name="startIndex">数据起始位置</param>
public IBMSingle(Byte[] data, Int32 startIndex)
{
this._data = new Byte[IBMSingle.LENGTH];
Array.Copy(data, startIndex, this._data, , IBMSingle.LENGTH);
} /// <summary>
/// 初始化新的IBM单精度浮点数字
/// </summary>
/// <param name="num">系统标准的单精度浮点数字</param>
public IBMSingle(Single num)
{
Int32 s = (num >= ? : ); Double v = Math.Abs(num);
Int32 e = (Int32)(Math.Log(v) / Math.Log(2.0) / 4.0 + 1.0 + IBMSingle.EXPONENT_BIAS); Double m = (v / Math.Pow(IBMSingle.BASE, e - IBMSingle.EXPONENT_BIAS));
Int32 f = (Int32)(m * IBMSingle.E24); this._data = new Byte[IBMSingle.LENGTH];
this._data[] = (Byte)(s + e);
this._data[] = (Byte)((f & 0x00FF0000) >> );
this._data[] = (Byte)((f & 0x0000FF00) >> );
this._data[] = (Byte)(f & 0x000000FF);
}
#endregion #region 方法
/// <summary>
/// 获取系统标准的单精度浮点数字
/// </summary>
/// <returns>系统标准的单精度浮点数字</returns>
public Single ToSingle()
{
Byte b1 = this._data[];
Byte b2 = this._data[];
Byte b3 = this._data[];
Byte b4 = this._data[]; Double s = (b1 & 0x80) >> ;
Double e = (b1 & 0x7F);
Double f = (b2 << ) + (b3 << ) + b4;
Double m = f / IBMSingle.E24; if (e == && f == && s == ) return ; return (Single)((s == ? 1.0 : -1.0) * m * Math.Pow(IBMSingle.BASE, e - IBMSingle.EXPONENT_BIAS));
} /// <summary>
/// 获取IBM单精度浮点数据字节数组
/// </summary>
/// <returns>字节数组</returns>
public Byte[] ToArray()
{
Byte[] data = new Byte[IBMSingle.LENGTH]; Array.Copy(this._data, data, IBMSingle.LENGTH); return data;
}
#endregion
}
}

【三、双精度浮点数的处理】

双精度浮点数与单精度浮点数类似,只不过会扩大阶码和尾数的范围罢了。对于IEEE754的双精度浮点而言,不仅尾数的位数增加,还会增加阶码的尾数,字节存储如下:

SEF    S     EEEEEEE EEEE  FFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF
bits
bytes byte1 byte2 byte3 byte4 byte5 byte6 byte7 byte8

可见,其阶码增加了3位,即最大值是原来翻了3翻,为1024。而为了保证能表示无穷值,所以B为1023。除此之外只需要多读取后边增加的尾数即可,步骤与单精度基本相同。

而对于VAX和IBM的双精度浮点,更是没有扩大阶码的范围,而只是扩大了尾数的范围,使得只要多读取增加的4位尾数即可,而常数A、B、C更是无需修改。两者字节存储如下:

VAX双精度浮点:

SEF    S     EEEEEEEE     FFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF
bits
bytes byte2 byte3 byte0 byte1 byte6 byte7 byte4 byte5

IBM双精度浮点:

SEF    S     EEEEEEE  FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF
bits
bytes byte1 byte2 byte3 byte4 byte5 byte6 byte7 byte8

【相关链接】

  1. Transform between IEEE, IBM or VAX floating point number formats and bytes expressions:http://www.codeproject.com/Articles/12363/Transform-between-IEEE-IBM-or-VAX-floating-point-n
  2. VAX F_FLOAT and D_FLOAT to IEEE T_FLOAT and S_FLOAT (double):http://yaadc.blogspot.com/2013/01/vax-ffloat-and-dfloat-to-ieee-tfloat.html
  3. IEEE Arithmetic:http://docs.oracle.com/cd/E19957-01/806-3568/ncg_math.html
  4. Floating-Point:http://andromeda.rutgers.edu/~dupre/231/lecture13.doc
  5. IBM Floating Point Architecture:http://en.wikipedia.org/wiki/IBM_Floating_Point_Architecture
  6. VAX floating point to Decimal:http://arstechnica.com/civis/viewtopic.php?f=20&t=171682