我想对“非规范数据”及其含义有一个广泛的看法,因为我认为我唯一正确的是,从程序员的角度来看,它与浮点值特别相关,并且与一般情况有关-从 CPU 角度计算的方法。
有人可以帮我解密这两个字吗?
编辑
请记住,我面向 C++ 应用程序并且只面向 C++ 语言。
我想对“非规范数据”及其含义有一个广泛的看法,因为我认为我唯一正确的是,从程序员的角度来看,它与浮点值特别相关,并且与一般情况有关-从 CPU 角度计算的方法。
有人可以帮我解密这两个字吗?
编辑
请记住,我面向 C++ 应用程序并且只面向 C++ 语言。
您询问 C++,但浮点值和编码的细节是由浮点规范决定的,特别是 IEEE 754,而不是 C++。IEEE 754 是迄今为止使用最广泛的浮点规范,我将使用它来回答。
在 IEEE 754 中,二进制浮点值由三部分编码:符号位s(0 表示正,1 表示负)、偏置指数e(表示的指数加上固定偏移量)和有效位字段f(分数部分)。对于普通数,这些精确地表示数字 (-1) s • 2 e -<i>bias • 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
. 为了避免这种情况,使用了次正规数。通过使用次正规数,我们有一个实际指数不减少的特殊区间,我们可以执行算术而不创建太小而无法表示的数字。当编码指数为零时,实际指数与编码指数为一时相同,但有效数字的值变为 0. f而不是 1. f。当我们这样做时,a != b
保证计算的值a-b
不为零。
以下是 64 位 IEEE 754 二进制浮点编码中的值组合:
符号 | 指数 ( e ) | 有效位 ( f ) | 意义 |
---|---|---|---|
0 | 0 | 0 | +零 |
0 | 0 | 非零 | +2 -1022 •0。f(次正常) |
0 | 1 至 2046 | 任何事物 | +2 e −1023 •1。f(正常) |
0 | 2047 | 0 | +无穷大 |
0 | 2047 | 非零但高位关闭 | +,信号 NaN |
0 | 2047 | 高位开启 | +, 安静的 NaN |
1 | 0 | 0 | -零 |
1 | 0 | 非零 | -2 -1022 •0。f(次正常) |
1 | 1 至 2046 | 任何事物 | -2 e -1023 •1。f(正常) |
1 | 2047 | 0 | −无穷大 |
1 | 2047 | 非零但高位关闭 | −, 信号 NaN |
1 | 2047 | 高位开启 | −, 安静的 NaN |
一些注意事项:
+0 和 -0 在数学上相等,但符号被保留。精心编写的应用程序可以在某些特殊情况下使用它。
NaN 的意思是“不是数字”。通常,这意味着发生了一些非数学结果或其他错误,应该放弃计算或以其他方式重做。通常,使用 NaN 的操作会产生另一个 NaN,从而保留出现问题的信息。例如,3 + NaN
产生一个 NaN。信号 NaN 旨在引起异常,或者指示程序出错或允许其他软件(例如,调试器)执行某些特殊操作。安静的 NaN 旨在传播到进一步的结果,允许完成大型计算的其余部分,在 NaN 只是大量数据的一部分并且稍后将单独处理或将被丢弃的情况下。
符号 + 和 - 与 NaN 一起保留,但没有数学价值。
在正常编程中,您不应该关心浮点编码,除非它告诉您浮点计算的限制和行为。对于次正规数,您不需要做任何特别的事情。
不幸的是,一些处理器被破坏,因为它们要么违反 IEEE 754 标准,将次正规数更改为零,要么在使用次正规数时执行速度非常慢。在为此类处理器编程时,您可能会尽量避免使用次正规数。
要了解非正常浮点值,您首先必须了解正常浮点值。浮点值具有尾数和指数。在十进制值中,如 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 处达到最大值。
嗯,你可以。您可以放弃规范化表示并忘记隐含的数字优化。您可以将其存储为de-normalized。现在您可以存储 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 个有效数字时,你不能再真正相信最终结果了。
浮点处理器有办法让您知道这一点,或者以其他方式绕过问题。例如,它们可以在值变得非规范化时生成中断或信号,让您中断计算。他们有一个“清零”选项,状态字中的一个位告诉处理器自动将所有非正常值转换为零。这往往会产生无穷大,一个结果告诉你结果是垃圾,应该被丢弃。
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.
部分开始。
因此,让该数字几乎占据每个数字的精度位是浪费的。
出于这个原因,他们创建了“领先位约定”:
总是假设数字以一开头
但是接下来怎么处理0.0
呢?好吧,他们决定创建一个例外:
0.0
这样字节00 00 00 00
也代表0.0
,看起来不错。
如果我们只考虑这些规则,那么可以表示的最小非零数将是:
由于前导位约定,它在十六进制分数中看起来像这样:
1.000002 * 2 ^ (-127)
其中.000002
是 22 个零,1
最后是 a。
我们不能拿fraction = 0
,否则那个数字就是0.0
。
但随后同样具有敏锐艺术感的工程师们想:这不丑吗?我们从直接跳到0.0
甚至不是 2 的正确幂的东西?我们不能以某种方式代表更小的数字吗?
非正规数
工程师们挠了挠头,然后像往常一样,带着另一个好主意回来了。如果我们创建一个新规则会怎样:
如果指数为 0,则:
- 前导位变为 0
- 指数固定为-126(不是-127,好像我们没有这个例外)
这样的数字称为次正规数(或同义的非正规数)。
这条规则立即暗示了这样的数字:
is 0.0
,这有点优雅,因为它意味着要跟踪的规则更少。
所以0.0
根据我们的定义实际上是一个次正规数!
有了这个新规则,最小的非次正规数是:
这代表:
1.0 * 2 ^ (-126)
那么,最大的次正规数是:
这等于:
0.FFFFFE * 2 ^ (-126)
where.FFFFFE
又是点右侧的 23 位。
这非常接近最小的非次正规数,这听起来很正常。
最小的非零次正规数是:
这等于:
0.000002 * 2 ^ (-126)
这看起来也很接近0.0
!
由于找不到任何合理的方法来表示小于该数字的数字,工程师们很高兴,于是又回到了网上查看猫的图片,或者他们在 70 年代所做的任何事情。
如您所见,次正规数在精度和表示长度之间进行权衡。
作为最极端的例子,最小的非零次正规:
0.000002 * 2 ^ (-126)
本质上具有一位而不是 32 位的精度。例如,如果我们将其除以二:
0.000002 * 2 ^ (-126) / 2
我们实际上达到0.0
了!
可运行的 C 示例
现在让我们使用一些实际代码来验证我们的理论。
在几乎所有当前和台式机中,Cfloat
表示单精度 IEEE 754 浮点数。
我的 Ubuntu 18.04 amd64 笔记本电脑尤其如此。
有了这个假设,所有断言都通过以下程序:
不正常的.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
通过比较两张图,我们可以看到:
subnormals 将指数范围的长度加倍0
,从[2^-127, 2^-126)
到[0, 2^-126)
低于正常范围的浮点数之间的空间与 for 相同[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:是否有任何实现允许在运行时控制它?
在某些实现中,次法线似乎不如法线快:为什么将 0.1f 更改为 0 会使性能降低 10 倍?
无穷大和 NaN
这是一个简短的可运行示例:C 中浮点数据类型的范围?
来自IEEE 文档
如果指数全为 0,但分数非零(否则将被解释为零),则该值是非规范化数字,在二进制点之前没有假定的前导 1。因此,这表示一个数字 (-1)s × 0.f × 2-126,其中 s 是符号位,f 是小数。对于双精度,非规格化数的形式为 (-1)s × 0.f × 2-1022。由此,您可以将零解释为一种特殊类型的非规范化数字。