什么是次正规浮点数?

新手上路,请多包涵

isnormal() 参考页 说:

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

很清楚数字为零、无限或 NaN 的含义。但它也说不正常。什么时候数字是次正态的?

原文由 BЈовић 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.5k
2 个回答

在 IEEE754 标准中,浮点数表示为二进制科学记数法 x = M × 2 e 。这里 M 是 _尾数_, e 是 _指数_。在数学上,您始终可以选择指数,使得 1 ≤ M < 2。* 但是,由于在计算机表示中,指数只能有一个有限范围,所以有些数字大于零,但小于 1.0 × 2 e 分钟。这些数字是 subnormalsdenormals

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

*) 更一般地,对于任何以 B 为底的科学记数法,1 ≤ M < B。

原文由 Kerrek SB 发布,翻译遵循 CC BY-SA 3.0 许可协议

IEEE 754 基础知识

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

我们将专注于单精度(32 位),但一切都可以立即推广到其他精度。

格式为:

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

或者如果你喜欢图片:

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
  • 那么数字代表正负 0.0

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

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

  • 指数:0
  • 分数:1

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

 1.000002 * 2 ^ (-127)

其中 .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)

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

  • 指数: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^23 个浮点数(这里用 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 示例

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

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

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

有了这个假设,所有断言都通过以下程序:

不正常的.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;
}

GitHub 上游.

编译并运行:

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

C++

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

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

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

实现

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

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

ARMv8 详细信息

ARM 体系结构参考手册 ARMv8 DDI 0487C.a 手册 A1.5.4“清零”描述了一种可配置模式,其中次规范被舍入为零以提高性能:

在进行涉及非规范化数字和下溢异常的计算时,浮点处理的性能可能会降低。在许多算法中,可以通过用零替换非规范化操作数和中间结果来恢复这种性能,而不会显着影响最终结果的准确性。为了允许这种优化,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 Media and VFP Feature Register 1” 表明非规范化支持实际上是完全可选的,并提供了一点来检测是否有支持:

FPFtZ,位 [3:0]

清零模式。指示浮点实现是否仅支持清零操作模式。定义的值是:

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

  • 0b0001 硬件支持完全非规范化数字运算。

保留所有其他值。

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

这表明当未实现次正规时,实现只是恢复为清零。

无穷大和 NaN

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

次正规如何改进计算

根据 Oracle(原 Sun) 数值计算指南

[S] 非正规数消除了下溢作为各种计算(通常是乘法后加法)的关注原因。 … 在逐渐下溢的情况下成功但在 Store 0 中失败的问题类别比 Store 0 的粉丝可能意识到的要大。 …在没有逐渐下溢的情况下,用户程序需要对隐含的不准确阈值敏感。比如在单精度下,如果计算的某些部分出现下溢,并且使用Store 0将下溢的结果替换为0,那么精度只能保证到10-31左右,而不是10-38,通常的较低范围对于单精度指数。

《数值计算指南》向读者介绍了另外两篇论文:

感谢 Willis Blackburn 为本部分的答案做出了贡献。

实际历史

Charles Severance (1998 年) 对浮点老人 的采访是对 William Kahan 的采访形式的简短现实世界历史概述,由 John Coleman 在评论中提出。

原文由 Ciro Santilli OurBigBook.com 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题