浮点数在存储器中的表示与计算

    技术2022-05-20  39

     1 ,变量在内存中的存储

     

    Microsues 发的微薄:

    C++ 中,把负值赋给 unsigned 对象是合法的,其结果是该负数对该类型的取值个数求模后的值。例如:把 -1 赋给 8 位的 unsigned char ,因为 -1256 求模后的值为 255 ,所以结果是 255

     

    我的回复:

    c/c++ 中赋值很随意的,只需要做好 " 合法 " 的类型转换。本质上是数 / 对象如何在机器中表示的问题,一个内存中的数,可以是 char, int, pointer, double.... 任何类型。你的例子中 , 内存中写入了一个 0xFF 字节,而

    0xFF=-1(char)=255(u char)=3.57331e-043(float)=......

     

    为此我写了两行代码

    float f = 3.57331e-043f; cout << f << '/t' << (int)*(char*)&f << '/t' << (unsigned int)*(unsigned char*)&f << '/t' << std::hex << *(unsigned int*)&f << endl;

    这两行代码的输出是:

    3.57331e-043 -1 255 ff

     

    也就是说 0x000000FF 表示浮点数的 3.57331e-043 ,看起来有点不可思议,这是怎么来的呢?

     

     2 :浮点数在内存中的表示

    IEEE 规定,浮点标准用

       

    形式来表示一个数,其中:

    S 是符号 (sign)M 是有效数 (significand) ,指数2 的 E 次幂,注意是2 不是 10

     

    浮点数的位分成三个区域表示这些值:

    1)s=1 个符号位 s ,直接编码 S

    2) k 位的指数编码 E

    3) n 位小数编码 M

     

    C 语言中, float 类型对应 s=1, k = 8, n=23 ,表示 32 位的浮点数, double 类型对应 s=1,k=11,n=5264 位浮点数。

     

    表1.  数的存储

     

    符号域指数域小数域Bias单精度 1 [31]8 [30-23]23 [22-00]127双精度1 [63]11 [62-52]52 [51-00]1023

     

     

    float 为例详细说明:

    符号位很好理解, 0 表示正, 1 表示负,指数位 E 和有效 M 位比较绕了, IEEE 规定了 3 种情况:

    1、  规格化:当 E 的二进制位不全为 0, 也不全为 1 时, V 为规格化形式。此时指数为被解释为表示偏置( biased )形式的整数:

    E = e - Bias ,    e 是无符号数,e=ek-1 ...e0 也就是指数位的2 进制数,而Bias = 2k-1 - 1

    因此 floatbias127 ,取值范围 -126~+127doublebias1023 ,取值范围 -1022~1023

    有效位M=1+f ,其中f=fn--1 ....f1 f0 就是 n 位的小数编码 ,也就是M= 1.fn--1 ....f1 f0整数部分1 是隐含的第一位,省略掉,因为这样可以获得额外的 1 位精度。总之:

    (公式1        E = e - Bias, M = 1 + f, Bias = 2k-1 - 1

    2、  非规格化:当 E 的二进制位全部为 0 时, V 为非规格化形式。此时 EM 的计算都非常简单:

    (公式2        E = 1 - Bias, M = f, Bias = 2k-1 - 1

    此时小数点左侧的隐含位为 0

     

    为什么 E 会等于 (1-bias) 而不是 (-bias) ,这主要是为规格化数值、非规格化数值之间的平滑过渡设计的。有了非规格化形式,我们就可以表示 0 了。把符号位 S1, 其余所有位均置 0 后,我们得到了 -0.0; 同理,把所有位均置 0, 则得到 +0.0 。非规格化数还有其他用途,比如表示非常接近 0 的小数,而且这些小数均匀地接近 0, 称为“逐渐下溢 (gradually underflow) ”属性。

     

    3 、特殊数值:当 E 的二进制位全为 1 时为特殊数值。此时,若 M 的二进制位全为 0 ,则 n 表示无穷大,若 S1 则为负无穷大,-INF,若 S0 则为正无穷大,+INF ; M 的二进制位不全为 0 时,表示 NaN(Not a Number) ,表示这不是一个合法实数或无穷,或者该数未经初始化。

     

    NaN有两种,QNaN和SNaN,QNaN表示一个不确定的运算,而SNaN表示一个错误的运算。例如INF-INF是一个不确定的运算,而1/0是一个错误的运算。

     

    3: 无穷/NaN的运算和判断

     

    在数值计算中非常重要. 

     

    表2. 关于无穷的运算可以总结成如下表格:

    运算结果n ÷ ±Infinity 0 ±Infinity × ±Infinity ±Infinity ±nonzero ÷ 0 ±Infinity Infinity + Infinity Infinity ±0 ÷ ±0 NaN Infinity - Infinity NaN ±Infinity ÷ ±Infinity NaN ±Infinity × 0 NaN

     

    在C语言中,可以用float.h中的

    int _isnan(double x),返回布尔值

    int _finite(double x),返回布尔值

    int _fpclass(double x)函数来判断

    fpclass的返回结果代表意义分别为

    _FPCLASS_SNAN (Signaling NaN)

    _FPCLASS_QNAN (Quiet NaN)

    _FPCLASS_NINF (Negative Infinity, –INF)

    _FPCLASS_PINF (Positive Infinity, +INF)

    在C++中,可以用STL中的limits类:

    numeric_limits::quiet_NaN()

    numeric_limits::signaling_NaN()

    numeric_limits::infinity()等方法判断

     

    4. 练习

    1)现在回到本文最初提出的问题, 计算 0x000000FF 表示的浮点数:

    因此s=0, 是一个正数

    指数位 e 的每位都是 0 ,是非规格化表示,使用公式 2

     

    这就是所求的结果。

    2)那么现在,我们把指数的最低位置成 1 ,也就是从左边数的第 9 位置为 1 ,这样就变成了规格化表示:

    用公式 1 计算 :

    有趣的是,这里的 E 同样也是 126 ,但是由于 M 不同,得到的 V 也不同,而且相差非常大,竟然有105 数量级。

     

    3)对于特殊的情况,我们除了使用输出f之外,再使用C runtime library中的几个函数如_fpclass做一个判断

    A. 现在把所有的指数位都置成1,也就是从左边的第2位到第9位都置为1,这样就是第三种特殊情况:

    0x7F8000FF16 =011111111000000000000000111111112

    这个数的M位不全为0,因此应该是一个 NaN(Not a Number),

     

    B. 然后我们让所有的小数位都为0,也就是

    0x7F80000016 =011111111000000000000000000000002

     

    C. 再把符号位设置为1,

    0xFF80000016 =111111111000000000000000000000002

     

    D. 最后我们做几个特殊运算0/0, INF/0, 0/INF, 看看是什么效果

     

    全部代码如下:

    #include <iostream> #include <stdio.h> #include <stdlib.h> #include <float.h> int _tmain(int argc, _TCHAR* argv[]) { float f = 3.57331e-043f; cout << "f is " << f << endl; *(int*)&f |= 0x800000; cout << "f is " << f << endl << endl; *(int*)&f |= 0x7F800000; cout << "f is " << f << " , " << "it is a NaN? " << _isnan(f) << " , " << "fpclass is " << _fpclass(f) << endl; *(int*)&f &= 0x7F800000; cout << "f is " << f << " , " << "it is a finite number? " << _finite(f) << " , " << "fpclass is " << _fpclass(f) << endl; *(int*)&f |= 0xFF800000; cout << "f is " << f << " , " << "it is a finite number? " << _finite(f) << " , " << "fpclass is " << _fpclass(f) << endl << endl; f = 0; float p = 0 / f; cout << "0/0 is " << p << " , " << "it is a NaN? " << _isnan(p) << " , " << "fpclass is " << _fpclass(p) << endl; p = 0x7F800000/f; cout << "INF/0 is " << p << " , " << "it is a finite number? " << _finite(p) << " , " << "fpclass is " << _fpclass(p) << endl; p = f/0x7F800000; cout << "0/INF is " << p << " , " << "it is a finite number? " << _finite(p) << " , " << "fpclass is " << _fpclass(p) << endl; }

    在 MS VC++2008下,运行结果如下:

    f is 3.57331e-043 f is 1.17553e-038 f is 1.#QNAN , it is a NaN? 1 , fpclass is 2 f is 1.#INF , it is a finite number? 0 , fpclass is 512 f is -1.#INF , it is a finite number? 0 , fpclass is 4 0/0 is -1.#IND , it is a NaN? 1 , fpclass is 2 INF/0 is 1.#INF , it is a finite number? 0 , fpclass is 512 0/INF is 0 , it is a finite number? 1 , fpclass is 64

    其中的 fpclass ,请对照float.h中的定义自行理解.

     

     

    5.  关于浮点数的总结:

    根据以上说明,可以总结成如下的表格:

     

    表3 . 全部的总结

    浮点数值(下表中 b = bias) 符号位指数位(e )小数位(f )数值 0 00..00 00..00 +0 0 00..00 00..01 : 11..11 Positive Denormalized Real 0.f × 2(-b +1) 0 00..01 : 11..10 XX..XX Positive Normalized Real 1.f × 2(e -b ) 0 11..11 00..00 +Infinity 0 11..11 00..01 : 01..11 SNaN 0 11..11 10..00 : 11..11 QNaN 1 00..00 00..00 -0 1 00..00 00..01 : 11..11 Negative Denormalized Real -0.f × 2(-b +1) 1 00..01 : 11..10 XX..XX Negative Normalized Real -1.f × 2(e -b ) 1 11..11 00..00 -Infinity 1 11..11 00..01 : 01..11 SNaN 1 11..11 10..00 : 11.11 QNaN

     

    通过使用公式1和公式2,可以计算得到:

     

    表4 . 浮点数表示的范围

    非规格化规格化表示数的范围 单精度 ± 2-149 to (1-2-23 )×2-126 ± 2-126 to (2-2-23 )×2127 ± ~10-44.85 to ~1038.53 双精度 ± 2-1074 to (1-2-52 )×2-1022 ± 2-1022 to (2-2-52 )×21023 ± ~10-323.3 to ~10308.3

     

     

    6:字节顺序

    需要强调的一个问题是字节顺序,在此不做详细说明,只是说明本文的例子。

    在存储器中,不同的操作系统有两种顺序,按照以下方法记忆就可以:

    1) 小端法 little endian ,就是同从左到右的书写顺序“相反”,这里的相反是按找字节相反,而不是全部逆序

    2) 大端法 big-endian 就是同从左到右的书写顺序相同

    采用什么方法并无明确规定,就像吃鸡蛋先从大头吃还是从小头吃都是一样的, Intel 的机器采用小头法,最高字节在最右边,最低字节在最左边,正好是反的,比如上例中:

    0x000000FF ,在内存中存储为 FF 00 00 00

    0x008000FF ,在内存中存储为 FF 00 80 00 , 注意这里 80 的位置

    在这种平台上, 如果从内存中读取数值时,无论是什么数据类型,一定要先反向排列,然后再进行计算

     

    7. 参考文献

    关于浮点数的话题还有很多很多,尤其是在 浮点数 计算的时候,会有一些让人惊讶的表现,  一些编译器的也许会有bug.更深入的资料有:

     

    1.  Computer Systems, A Programmer’s Perspective (CSAPP) ,中译本叫深入理解计算机系统

    2. M. Overton. Numerical Computing with IEEE floating Point Arithmetic

    3 . IEEE 754: Standard for Binary Floating-Point Arithmetic ,http://steve.hollasch.net/cgindex/coding/ieeefloat.html   http://grouper.ieee.org/groups/754/


    最新回复(0)