什么是"非正常数据"? - C++

use*_*534 18 c++ hardware floating-point cpu

我想对"非正常数据"以及它的内容有一个广泛的看法,因为我认为我唯一正确的事情是从程序员的角度来看与浮点值特别相关的事实,它与一般事项有关 -从CPU角度看计算方法.

有人可以为我解密这2个字吗?

编辑

请记住,我是面向C++应用程序而且只面向C++语言.

Eri*_*hil 26

您询问C++,但浮点值和编码的细节由浮点规范决定,特别是IEEE 754,而不是C++.IEEE 754是迄今为止使用最广泛的浮点规范,我将使用它来回答.

在IEEE 754中,二进制浮点值由三部分编码:符号位s(0表示正数,1表示负数),偏置指数e(表示的指数加固定偏移量)和有效数字字段f(分数部分).对于正常数字,这些正好代表数字(-1)s •2 e - bias1.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二进制浮点编码中的值组合:

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标准,或者在使用次正规数时执行速度非常慢.为这些处理器编程时,您可能会设法避免使用次正规数.


Han*_*ant 7

要了解偏离正常的浮点值,首先必须了解正常浮点值.浮点值具有尾数和指数.在十进制值中,如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位有效数字时,你不能再真正相信最终结果了.

浮点处理器有办法让你知道这个或以其他方式解决问题.例如,当值变为非标准化时,它们可以生成中断或信号,从而中断计算.并且它们具有"刷新到零"选项,状态字中的一个位告诉处理器自动将所有非正常值转换为零.这往往会产生无穷大,结果告诉你结果是垃圾,应该被丢弃.


Cir*_*四事件 7

IEEE 754 基础知识

首先让我们回顾一下 IEEE 754 编号组织的基础知识。

让我们首先关注单精度(32 位)。

格式为:

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

或者如果你喜欢图片:

在此处输入图片说明

来源

符号很简单: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
Run Code Online (Sandbox Code Playgroud)

前导位约定

在设计 IEEE 754 时,工程师注意到所有数字,除了0.0,都有一个1二进制的 1 作为第一位数字

例如:

25.0   == (binary) 11001 == 1.1001 * 2^4
 0.625 == (binary) 0.101 == 1.01   * 2^-1
Run Code Online (Sandbox Code Playgroud)

两者都从那个烦人的1.部分开始。

因此,让该数字几乎每个数字都占用精度位将是一种浪费。

出于这个原因,他们创建了“领先的位约定”:

始终假设数字以 1 开头

但是接下来怎么处理0.0呢?好吧,他们决定创建一个例外:

  • 如果指数为 0
  • 分数是0
  • 那么数字代表加号或减号 0.0

这样字节00 00 00 00也代表0.0,看起来不错。

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

  • 指数:0
  • 分数:1

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

1.000002 * 2 ^ (-127)
Run Code Online (Sandbox Code Playgroud)

其中.000002是 22 个零,1最后是 a 。

我们不能接受fraction = 0,否则那个数字就是0.0

但后来同样具有敏锐艺术感的工程师们想:那是不是很丑?我们从直接跳到0.0甚至不是 2 的适当幂的东西?我们不能以某种方式表示更小的数字吗?

非正规数

工程师们挠了挠头,像往常一样带着另一个好主意回来了。如果我们创建一个新规则会怎样:

如果指数为 0,则:

  • 前导位变为 0
  • 指数固定为 -126(不是 -127,好像我们没有这个例外)

这样的数称为次正规数(或同义的非正规数)。

此规则立即暗示该数字满足以下条件:

  • 指数:0
  • 分数:0

is 0.0,这有点优雅,因为它意味着要跟踪的规则少了。

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

有了这个新规则,最小的非次正规数是:

  • 指数:1(0 将低于正常)
  • 分数:0

这代表:

1.0 * 2 ^ (-126)
Run Code Online (Sandbox Code Playgroud)

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

  • 指数:0
  • 分数:0x7FFFFF(23 位 1)

这等于:

0.FFFFFE * 2 ^ (-126)
Run Code Online (Sandbox Code Playgroud)

这里.FFFFFE再次为23位一个点的权利。

这非常接近最小的非次正规数,这听起来很正常。

最小的非零次正规数是:

  • 指数:0
  • 分数:1

这等于:

0.000002 * 2 ^ (-126)
Run Code Online (Sandbox Code Playgroud)

这看起来也很接近0.0

无法找到任何合理的方式来表示比这更小的数字,工程师们很高兴,并回到在线查看猫图片,或者他们在 70 年代所做的任何事情。

如您所见,次正规数在精度和表示长度之间进行了权衡。

作为最极端的例子,最小的非零次正规:

0.000002 * 2 ^ (-126)
Run Code Online (Sandbox Code Playgroud)

本质上具有单个位而不是 32 位的精度。例如,如果我们将其除以二:

0.000002 * 2 ^ (-126) / 2
Run Code Online (Sandbox Code Playgroud)

我们实际上达到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;
}
Run Code Online (Sandbox Code Playgroud)

GitHub 上游.

编译并运行:

gcc -ggdb3 -O0 -std=c11 -Wall -Wextra -Wpedantic -Werror -o subnormal.out subnormal.c
./subnormal.out
Run Code Online (Sandbox Code Playgroud)

可视化

对我们学到的东西有几何直觉总是一个好主意,所以就这样吧。

如果我们为每个给定的指数在一条线上绘制 IEEE 754 浮点数,它看起来像这样:

          +---+-------+---------------+
exponent  |126|  127  |      128      |
          +---+-------+---------------+
          |   |       |               |
          v   v       v               v
          -----------------------------
floats    ***** * * * *   *   *   *   *
          -----------------------------
          ^   ^       ^               ^
          |   |       |               |
          0.5 1.0     2.0             4.0
Run Code Online (Sandbox Code Playgroud)

从中我们可以看出,对于每个指数:

  • 表示的数字之间没有重叠
  • 对于每个指数,我们有相同的数字 2^32 个数字(这里用 4 表示*
  • 对于给定的指数,点的间距相等
  • 更大的指数覆盖更大的范围,但点更分散

现在,让我们把它一直降低到指数 0。

没有次正规(假设):

          +---+---+-------+---------------+
exponent  | ? | 0 |   1   |       2       |
          +---+---+-------+---------------+
          |   |   |       |               |
          v   v   v       v               v
          ---------------------------------
floats    *   ***** * * * *   *   *   *   *
          ---------------------------------
          ^   ^   ^       ^               ^
          |   |   |       |               |
          0   |   2^-126  2^-125          2^-124
              |
              2^-127
Run Code Online (Sandbox Code Playgroud)

与次正规:

          +-------+-------+---------------+
exponent  |   0   |   1   |       2       |
          +-------+-------+---------------+
          |       |       |               |
          v       v       v               v
          ---------------------------------
floats    * * * * * * * * *   *   *   *   *
          ---------------------------------
          ^   ^   ^       ^               ^
          |   |   |       |               |
          0   |   2^-126  2^-125          2^-124
              |
              2^-127
Run Code Online (Sandbox Code Playgroud)

通过比较两张图,我们看到:

  • 次法线是指数范围长度的两倍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 中浮点数据类型的范围?