什么是次正规浮点数?

BЈо*_*вић 65 c++ floating-point ieee-754 c++11

isnormal()参考页面告诉:

确定给定的浮点数arg是否正常,即既不是零,也不是正常,无限,也不是NaN.

数字为零,无限或NaN很清楚它意味着什么.但它也说低于正常.什么时候是一个数字次正常?

Ker*_* SB 64

在IEEE754标准中,浮点数表示为二进制科学符号,x  =  M  × 2e.这里M尾数,e指数.在数学上,可以随时选择指数,使得1≤  中号  <2*然而,由于在计算机中表示的指数只能具有有限的范围内,有一些数字它们是大于零,但小于1.0×2 ë 分钟.这些数字是次正规或非正规数.

实际上,尾数存储时没有前导1,因为除了正常数(和零)之外总是有前导1 .因此,解释是如果指数是非最小的,则存在隐含的前导1,并且如果指数是最小的,则不存在,并且该数字是次正规的.

*)更一般地,1≤  中号  <    任何碱基科学记数法.


Cir*_*四事件 40

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二进制数字作为第一个数字

例如:

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

两者都从那个讨厌的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的最后一个.

我们不能采取fraction = 0,否则这个数字将是0.0.

但是那些同样具有敏锐艺术感的工程师认为:那不是那么难看吗?我们从直接跳到0.0一个甚至不是2的适当力量的东西?我们难道不能以某种方式代表更小的数字吗?

次正规数

工程师们摸了一会儿,像往常一样,带着另一个好主意回来了.如果我们创建新规则怎么办:

如果指数为0,则:

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

这些数字称为次正规数(或同义词的非正规数).

这条规则立即暗示了这样的数字:

  • 指数:0
  • 分数:0

是的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位的精度.例如,如果我们将它除以2:

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

我们确实达到0.0了!

Runnable C的例子

现在让我们玩一些实际的代码来验证我们的理论.

在几乎所有当前和台式机中,C *代表单精度IEEE 754浮点数.

对于我的Ubuntu 18.04 amd64联想P51笔记本电脑来说尤其如此.

根据该假设,所有断言都传递以下程序:

subnormal.c

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

GitHub上游.

编译并运行:

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

可视化

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

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

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

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

  • 对于每个指数,所表示的数字之间没有重叠
  • 对于每个指数,我们有相同的数字2 ^ 32的数字(这里用4表示0)
  • 对于给定的指数,点是等间隔的
  • 较大的指数涵盖较大的范围,但分数更加分散

现在,让我们把它一直带到指数0.

如果没有次正规,它会假设看起来像:

#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)

使用次正规:

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

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

  • subnormals是指数范围的两倍[2^-127, 2^-126),从.[0, 2^-126)[0, 2^-126)

    低于正常范围的浮动之间的空间与之相同[2^-127, 2^-126).

  • 该范围[0, 2^-127)具有没有次正规的点数的一半.

    这些点中有一半用于填补范围的另一半.

  • 范围[0, 2^-127)有一些具有次正规的点,但没有没有.

    这种缺点[2^-128, 2^-127)并不是很优雅,是次正常存在的主要原因!

  • 由于点间距相等:

    • 范围[2^-127, 2^-126)有一半的分数[2^-129, 2^-128) - [2^-128, 2^-127)比分数的一半float
    • 等等

    这就是我们所说的次正规是尺寸和精度之间的权衡.

实现

x86_64直接在硬件上实现IEEE 754,C代码转换为硬件.

TODO:没有次正规的现代硬件的任何值得注意的例子?

TODO:任何实现都允许在运行时控制它吗?

在某些实现中,次正规似乎比正常情况快:为什么将0.1f改为0会使性能降低10倍?

无限和NaN

这是一个简短的可运行示例:C中浮点数据类型的范围?


all*_*zes 26

来自http://blogs.oracle.com/d/entry/subnormal_numbers:

有可能有多种表示相同数字的方法,使用小数作为示例,数字0.1可以表示为1*10 -1或0.1*10 0甚至0.01*10.标准规定数字始终存储为第一位作为一个.在十进制中,对应于1*10-1示例.

现在假设可以表示的最低指数是-100.因此,可以用正常形式表示的最小数字是1*10 -100.但是,如果我们放宽前导位为1的约束,那么我们实际上可以在同一空间中表示较小的数字.以十进制为例,我们可以表示0.1*10 -100.这称为次正规数.具有次正规数的目的是平滑最小正常数和零之间的差距.

认识到正常数字的精度低于正常数字是非常重要的.事实上,他们以较小的尺寸交易精度较低.因此,使用次正规数的计算与正常数的计算不具有相同的精度.因此,对次正规数进行重要计算的应用程序可能值得研究,以确定重新缩放(即将数字乘以某个缩放因子)将产生更少的次正规,并且更准确的结果.