我想对“非正常数据”以及它的内容有一个广泛的看法,因为我认为我唯一正确的事情是与浮点值特别相关的事实程序员的观点,它与CPU的角度来看是一种通用计算方法。
有人可以为我解密这两个字吗?
修改的
请记住我是面向C ++应用程序而只是C ++语言。
答案 0 :(得分:24)
您询问C ++,但浮点值和编码的细节由浮点规范决定,特别是IEEE 754,而不是C ++。 IEEE 754是迄今为止使用最广泛的浮点规范,我将用它来回答。
在IEEE 754中,二进制浮点值由三部分编码:符号位 s (0表示正数,1表示负数),偏差指数 e (表示的指数加上固定的偏移量)和有效数字字段 f (分数部分)。对于正常数字,这些正好代表数字(-1) s •2 e - 偏见 •1 f ,其中1. f 是通过在“1”之后写有效位而形成的二进制数。 (例如,如果有效数字段的十位为0010111011,则表示有效数字1.0010111011 2 ,即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.0010111011 2 和1.0000000000 2 。减去有效数字时,结果为.0010111011 2 。不幸的是,没有办法将其表示为正常数字。因为您已经处于最低指数,所以您不能表示需要说明此结果中第一个1所在位置的较低指数。由于数学结果太小而无法表示,计算机将被迫返回最接近的可表示数字,该数字为零。
这会在浮点系统中创建不受欢迎的属性,您可以a != b
但a-b == 0
。为避免这种情况,使用了次正规数。通过使用次正规数,我们有一个特殊的区间,实际指数不会减少,我们可以执行算术而不会创建太小而无法表示的数字。当编码指数为零时,实际指数与编码指数为1时相同,但有效数的值变为0. f 而不是1. f 。当我们这样做时,a != b
保证a-b
的计算值不为零。
以下是64位IEEE 754二进制浮点编码中的值组合:
Sign Exponent (e) Significand Bits (f) Meaning 0 0 0 +zero 0 0 Non-zero +2-1022•0.f (subnormal) 0 1 to 2046 Anything +2e-1023•1.f (normal) 0 2047 0 +infinity 0 2047 Non-zero but high bit off +, signaling NaN 0 2047 High bit on +, quiet NaN 1 0 0 -zero 1 0 Non-zero -2-1022•0.f (subnormal) 1 1 to 2046 Anything -2e-1023•1.f (normal) 1 2047 0 -infinity 1 2047 Non-zero but high bit off -, signaling NaN 1 2047 High bit on -, quiet NaN
一些注意事项:
+0和-0在数学上相等,但符号保留。精心编写的应用程序可以在某些特殊情况下使用它。
NaN表示“不是数字”。通常,这意味着发生了一些非数学结果或其他错误,应该丢弃计算或以另一种方式重做。通常,使用NaN的操作会产生另一个NaN,从而保留出现问题的信息。例如,3 + NaN
生成NaN。信令NaN旨在引起异常,指示程序出错或允许其他软件(例如,调试器)执行某些特殊动作。在NaN仅是大量数据的一部分并且稍后将单独处理或将被丢弃的情况下,安静的NaN旨在传播到进一步的结果,允许完成大量计算的其余部分。
符号+和 - 用NaNs保留,但没有数学价值。
在正常编程中,您不应该关注浮点编码,除非它告知您有关浮点计算的限制和行为的程度。关于次正规数,你不需要做任何特殊的事情。
不幸的是,有些处理器因为将次正规数更改为零而违反IEEE 754标准,或者在使用次正规数时执行速度非常慢。在为这些处理器编程时,您可能会设法避免使用次正规数。
答案 1 :(得分:6)
要了解偏离正常的浮点值,首先必须了解正常值。浮点值具有尾数和指数。在十进制值中,如1.2345E6,1.3345是尾数,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或1E-103,这是您现在可以存储的绝对最小数字。
这通常是可取的,它扩展了您可以存储的值的范围。这在实际计算中往往很重要,非常小的数字在差异分析等现实问题中非常常见。
然而,它也存在一个很大的问题,你会因为非规范化数字而失去准确性。浮点计算的准确性受限于您可以存储的位数。我使用伪十进制处理器作为示例是直观的,它只能用5位有效数字计算。只要该值被标准化,您总是得到5位有效数字。
但是当你去标准化时你会丢失数字。 0.1000E-99和0.9999E-99之间的任何值只有4位有效数字。 0.0100E-99和0.0999E-99之间的任何值只有3位有效数字。一直到0.0001E-99和0.0009E-99,只剩下一个有效数字。
这会大大降低最终计算结果的准确性。更糟糕的是,它以高度不可预测的方式这样做,因为这些非常小的非标准化值往往会出现在更复杂的计算中。这当然值得担心,当它只剩下1位有效数字时,你不能再真正相信最终结果了。
浮点处理器有办法让您了解此问题或以其他方式解决问题。例如,当值变为非标准化时,它们可以生成中断或信号,让您中断计算。并且它们具有“刷新到零”选项,状态字中的一个位告诉处理器自动将所有非正常值转换为零。这往往会产生无穷大,这个结果告诉你结果是垃圾,应该被丢弃。
答案 2 :(得分:3)
如果指数全为0,但分数不为零(否则它会 被解释为零),那么该值是非规范化数字, 在二进制点之前没有假设的前导1。 因此,这表示数字(-1)s×0.f×2-126,其中s是 符号位和f是分数。对于双精度,非规范化 数字的形式为(-1)s×0.f×2-1022。你可以这样做 将零解释为一种特殊类型的非规范化数字。
答案 3 :(得分:0)
IEEE 754基础
首先让我们回顾一下IEEE 754数字的基本构成。
让我们首先关注单精度(32位)。
格式为:
或者,如果您喜欢图片:
标志很简单:故事结束时,0为正,而1为负。
指数是8位长,因此它的范围是0到255。
由于指数的偏移量为-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
作为第一位数字
例如:
25.0 == (binary) 11001 == 1.1001 * 2^4
0.625 == (binary) 0.101 == 1.01 * 2^-1
两者都从烦人的1.
部分开始。
因此,让该数字几乎在每个数字上都占据精度位会很浪费。
因此,他们创建了“前导约定”:
始终假定数字以1开头
但是接下来如何处理0.0
?好吧,他们决定创建一个例外:
0.0
这样字节00 00 00 00
也代表0.0
,看起来不错。
如果我们仅考虑这些规则,那么可以表示的最小非零数将是:
由于前导位约定,它看起来像是十六进制分数:
1.000002 * 2 ^ (-127)
其中.000002
是22个零,结尾是1
。
我们不能取fraction = 0
,否则该数字将为0.0
。
但是然后,那些对艺术也很敏锐的工程师想到:这不是很丑吗?我们从直线0.0
跳到了2甚至不是幂次的东西吗?我们不能以某种方式代表更小的数字吗?
非正常数字
工程师们挠了一下头,然后像往常一样带着另一个好主意回来了。如果我们创建新规则怎么办:
如果指数为0,则:
- 首位变为0
- 指数固定为-126(不是-127,就好像我们没有这个例外)
这类数字称为次正规数(或称为同义词的非正规数)。
此规则立即暗示该数字应为:
是0.0
,这很优雅,因为它意味着要跟踪的规则要少一些。
根据我们的定义,0.0
实际上是一个次正规的数字!
那么,使用此新规则,最小的非次级数为:
代表:
1.0 * 2 ^ (-126)
然后,最大的次正规数是:
等于:
0.FFFFFE * 2 ^ (-126)
.FFFFFE
再次位于点的右侧23位。
这非常接近最小的非次级数,听起来很理智。
最小的非零次正规数为:
等于:
0.000002 * 2 ^ (-126)
看起来也很接近0.0
!
无法找到任何一种合理的方式来表示小于该数字的数字,工程师们很高兴,他们又回到了网上观看猫的照片或他们在70年代所做的任何事情。
如您所见,次正规数在精度和表示长度之间进行权衡。
作为最极端的示例,最小的非零子法线:
0.000002 * 2 ^ (-126)
本质上具有一位而不是32位的精度。例如,如果我们将其除以二:
0.000002 * 2 ^ (-126) / 2
我们实际上准确地到达了0.0
!
可运行的C示例
现在让我们玩一些实际的代码来验证我们的理论。
在几乎所有当前和台式机中,C float
表示单精度IEEE 754浮点数。
我的Ubuntu 18.04 amd64笔记本电脑尤其如此。
在这种假设下,所有断言都将传递给以下程序:
subnormal.c
#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;
}
编译并运行:
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)
相同。
范围[2^-127, 2^-126)
的点数是不具有法线的点的一半。
这些点的一半将填补范围的另一半。
范围[0, 2^-127)
的某些点具有次法线,但没有一点。
范围[2^-128, 2^-127)
的得分是[2^-127, 2^-126)
的一半。
这就是我们所说的次法线是大小和精度之间的折衷。
在这种设置下,0
和2^-127
之间将有一个空白,这不是很优雅。
但是该间隔填充得很好,并且包含2^23
个浮点数。
实施
x86_64直接在C代码转换为硬件的硬件上实现IEEE 754。
TODO:没有次规范的现代硬件的显着例子吗?
TODO:任何实现都允许在运行时对其进行控制吗?
在某些实现中,次法线似乎不如法线快:Why does changing 0.1f to 0 slow down performance by 10x?
无穷大和NaN
这是一个简短的示例:Ranges of floating point datatype in C?