我想对“非规范数据”有一个广泛的看法,因为我认为我唯一正确的是,从程序员的Angular 来看,它与浮点值特别相关,从CPU的Angular 来看,它与通用计算方法有关。有人能帮我破解这两个字吗?
j2cgzkjk1#
你会问C++,但浮点值和编码的细节是由浮点规范(特别是IEEE 754)决定的,而不是由C++决定的。IEEE 754是迄今为止使用最广泛的浮点规范,我将使用它来回答。在IEEE 754中,二进制浮点值编码为三部分:符号位s(0表示正,1表示负)、偏置指数e(所表示的指数加上固定偏移量)和有效位字段f(分数部分)。对于正常数,这些数字正好表示数字(−1)s·2x 1 m4n1x − bias· 1。f,其中1. f是通过在“1”之后写入有效位而形成的二进制数。(例如,如果有效数字段具有10位0010111011,则其表示有效数1.00101110112,其为1.182617175或1211/1024。偏差取决于浮点格式。对于64位IEEE 754二进制,指数字段具有11位,并且偏置为1023。当实际指数为0时,编码的指数字段为1023。实际的-2、-1、0、1和2的指数的编码指数为1021、1022、1023、1024和1025。当有人说一个次正态数的指数为零时,他们的意思是编码的指数为零。实际的指数将小于-1022。对于64位,正常指数区间为-1022至1023(编码值为1至2046)。当指数移动到这个区间之外时,会发生一些特殊的事情。在此指数区间以上,浮点数停止表示有限数。2047的编码指数(全部为1位)表示无穷大(有效数字段设置为零)。低于此指数间隔时,浮点数将更改为次正常数。当编码指数为零时,有效数字段表示0。f而不是1。f。这其中有一个重要的原因。如果最低指数值只是另一种正常编码,则其有效数的低位将太小而不能单独表示为浮点值。如果没有前导“1.”,就无法说出第一个1位在哪里。例如,假设你有两个数字,它们的指数都是最小的,有效数分别是1.00101110112和1.00000000002。当减去有效数时,结果为.00101110112。不幸的是,没有办法将其表示为正常数。因为您已经处于最低指数,所以您不能表示较低的指数,而较低的指数需要说明第一个1在此结果中的位置。由于数学结果太小而无法表示,计算机将被迫返回最接近的可表示数字,即零。这在浮点系统中创建了一个不受欢迎的属性,您可以有a != b,但a-b == 0。为了避免这种情况,使用了次正态数。通过使用次正态数,我们有一个特殊的区间,其中实际指数不会减少,并且我们可以执行算术,而不会创建太小而无法表示的数字。当编码指数为零时,实际指数与编码指数为一时相同,但有效数的值变为0。f而不是1。f。当我们这样做时,a != b保证a-b的计算值不为零。以下是64位IEEE 754二进制浮点编码中的值组合:| 指数(e)|有效位(f)|含义| Meaning || --|--|--| ------------ || 0| 0|+零| +zero || 0|非零|+2 1022·0.f(低于正常)| +2−1022·0. f (subnormal) || 1至2046年|任何事|+2e −1023·1。|f(正常) f (normal) || 2047年|0|+无穷大| +infinity || 2047年|非零但高位关闭|+,发信号NaN| +, signaling NaN || 2047年|高位开|+,安静NaN| +, quiet NaN || 0| 0| −零| −zero || 0|非零|−2−1022·0. f(亚正常)| −2−1022·0. f (subnormal) || 1至2046年|任何事|−2e −1023·1。|f(正常) f (normal) || 2047年|0| −无穷大| −infinity || 2047年|非零但高位关闭|−,信号NaN| −, signaling NaN || 2047年|高位开|−,quiet NaN| −, quiet NaN |一些注意事项:+0和-0在数学上是相等的,但符号保持不变。精心编写的应用程序可以在某些特殊情况下使用它。NaN的意思是“不是数字”。通常,它意味着发生了一些非数学结果或其他错误,应该放弃计算或以其他方式重新进行计算。通常,使用NaN的操作会产生另一个NaN,从而保留了出错的信息。例如,3 + NaN产生NaN。发信号NaN旨在引起异常,以指示程序出错或允许其他软件(例如,调试器)执行某些特殊动作。安静的NaN旨在传播到进一步的结果,允许在NaN仅是大数据集的一部分并且稍后将被单独处理或将被丢弃的情况下完成大计算的其余部分。
s
e
f
bias
a != b
a-b == 0
a-b
3 + NaN
符号+和−保留在NaN中,但没有数学值。在常规编程中,您不应该关心浮点编码,除非它告诉您浮点计算的限制和行为。你不需要做任何特殊的关于低于正常的数字。不幸的是,一些处理器被破坏,因为它们通过将次正常数改变为零而违反IEEE 754标准,或者当使用次正常数时它们执行得非常慢。在为这样的处理器编程时,您可能会设法避免使用次正常数。
93ze6v8z2#
要理解非正规浮点值,首先必须理解正规浮点值。浮点值具有尾数和指数。在十进制数值中,如1.2345E6,1.2345是尾数,6是指数。浮点表示法的一个好处是,你总是可以把它写成规范化的。例如0.012345E8和0.12345E7与1.2345E6的值相同。或者换句话说,只要尾数的值不为零,您总是可以使尾数的第一位数字为非零数字。计算机以二进制存储浮点值,数字是0或1。因此,不为零的二进制浮点值的一个属性是,它总是可以从1开始写入。这是一个非常有吸引力的优化目标。由于值总是以1开始,所以存储1* 没有意义。它的好处在于,你实际上可以免费获得额外的精度。在64位双精度型上,尾数有52位的存储空间。由于隐含的1,实际精度为53位。我们必须讨论一下这种方式可以存储的最小浮点值。首先用十进制来做,如果你有一个十进制处理器,尾数存储5位数,指数存储2位数,那么它可以存储的不为零的最小值是1.00000E-99。其中1是未存储的隐含数字(不适用于十进制,但请原谅我)。所以尾数存储00000,指数存储-99。不能存储更小的数字,指数在-99处达到最大值。你可以的您可以给予规范化表示,并忘记隐含的数字优化。您可以将其存储为“非规范化”。现在您可以存储0.1000E-99或1.000E-100。一直到0.0001E-99或1 E-103,这是您现在可以存储的绝对最小数字。这通常是可取的,它扩展了您可以存储的值的范围。这在实际计算中往往很重要,非常小的数字在微分分析等现实世界的问题中非常常见。然而,它也有一个很大的问题,你失去了去规范化的数字的准确性。浮点计算的准确性受到可以存储的位数的限制。这是直观的与假十进制处理器我用作为一个例子,它只能计算与5个有效数字。只要该值是标准化的,您总是得到5位有效数字。但你不正常化的时候会失去手指。0.1000E-99和0.9999E-99之间的任何值都只有4位有效数字。0.0100E-99和0.0999E-99之间的任何值都只有3位有效数字。一直到0.0001E-99和0.0009E-99,只剩下一个有效数字。这会大大降低最终计算结果的精度。更糟糕的是,它以一种高度不可预测的方式这样做,因为这些非常小的非归一化值往往会出现在更复杂的计算中。这当然是需要担心的,当最终结果只剩下1个有效数字时,您无法再真正信任它。浮点处理器有办法让你知道这一点,或者绕过这个问题。例如,当一个值变得非标准化时,它们可以生成一个中断或信号,让您中断计算。它们有一个“flush-to-zero”选项,状态字中的一个位告诉处理器自动将所有非正常值转换为零。这往往会产生无穷大,一个结果,告诉你,结果是垃圾,应该被丢弃。
zlwx9yxi3#
IEEE 754基础
首先让我们回顾一下IEEE 754数字的基本知识。让我们首先关注单精度(32位)。格式为:
如果你喜欢图片:
Source。标志很简单:0是正的,1是负的,故事结束。指数是8位长,因此它的范围从0到255。指数被称为偏置,因为它具有-127的偏移,例如:
-127
0 == special case: zero or subnormal, explained below 1 == 2 ^ -126 ... 125 == 2 ^ -2 126 == 2 ^ -1 127 == 2 ^ 0 128 == 2 ^ 1 129 == 2 ^ 2 ... 254 == 2 ^ 127 255 == special case: infinity and NaN
前导位约定
在设计IEEE 754时,工程师们注意到,除了0.0之外,所有数字的第一位都是二进制的1 1例如:
0.0
1
25.0 == (binary) 11001 == 1.1001 * 2^4 0.625 == (binary) 0.101 == 1.01 * 2^-1
都是从那个烦人的1.部分开始的。因此,让该数字占据几乎每个单个数字的精度位将是浪费的。为此,他们创建了“前导位约定”:总是假设数字以1开头那么如何处理0.0呢?他们决定创建一个例外:
1.
因此字节00 00 00 00也表示0.0,这看起来不错。如果我们只考虑这些规则,那么可以表示的最小非零数将是:
00 00 00 00
由于前导位约定,十六进制小数看起来像这样:
1.000002 * 2 ^ (-127)
其中.000002是22个零,结尾是1。我们不能取fraction = 0,否则这个数将是0.0。但后来同样有着敏锐艺术感的工程师们想:是不是很难看?我们从直线0.0跳到甚至不是2的正确幂的东西?难道我们不能以某种方式代表更小的数字吗?
.000002
fraction = 0
非正规数
工程师们挠了一会儿头,然后回来了,像往常一样,带着另一个好主意。如果我们创建一个新规则:如果指数为0,则:
这样的数被称为次正规数(或反正规数,这是同义词)。这条规则直接意味着该数字使得:
是0.0,这是一种优雅,因为它意味着少了一个需要跟踪的规则。所以根据我们的定义,0.0实际上是一个次正规数!有了这个新规则,最小的非次正态数是:
其表示:
1.0 * 2 ^ (-126)
那么,最大的次正态数是:
这等于:
0.FFFFFE * 2 ^ (-126)
其中.FFFFFE再次是点右边的23位。这是非常接近最小的非低于正常的数字,这听起来很正常。最小的非零次正规数是:
.FFFFFE
0.000002 * 2 ^ (-126)
它看起来也非常接近0.0!由于找不到任何合理的方法来表示小于这个数字的数字,工程师们很高兴,于是又回到了网上查看猫的照片,或者他们在70年代做的任何事情。正如您所看到的,次正态数在精度和表示长度之间进行了权衡。作为最极端的例子,最小的非零次法线:
基本上具有单个比特而不是32比特的精度。例如,如果我们将其除以2:
0.000002 * 2 ^ (-126) / 2
我们实际上正好到达0.0!
可运行的C示例
现在让我们用一些实际的代码来验证我们的理论。在几乎所有当前和桌面计算机中,C float表示单精度IEEE 754浮点数。我的Ubuntu 18.04 amd 64笔记本电脑尤其如此。在这个假设下,所有Assert都传递给以下程序:subnormal.c
float
#if __STDC_VERSION__ < 201112L #error C11 required #endif #ifndef __STDC_IEC_559__ #error IEEE 754 not implemented #endif #include <assert.h> #include <float.h> /* FLT_HAS_SUBNORM */ #include <inttypes.h> #include <math.h> /* isnormal */ #include <stdlib.h> #include <stdio.h> #if FLT_HAS_SUBNORM != 1 #error float does not have subnormal numbers #endif typedef struct { uint32_t sign, exponent, fraction; } Float32; Float32 float32_from_float(float f) { uint32_t bytes; Float32 float32; bytes = *(uint32_t*)&f; float32.fraction = bytes & 0x007FFFFF; bytes >>= 23; float32.exponent = bytes & 0x000000FF; bytes >>= 8; float32.sign = bytes & 0x000000001; bytes >>= 1; return float32; } float float_from_bytes( uint32_t sign, uint32_t exponent, uint32_t fraction ) { uint32_t bytes; bytes = 0; bytes |= sign; bytes <<= 8; bytes |= exponent; bytes <<= 23; bytes |= fraction; return *(float*)&bytes; } int float32_equal( float f, uint32_t sign, uint32_t exponent, uint32_t fraction ) { Float32 float32; float32 = float32_from_float(f); return (float32.sign == sign) && (float32.exponent == exponent) && (float32.fraction == fraction) ; } void float32_print(float f) { Float32 float32 = float32_from_float(f); printf( "%" PRIu32 " %" PRIu32 " %" PRIu32 "\n", float32.sign, float32.exponent, float32.fraction ); } int main(void) { /* Basic examples. */ assert(float32_equal(0.5f, 0, 126, 0)); assert(float32_equal(1.0f, 0, 127, 0)); assert(float32_equal(2.0f, 0, 128, 0)); assert(isnormal(0.5f)); assert(isnormal(1.0f)); assert(isnormal(2.0f)); /* Quick review of C hex floating point literals. */ assert(0.5f == 0x1.0p-1f); assert(1.0f == 0x1.0p0f); assert(2.0f == 0x1.0p1f); /* Sign bit. */ assert(float32_equal(-0.5f, 1, 126, 0)); assert(float32_equal(-1.0f, 1, 127, 0)); assert(float32_equal(-2.0f, 1, 128, 0)); assert(isnormal(-0.5f)); assert(isnormal(-1.0f)); assert(isnormal(-2.0f)); /* The special case of 0.0 and -0.0. */ assert(float32_equal( 0.0f, 0, 0, 0)); assert(float32_equal(-0.0f, 1, 0, 0)); assert(!isnormal( 0.0f)); assert(!isnormal(-0.0f)); assert(0.0f == -0.0f); /* ANSI C defines FLT_MIN as the smallest non-subnormal number. */ assert(FLT_MIN == 0x1.0p-126f); assert(float32_equal(FLT_MIN, 0, 1, 0)); assert(isnormal(FLT_MIN)); /* The largest subnormal number. */ float largest_subnormal = float_from_bytes(0, 0, 0x7FFFFF); assert(largest_subnormal == 0x0.FFFFFEp-126f); assert(largest_subnormal < FLT_MIN); assert(!isnormal(largest_subnormal)); /* The smallest non-zero subnormal number. */ float smallest_subnormal = float_from_bytes(0, 0, 1); assert(smallest_subnormal == 0x0.000002p-126f); assert(0.0f < smallest_subnormal); assert(!isnormal(smallest_subnormal)); return EXIT_SUCCESS; }
GitHub upstream。编译并运行:
gcc -ggdb3 -O0 -std=c11 -Wall -Wextra -Wpedantic -Werror -o subnormal.out subnormal.c ./subnormal.out
可视化
对我们所学的东西有一个几何直觉总是一个好主意,所以这里开始。如果我们为每个给定的指数在一条线上绘制IEEE 754浮点数,它看起来像这样:
+---+-------+---------------+ exponent |126| 127 | 128 | +---+-------+---------------+ | | | | v v v v ----------------------------- floats ***** * * * * * * * * ----------------------------- ^ ^ ^ ^ | | | | 0.5 1.0 2.0 4.0
由此我们可以看出,对于每个指数:
*
现在,让我们把它一直降到指数0。无次法线(假设):
+---+---+-------+---------------+ exponent | ? | 0 | 1 | 2 | +---+---+-------+---------------+ | | | | | v v v v v --------------------------------- floats * ***** * * * * * * * * --------------------------------- ^ ^ ^ ^ ^ | | | | | 0 | 2^-126 2^-125 2^-124 | 2^-127
使用次法线:
+-------+-------+---------------+ exponent | 0 | 1 | 2 | +-------+-------+---------------+ | | | | v v v v --------------------------------- floats * * * * * * * * * * * * * --------------------------------- ^ ^ ^ ^ ^ | | | | | 0 | 2^-126 2^-125 2^-124 | 2^-127
通过比较这两张图,我们可以看到:
0
[2^-127, 2^-126)
[0, 2^-126)
低于正常范围的浮点数之间的间距与[0, 2^-126)相同。
这些点的一半将填充范围的另一半。
[0, 2^-127)
[2^-128, 2^-127)
这就是我们所说的次法线是大小和精度之间的折衷。
在这个设置中,我们在0和2^-127之间有一个空的间隙,这不是很优雅。然而,区间被很好地填充,并且像任何其他一样包含2^23浮点数。
2^-127
2^23
实现方式
x86_64直接在硬件上实现IEEE 754,C代码将转换为硬件。TODO:有没有现代硬件没有次法线的显著例子?TODO:是否有任何实现允许在运行时控制它?在某些实现中,次法线似乎不如法线快:Why does changing 0.1f to 0 slow down performance by 10x?
Infinity和NaN
下面是一个简短的可运行示例:C语言中的浮点数数据类型的范围
qxgroojn4#
从IEEE Documentation如果指数全为0,但分数不为零(否则将被解释为零),则该值是一个非规范化的数,在二进制点之前没有假定的前导1。因此,这表示数字(-1)s × 0.f × 2-126,其中s是符号位,f是分数。对于双精度,非正规化数的形式为(-1)s × 0.f × 2-1022。由此,你可以将零解释为一种特殊类型的非正规化数。
4条答案
按热度按时间j2cgzkjk1#
你会问C++,但浮点值和编码的细节是由浮点规范(特别是IEEE 754)决定的,而不是由C++决定的。IEEE 754是迄今为止使用最广泛的浮点规范,我将使用它来回答。
在IEEE 754中,二进制浮点值编码为三部分:符号位
s
(0表示正,1表示负)、偏置指数e
(所表示的指数加上固定偏移量)和有效位字段f
(分数部分)。对于正常数,这些数字正好表示数字(−1)s
·2x 1 m4n1x −bias
· 1。f
,其中1.f
是通过在“1”之后写入有效位而形成的二进制数。(例如,如果有效数字段具有10位0010111011,则其表示有效数1.00101110112,其为1.182617175或1211/1024。偏差取决于浮点格式。对于64位IEEE 754二进制,指数字段具有11位,并且偏置为1023。当实际指数为0时,编码的指数字段为1023。实际的-2、-1、0、1和2的指数的编码指数为1021、1022、1023、1024和1025。当有人说一个次正态数的指数为零时,他们的意思是编码的指数为零。实际的指数将小于-1022。对于64位,正常指数区间为-1022至1023(编码值为1至2046)。当指数移动到这个区间之外时,会发生一些特殊的事情。
在此指数区间以上,浮点数停止表示有限数。2047的编码指数(全部为1位)表示无穷大(有效数字段设置为零)。低于此指数间隔时,浮点数将更改为次正常数。当编码指数为零时,有效数字段表示0。
f
而不是1。f
。这其中有一个重要的原因。如果最低指数值只是另一种正常编码,则其有效数的低位将太小而不能单独表示为浮点值。如果没有前导“1.”,就无法说出第一个1位在哪里。例如,假设你有两个数字,它们的指数都是最小的,有效数分别是1.00101110112和1.00000000002。当减去有效数时,结果为.00101110112。不幸的是,没有办法将其表示为正常数。因为您已经处于最低指数,所以您不能表示较低的指数,而较低的指数需要说明第一个1在此结果中的位置。由于数学结果太小而无法表示,计算机将被迫返回最接近的可表示数字,即零。
这在浮点系统中创建了一个不受欢迎的属性,您可以有
a != b
,但a-b == 0
。为了避免这种情况,使用了次正态数。通过使用次正态数,我们有一个特殊的区间,其中实际指数不会减少,并且我们可以执行算术,而不会创建太小而无法表示的数字。当编码指数为零时,实际指数与编码指数为一时相同,但有效数的值变为0。f
而不是1。f
。当我们这样做时,a != b
保证a-b
的计算值不为零。以下是64位IEEE 754二进制浮点编码中的值组合:
| 指数(
e
)|有效位(f
)|含义| Meaning || --|--|--| ------------ |
| 0| 0|+零| +zero |
| 0|非零|+2 1022·0.
f
(低于正常)| +2−1022·0.f
(subnormal) || 1至2046年|任何事|+2
e
−1023·1。|f
(正常)f
(normal) || 2047年|0|+无穷大| +infinity |
| 2047年|非零但高位关闭|+,发信号NaN| +, signaling NaN |
| 2047年|高位开|+,安静NaN| +, quiet NaN |
| 0| 0| −零| −zero |
| 0|非零|−2−1022·0.
f
(亚正常)| −2−1022·0.f
(subnormal) || 1至2046年|任何事|−2
e
−1023·1。|f
(正常)f
(normal) || 2047年|0| −无穷大| −infinity |
| 2047年|非零但高位关闭|−,信号NaN| −, signaling NaN |
| 2047年|高位开|−,quiet NaN| −, quiet NaN |
一些注意事项:
+0和-0在数学上是相等的,但符号保持不变。精心编写的应用程序可以在某些特殊情况下使用它。
NaN的意思是“不是数字”。通常,它意味着发生了一些非数学结果或其他错误,应该放弃计算或以其他方式重新进行计算。通常,使用NaN的操作会产生另一个NaN,从而保留了出错的信息。例如,
3 + NaN
产生NaN。发信号NaN旨在引起异常,以指示程序出错或允许其他软件(例如,调试器)执行某些特殊动作。安静的NaN旨在传播到进一步的结果,允许在NaN仅是大数据集的一部分并且稍后将被单独处理或将被丢弃的情况下完成大计算的其余部分。符号+和−保留在NaN中,但没有数学值。
在常规编程中,您不应该关心浮点编码,除非它告诉您浮点计算的限制和行为。你不需要做任何特殊的关于低于正常的数字。
不幸的是,一些处理器被破坏,因为它们通过将次正常数改变为零而违反IEEE 754标准,或者当使用次正常数时它们执行得非常慢。在为这样的处理器编程时,您可能会设法避免使用次正常数。
93ze6v8z2#
要理解非正规浮点值,首先必须理解正规浮点值。浮点值具有尾数和指数。在十进制数值中,如1.2345E6,1.2345是尾数,6是指数。浮点表示法的一个好处是,你总是可以把它写成规范化的。例如0.012345E8和0.12345E7与1.2345E6的值相同。或者换句话说,只要尾数的值不为零,您总是可以使尾数的第一位数字为非零数字。
计算机以二进制存储浮点值,数字是0或1。因此,不为零的二进制浮点值的一个属性是,它总是可以从1开始写入。
这是一个非常有吸引力的优化目标。由于值总是以1开始,所以存储1* 没有意义。它的好处在于,你实际上可以免费获得额外的精度。在64位双精度型上,尾数有52位的存储空间。由于隐含的1,实际精度为53位。
我们必须讨论一下这种方式可以存储的最小浮点值。首先用十进制来做,如果你有一个十进制处理器,尾数存储5位数,指数存储2位数,那么它可以存储的不为零的最小值是1.00000E-99。其中1是未存储的隐含数字(不适用于十进制,但请原谅我)。所以尾数存储00000,指数存储-99。不能存储更小的数字,指数在-99处达到最大值。
你可以的您可以给予规范化表示,并忘记隐含的数字优化。您可以将其存储为“非规范化”。现在您可以存储0.1000E-99或1.000E-100。一直到0.0001E-99或1 E-103,这是您现在可以存储的绝对最小数字。
这通常是可取的,它扩展了您可以存储的值的范围。这在实际计算中往往很重要,非常小的数字在微分分析等现实世界的问题中非常常见。
然而,它也有一个很大的问题,你失去了去规范化的数字的准确性。浮点计算的准确性受到可以存储的位数的限制。这是直观的与假十进制处理器我用作为一个例子,它只能计算与5个有效数字。只要该值是标准化的,您总是得到5位有效数字。
但你不正常化的时候会失去手指。0.1000E-99和0.9999E-99之间的任何值都只有4位有效数字。0.0100E-99和0.0999E-99之间的任何值都只有3位有效数字。一直到0.0001E-99和0.0009E-99,只剩下一个有效数字。
这会大大降低最终计算结果的精度。更糟糕的是,它以一种高度不可预测的方式这样做,因为这些非常小的非归一化值往往会出现在更复杂的计算中。这当然是需要担心的,当最终结果只剩下1个有效数字时,您无法再真正信任它。
浮点处理器有办法让你知道这一点,或者绕过这个问题。例如,当一个值变得非标准化时,它们可以生成一个中断或信号,让您中断计算。它们有一个“flush-to-zero”选项,状态字中的一个位告诉处理器自动将所有非正常值转换为零。这往往会产生无穷大,一个结果,告诉你,结果是垃圾,应该被丢弃。
zlwx9yxi3#
IEEE 754基础
首先让我们回顾一下IEEE 754数字的基本知识。
让我们首先关注单精度(32位)。
格式为:
如果你喜欢图片:
Source。
标志很简单:0是正的,1是负的,故事结束。
指数是8位长,因此它的范围从0到255。
指数被称为偏置,因为它具有
-127
的偏移,例如:前导位约定
在设计IEEE 754时,工程师们注意到,除了
0.0
之外,所有数字的第一位都是二进制的11
例如:
都是从那个烦人的
1.
部分开始的。因此,让该数字占据几乎每个单个数字的精度位将是浪费的。
为此,他们创建了“前导位约定”:
总是假设数字以1开头
那么如何处理
0.0
呢?他们决定创建一个例外:0.0
因此字节
00 00 00 00
也表示0.0
,这看起来不错。如果我们只考虑这些规则,那么可以表示的最小非零数将是:
由于前导位约定,十六进制小数看起来像这样:
其中
.000002
是22个零,结尾是1
。我们不能取
fraction = 0
,否则这个数将是0.0
。但后来同样有着敏锐艺术感的工程师们想:是不是很难看?我们从直线
0.0
跳到甚至不是2的正确幂的东西?难道我们不能以某种方式代表更小的数字吗?非正规数
工程师们挠了一会儿头,然后回来了,像往常一样,带着另一个好主意。如果我们创建一个新规则:
如果指数为0,则:
这样的数被称为次正规数(或反正规数,这是同义词)。
这条规则直接意味着该数字使得:
是
0.0
,这是一种优雅,因为它意味着少了一个需要跟踪的规则。所以根据我们的定义,
0.0
实际上是一个次正规数!有了这个新规则,最小的非次正态数是:
其表示:
那么,最大的次正态数是:
这等于:
其中
.FFFFFE
再次是点右边的23位。这是非常接近最小的非低于正常的数字,这听起来很正常。
最小的非零次正规数是:
这等于:
它看起来也非常接近
0.0
!由于找不到任何合理的方法来表示小于这个数字的数字,工程师们很高兴,于是又回到了网上查看猫的照片,或者他们在70年代做的任何事情。
正如您所看到的,次正态数在精度和表示长度之间进行了权衡。
作为最极端的例子,最小的非零次法线:
基本上具有单个比特而不是32比特的精度。例如,如果我们将其除以2:
我们实际上正好到达
0.0
!可运行的C示例
现在让我们用一些实际的代码来验证我们的理论。
在几乎所有当前和桌面计算机中,C
float
表示单精度IEEE 754浮点数。我的Ubuntu 18.04 amd 64笔记本电脑尤其如此。
在这个假设下,所有Assert都传递给以下程序:
subnormal.c
GitHub upstream。
编译并运行:
可视化
对我们所学的东西有一个几何直觉总是一个好主意,所以这里开始。
如果我们为每个给定的指数在一条线上绘制IEEE 754浮点数,它看起来像这样:
由此我们可以看出,对于每个指数:
*
表示)现在,让我们把它一直降到指数0。
无次法线(假设):
使用次法线:
通过比较这两张图,我们可以看到:
0
的长度加倍,从[2^-127, 2^-126)
到[0, 2^-126)
低于正常范围的浮点数之间的间距与
[0, 2^-126)
相同。[2^-127, 2^-126)
的点数是没有次法线时的一半。这些点的一半将填充范围的另一半。
[0, 2^-127)
有一些具有次法线的点,但没有不具有次法线的点。[2^-128, 2^-127)
的点数是[2^-127, 2^-126)
的一半。这就是我们所说的次法线是大小和精度之间的折衷。
在这个设置中,我们在
0
和2^-127
之间有一个空的间隙,这不是很优雅。然而,区间被很好地填充,并且像任何其他一样包含
2^23
浮点数。实现方式
x86_64直接在硬件上实现IEEE 754,C代码将转换为硬件。
TODO:有没有现代硬件没有次法线的显著例子?
TODO:是否有任何实现允许在运行时控制它?
在某些实现中,次法线似乎不如法线快:Why does changing 0.1f to 0 slow down performance by 10x?
Infinity和NaN
下面是一个简短的可运行示例:C语言中的浮点数数据类型的范围
qxgroojn4#
从IEEE Documentation
如果指数全为0,但分数不为零(否则将被解释为零),则该值是一个非规范化的数,在二进制点之前没有假定的前导1。因此,这表示数字(-1)s × 0.f × 2-126,其中s是符号位,f是分数。对于双精度,非正规化数的形式为(-1)s × 0.f × 2-1022。由此,你可以将零解释为一种特殊类型的非正规化数。