关于浮点:这个“非常规数据”是什么?

What is this “denormal data” about ? - C++

我想对"非正规数据"有一个更广泛的观点,以及它是关于什么的,因为我认为我唯一正确的是事实,从程序员的角度来看,它与浮点值特别相关,从CPU的角度来看,它与一般的计算方法有关。

有人能帮我解密这两个字吗?

编辑

请记住,我面向C++应用程序,只面向C++语言。


您询问C++,但浮点值和编码的细节是由浮点规范确定的,特别是IEEE 754,而不是C++。IEEE754是目前使用最广泛的浮点规范,我将用它来回答。好的。

在IEEE754中,二进制浮点值由三部分编码:符号位S(0表示正,1表示负)、有偏指数E(表示的指数加固定偏移量)和有效位字段F(小数部分)。对于普通数字,这些正好代表数字(-1)s?2E偏差?1.f,其中1.f是通过在"1"后写入有效位而形成的二进制数字。(例如,如果有效位字段有十个位001011011,则它表示有效位1.0010110112,它是1.182617175或1211/1024。)好的。

偏差取决于浮点格式。对于64位IEEE754二进制,指数字段有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.0010110112和1.00000000002。当您减去有效位时,结果是.00101110112。不幸的是,无法将其表示为正常数字。因为您已经处于最低的指数,所以不能表示表示表示第一个1在这个结果中的位置所需的较低指数。由于数学结果太小,无法表示,计算机将被迫返回最近的可表示数字,即零。好的。

这会在浮点系统中创建不需要的属性,您可以使用a != b,但使用a-b == 0。为了避免这种情况,使用了次正规数。通过使用次正规数,我们有一个特殊的区间,其中实际指数不会减少,我们可以在不创建太小而无法表示的数字的情况下执行算术。当编码指数为零时,实际指数与编码指数为1时相同,但有效值变为0.f而不是1.f。当我们这样做时,a != b保证a-b的计算值不为零。好的。

以下是64位IEEE754二进制浮点编码中的值组合:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
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将传播到进一步的结果中,从而允许完成其余的大型计算。好的。

符号+和-保留为NaN,但没有数学值。好的。

在正常编程中,您不应该关心浮点编码,除非它告诉您浮点计算的限制和行为。对于次正规数,不需要做任何特殊的事情。好的。

不幸的是,有些处理器被破坏了,因为它们要么违反了IEEE754标准,将次正规数更改为零,要么在使用次正规数时运行非常缓慢。在为此类处理器编程时,您可能会设法避免使用次标准数。好的。好啊。


要了解非正常浮点值,首先必须了解正常浮点值。浮点值有尾数和指数。在十进制值中,如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或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文档

If the exponent is all 0s, but the fraction is non-zero (else it would
be interpreted as zero), then the value is a denormalized number,
which does not have an assumed leading 1 before the binary point.
Thus, this represents a number (-1)s × 0.f × 2-126, where s is the
sign bit and f is the fraction. For double precision, denormalized
numbers are of the form (-1)s × 0.f × 2-1022. From this you can
interpret zero as a special type of denormalized number.


IEEE 754基础好的。

首先,让我们回顾一下IEEE754数字的基本结构。好的。

让我们先关注一下单精度(32位)。好的。

格式为:好的。

  • 1位:符号
  • 8位:指数
  • 23位:分数

或者如果你喜欢图片:好的。

enter image description here。好的。

来源。好的。

符号很简单:0为正,1为负,故事结束。好的。

指数是8位长,所以它的范围是0到255。好的。

指数称为有偏指数,因为它的偏移量为-127,例如:好的。

1
2
3
4
5
6
7
8
9
10
11
  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

前导位约定好的。

在设计IEEE754时,工程师们注意到,除了0.0以外,所有的数字都有一个二进制的1作为第一个数字。好的。

例如。:好的。

1
2
25.0   == (binary) 11001 == 1.1001 * 2^4
 0.625 == (binary) 0.101 == 1.01   * 2^-1

两者都是从恼人的1.部分开始的。好的。

因此,让这个数字几乎占据每个数字的精确位是浪费的。好的。

为此,他们创建了"前导位约定":好的。

always assume that the number starts with one

Ok.

但是如何处理0.0呢?嗯,他们决定创建一个例外:好的。

  • 如果指数为0
  • 分数是0
  • 那么这个数字表示正负两个数:0.0

所以字节00 00 00 00也代表0.0,看起来不错。好的。

如果我们只考虑这些规则,那么可以表示的最小非零数字是:好的。

  • 指数:0
  • 分数:1

由于前导位的约定,它在十六进制小数中看起来像这样:好的。

1
1.000002 * 2 ^ (-127)

其中,.000002是22个零,末尾是1。好的。

我们不能取fraction = 0,否则这个数字就是0.0。好的。

但是工程师们也有敏锐的艺术头脑,他们想:那不是很难看吗?我们从直线的0.0跳到一个甚至不是2的适当幂的东西?我们不能代表更小的数字吗?好的。

非正规数好的。

工程师们挠头了一会儿,又像往常一样回来了,想出了另一个好主意。如果我们创建一个新规则:好的。

If the exponent is 0, then:

Ok.

  • the leading bit becomes 0
  • the exponent is fixed to -126 (not -127 as if we didn't have this exception)

Such numbers are called subnormal numbers (or denormal numbers which is synonym).

Ok.

此规则立即暗示该数字如下:好的。

  • 指数:0
  • 分数:0

0.0,它有点优雅,因为它意味着少了一个规则来跟踪。好的。

所以根据我们的定义,0.0实际上是一个次正规数!好的。

根据这个新规则,最小的非次正规数是:好的。

  • 指数:1(0将是次正规)
  • 分数:0

代表:好的。

1
1.0 * 2 ^ (-126)

那么,最大的次正规数是:好的。

  • 指数:0
  • 0x7fffff(23位):1)

这等于:

1
0.FFFFFE * 2 ^ (-126)

在一个23位的.FFFFFE再一次向右的斑点。

这是一个漂亮的特写镜头在smallest非次正规数,同时它的声音。

非零和smallest次数是:

  • 指数:0
  • 分数:1

这等于:

1
0.000002 * 2 ^ (-126)

这也是近0.0看起来漂亮!

无法找到任何合理的方式来代表数的比是小的,和工程师为快乐,猫图片在线观看去回,或任何他们所做的,而不是在70年代。

你可以看到的,次正规数之间的权衡精度和表示的长度。

最极端的例子,在非正规的smallest零:

1
0.000002 * 2 ^ (-126)

蛛网膜下腔出血(SAH)精密单位基本上而不是32位。例如,如果我们将它用二:

1
0.000002 * 2 ^ (-126) / 2

事实上,我们达到0.0是!

C的例子runnable

现在,让我们玩一些现有的代码来验证我们的理论。

在几乎所有的台式机和float电流C,IEEE 754单精密浮点表示的数字。

这是特别的情况下,我的Ubuntu的18.04 AMD64的笔记本电脑。

这样假设,所有的assertions通在线以下的程序:

subnormal.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#if __STDC_VERSION__ < 201112L
#error C11 required
#endif

#ifndef __STDC_IEC_559__
#error IEEE 754 not implemented
#endif

#include
#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"
"
,
        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的上游。

使用:编译和运行

1
2
gcc -ggdb3 -O0 -std=c11 -Wall -Wextra -Wpedantic -Werror -o subnormal.out subnormal.c
./subnormal.out

可视化

这是一个很好的想法,总是要学,我们intuition关于几何,所以这里去。

如果我们的IEEE 754浮点数图上的每个线给出了一些指数,它看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
          +---+-------+---------------+
exponent  |126|  127  |      128      |
          +---+-------+---------------+
          |   |       |               |
          v   v       v               v
          -----------------------------
floats    ***** * * * *   *   *   *   *
          -----------------------------
          ^   ^       ^               ^
          |   |       |               |
          0.5 1.0     2.0             4.0

我们可以看到,从每个指数:

  • 有没有重叠之间的代表数
  • 每个指数的,我们有相同的号码(2 ^ 32数在*代表(4)
  • 同样是在给定的间隔开的点指数
  • exponents覆盖较大的范围较大,但与点扩展出更多的

现在,让我们把这所有的方式下的指数为0。

没有subnormals(hypothetical):

1
2
3
4
5
6
7
8
9
10
11
12
13
          +---+---+-------+---------------+
exponent  | ? | 0 |   1   |       2       |
          +---+---+-------+---------------+
          |   |   |       |               |
          v   v   v       v               v
          ---------------------------------
floats    *   ***** * * * *   *   *   *   *
          ---------------------------------
          ^   ^   ^       ^               ^
          |   |   |       |               |
          0   |   2^-126  2^-125          2^-124
              |
              2^-127

与subnormals:

1
2
3
4
5
6
7
8
9
10
11
12
13
          +-------+-------+---------------+
exponent  |   0   |   1   |       2       |
          +-------+-------+---------------+
          |       |       |               |
          v       v       v               v
          ---------------------------------
floats    * * * * * * * * *   *   *   *   *
          ---------------------------------
          ^   ^   ^       ^               ^
          |   |   |       |               |
          0   |   2^-126  2^-125          2^-124
              |
              2^-127

通过比较在这两个图,我们认为:国有企业

  • subnormals双指数的长度范围从[2^-127, 2^-126)0,到[0, 2^-126)

    空间中的正规的浮标之间的[0, 2^-126)范围是相同的。

  • 蛛网膜下腔出血(SAH)的范围[2^-127, 2^-126)半数,这就有点不subnormals。

    这些点半去填充的另一半的距离。

  • 在一些点的距离与subnormals [0, 2^-127)蛛网膜下腔出血,但没有没有。

  • 蛛网膜下腔出血(SAH)的距离比[2^-127, 2^-126)[2^-128, 2^-127)半分。

    这就是我们说的是,当subnormals均tradeoff之间的尺寸和精度。

在这样的设置,我们将有一个空的2^-1270和之间的差距,这是不是很优雅。

不过,这个间隔填充得很好,并且像其他间隔一样包含2^23浮点数。好的。

启动位置好的。

x86_64直接在硬件上实现IEEE754,C代码将其转换为。好的。

托多:有没有没有没有亚标准的现代硬件的显著例子?好的。

TODO:是否有任何实现允许在运行时控制它?好的。

在某些实现中,子规范似乎不如规范快:为什么将0.1f更改为0会将性能降低10倍?好的。

无穷大和NaN好的。

下面是一个可运行的简短示例:C中浮点数据类型的范围?好的。好啊。