什么是次正规浮点数?


80

isnormal()参考页告诉您:

确定给定的浮点数arg是否正常,即既不是零,次正规,无穷也不是NaN。

一个数字为零,无穷大或NaN很清楚这意味着什么。但这也说不合正常。什么时候是次正规的?


2
谷歌的第一个结果表明它只是
反常态

9
然而,现在Google的第二个热门话题(搜索“低于正常浮点数”)就是这个问题本身。
Slipp D. Thompson

请参见以下问题,以深入讨论异常情况及其处理方法:stackoverflow.com/questions/9314534/…–

Answers:


76

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

实际上,尾数存储时不带前导1,因为次正规数(和零),总会有一个前导1 。因此,这种解释是,如果指数不是最小,则隐式前导1,如果指数最小,则隐式前导1,且数字为次正规。

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


你说isnomaltrue如果8位均为零,false否则?
Pacerier,

“存储”或解释?
Pacerier,

@Pacerier:“存储”:存储时不带前导1,例如,为001010,并解释1.001010
Kerrek SB

74

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

前导约定

在设计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
  • 然后数字代表正负 0.0

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

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

  • 指数:0
  • 分数:1

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

1.000002 * 2 ^ (-127)

其中.000002是22个零,1结尾处是a 。

我们不能接受fraction = 0,否则该数字将是0.0

但是,那些对美学也很敏锐的工程师却想:这不是很丑吗?我们从直线跳到0.0甚至不是2的幂次幂的东西吗?我们不能以某种方式代表更小的数字吗?

次正规数

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

如果指数为0,则:

  • 前导位变为0
  • 指数固定为-126(不是-127,就好像我们没有此异常)

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

该规则立即暗示该数字应为:

  • 指数:0
  • 分数:0

仍然是0.0,这有点优雅,因为这意味着要少追踪一条规则。

所以,0.0实际上是根据我们的定义低于正常的数量!

那么,使用此新规则,最小的非次级数为:

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

代表:

1.0 * 2 ^ (-126)

然后,最大的次正规数为:

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

等于:

0.FFFFFE * 2 ^ (-126)

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

这非常接近最小的非次级数,听起来很理智。

最小的非零次正规数为:

  • 指数:0
  • 分数:1

等于:

0.000002 * 2 ^ (-126)

看起来也很接近0.0

工程师们找不到合适的方法来表示小于该数字的数字,他们很高兴,他们又回到网上观看猫的照片,或者说他们在70年代所做的一切。

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

作为最极端的示例,最小的非零子法线:

0.000002 * 2 ^ (-126)

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

0.000002 * 2 ^ (-126) / 2

我们实际上达到0.0了!

可视化

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

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

          +---+-------+---------------+-------------------------------+
exponent  |126|  127  |      128      |              129              |
          +---+-------+---------------+-------------------------------+
          |   |       |               |                               |
          v   v       v               v                               v
          -------------------------------------------------------------
floats    ***** * * * *   *   *   *   *       *       *       *       *
          -------------------------------------------------------------
          ^   ^       ^               ^                               ^
          |   |       |               |                               |
          0.5 1.0     2.0             4.0                             8.0

从中我们可以看到:

  • 对于每个指数,所表示的数字之间没有重叠
  • 对于每个指数,我们具有相同的2 ^ 32数字(此处由4表示*
  • 在每个指数内,点等距分布
  • 较大的指数覆盖较大的范围,但点分布更多

现在,让我们将其降低到指数0。

如果没有子正常,则假设如下所示:

          +---+---+-------+---------------+-------------------------------+
exponent  | ? | 0 |   1   |       2       |               3               |
          +---+---+-------+---------------+-------------------------------+
          |   |   |       |               |                               |
          v   v   v       v               v                               v
          -----------------------------------------------------------------
floats    *    **** * * * *   *   *   *   *       *       *       *       *
          -----------------------------------------------------------------
          ^   ^   ^       ^               ^                               ^
          |   |   |       |               |                               |
          0   |   2^-126  2^-125          2^-124                          2^-123
              |
              2^-127

使用次普通态时,它看起来像这样:

          +-------+-------+---------------+-------------------------------+
exponent  |   0   |   1   |       2       |               3               |
          +-------+-------+---------------+-------------------------------+
          |       |       |               |                               |
          v       v       v               v                               v
          -----------------------------------------------------------------
floats    * * * * * * * * *   *   *   *   *       *       *       *       *
          -----------------------------------------------------------------
          ^   ^   ^       ^               ^                               ^
          |   |   |       |               |                               |
          0   |   2^-126  2^-125          2^-124                          2^-123
              |
              2^-127

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

  • 次正态的指数范围的长度加倍0,从[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)
    • 等等

    这就是我们所说的次法线是大小和精度之间的折衷。

可运行的C示例

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

在几乎所有当前和台式计算机中,Cfloat表示单精度IEEE 754浮点数。

我的Ubuntu 18.04 amd64 Lenovo P51笔记本电脑尤其如此。

在这种假设下,所有断言都将通过以下程序:

次正态

#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;
}

GitHub上游

编译并运行:

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

C ++

除了公开所有C的API外,C ++还公开了一些额外的次规范相关功能,这些功能在C in中不那么可用<limits>,例如:

  • denorm_min:返回类型T的最小正次正规值

在C ++中,整个API都是针对每种浮点类型进行模板化的,并且更好。

实作

x86_64和ARMv8直接在C代码转换为硬件的硬件上实现IEEE 754。

在某些实现中,次法线似乎没有法线快:为什么将0.1f更改为0会使性能降低10倍?在ARM手册中对此进行了提及,请参见此答案的“ ARMv8详细信息”部分。

ARMv8详细信息

《 ARM体系结构参考手册》 ARMv8 DDI 0487C.a手册A1.5.4“刷新至零”描述了一种可配置模式,在该模式中,将子法线舍入为零以提高性能:

在进行涉及非规格化数和下溢异常的计算时,可能会降低浮点处理的性能。在许多算法中,可以通过用0代替非规格化的操作数和中间结果来恢复该性能,而不会显着影响最终结果的准确性。为了进行此优化,ARM浮点实现允许将刷新到零模式用于不同的浮点格式,如下所示:

  • 对于AArch64:

    • 如果为FPCR.FZ==1,则所有指令的所有单精度和双精度输入和输出都使用“齐零”模式。

    • 如果为FPCR.FZ16==1,则将浮点到零模式用于浮点指令的所有半精度输入和输出,除了:-半精度和单精度数字之间的转换。-半精度和双精度数字之间的转换。数字。

A1.5.2“浮点标准和术语”表A1-3“浮点术语”确认次正规和非正规是同义词:

This manual                 IEEE 754-2008
-------------------------   -------------
[...]
Denormal, or denormalized   Subnormal

C5.2.7“ FPCR,浮点控制寄存器”描述了每当浮点操作的输入不正常时,ARMv8如何可选地引发异常或设置标志位:

FPCR.IDE,位[15]输入反常浮点异常陷阱使能。可能的值为:

  • 0b0选择了未捕获的异常处理。如果发生浮点异常,则FPSR.IDC位设置为1。

  • 0b1选择了陷阱异常处理。如果发生浮点异常,则PE不会更新FPSR.IDC位。陷阱处理软件可以决定是否将FPSR.IDC位设置为1。

D12.2.88“ MVFR1_EL1,AArch32媒体和VFP功能寄存器1”表明,非标准支持实际上是完全可选的,并提供了一些检测是否支持的方法:

FPFtZ,位[3:0]

刷新至零模式。指示浮点实现是否仅对“从刷新到零”操作模式提供支持。定义的值为:

  • 0b0000未实现,或硬件仅支持“从零清除”操作模式。

  • 0b0001硬件支持完整的非规格化数字算法。

所有其他值均保留。

在ARMv8-A中,允许的值为0b0000和0b0001。

这表明,当未实现次规范时,实现将恢复为刷新至零。

无限和NaN

好奇?我在以下位置写过一些东西:


1
引用“在设计IEEE 754 ..时”?或以“应该”开头句子
-Pacerier,

@Pacerier我不认为事实可能是错误的:-)还有其他理由吗?这可能是以前知道的,但是我认为可以。
西罗Santilli郝海东冠状病六四事件法轮功

27

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。这称为次正规数。具有次正规数的目的是使最小的正规数与零之间的间隙变平滑。

重要的是要认识到,次正态数表示的精度要低于正态数。实际上,它们为较小的尺寸而降低了精度。因此,使用非正规数的计算将不会具有与基于正规数的计算相同的精度。因此,对次正规数进行大量计算的应用程序可能值得研究,以查看重新缩放(即,将数字乘以某个缩放因子)是否会产生更少的次正规量,并获得更准确的结果。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.