[英]What is a subnormal floating point number?
确定给定的浮点数 arg 是否正常,即既不是零、次正规、无限也不是 NaN。
数字为零、无穷大或 NaN 很清楚它的含义。 但它也说不正常。 什么时候一个数低于正规数?
IEEE 754 基础知识
首先让我们回顾一下 IEEE 754 编号组织的基础知识。
我们将专注于单精度(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.
部分开始。
因此,让该数字几乎每个数字都占用一个精度位将是一种浪费。
出于这个原因,他们创建了“领先的位约定”:
始终假设数字以 1 开头
但是如何处理0.0
呢? 好吧,他们决定创建一个例外:
0.0
这样字节00 00 00 00
也代表0.0
,看起来不错。
如果我们只考虑这些规则,那么可以表示的最小非零数将是:
由于前导位约定,它在十六进制分数中看起来像这样:
1.000002 * 2 ^ (-127)
其中.000002
是 22 个零,最后是1
。
我们不能采用fraction = 0
,否则该数字将为0.0
。
但后来同样具有敏锐审美的工程师们想:那是不是很丑? 我们从直接的0.0
跳到甚至不是 2 的适当幂的东西? 我们不能以某种方式表示更小的数字吗? (好吧,这比“丑陋”更令人担忧:实际上人们的计算结果很差,请参阅下面的“次正规数如何改进计算”)。
次正规数
工程师们挠了挠头,像往常一样带着另一个好主意回来了。 如果我们创建一个新规则会怎样:
如果指数为 0,则:
- 前导位变为 0
- 指数固定为 -126(不是 -127,好像我们没有这个例外)
这样的数字称为次正规数(或同义词的非正规数)。
此规则立即暗示该数字满足以下条件:
仍然是0.0
,这有点优雅,因为它意味着要跟踪的规则少了。
所以根据我们的定义, 0.0
实际上是一个次正规数!
有了这个新规则,最小的非次正规数是:
这代表:
1.0 * 2 ^ (-126)
那么,最大的次正规数是:
这等于:
0.FFFFFE * 2 ^ (-126)
其中.FFFFFE
再次是点右侧 1 的 23 位。
这非常接近最小的非次正规数,这听起来很正常。
最小的非零次正规数是:
这等于:
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
从中我们可以看出:
*
表示)现在,让我们把它一直降低到指数 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)
一半[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;
}
编译并运行:
gcc -ggdb3 -O0 -std=c11 -Wall -Wextra -Wpedantic -Werror -o subnormal.out subnormal.c
./subnormal.out
C++
除了公开所有 C 的 API 之外,C++ 还公开了一些额外的次规范相关功能,这些功能在 C 中的<limits>
并不容易获得,例如:
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“Flush-to-zero”描述了一种可配置模式,其中次法线四舍五入为零以提高性能:
在进行涉及非规范化数字和下溢异常的计算时,可能会降低浮点处理的性能。 在许多算法中,通过用零替换非规范化操作数和中间结果,可以恢复这种性能,而不会显着影响最终结果的准确性。 为实现这种优化,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]
清零模式。 指示浮点实现是否仅提供对 Flush-to-Zero 操作模式的支持。 定义的值是:
0b0000 未实现,或硬件仅支持清零操作模式。
0b0001 硬件支持完全非规范化数字算法。
保留所有其他值。
在 ARMv8-A 中,允许的值为 0b0000 和 0b0001。
这表明当未实现次正规化时,实现只是恢复到清零。
无穷大和 NaN
好奇的? 我写了一些东西:
次正规如何改进计算
TODO:进一步更准确地了解跳跃如何使计算结果更糟/次正规如何改善计算结果。
实际历史
查尔斯·塞弗伦斯( Charles Severance ) 对浮点老人的采访。(1998) 是一个简短的现实世界历史概述,约翰·科尔曼 (John Coleman) 在评论中建议采用对威廉·卡汉( William Kahan ) 的采访形式。
在 IEEE754 标准中,浮点数表示为二进制科学记数法, x = M × 2 e 。 这里M是尾数, e是指数。 在数学上,你总是可以选择指数,使得 1 ≤ M < 2.* 但是,由于在计算机表示中指数只能有一个有限的范围,所以有些数字大于零但小于 1.0 × 2 e分钟这些数字是subnormals或denormals 。
实际上,尾数的存储没有前导 1,因为总是有前导 1,除了次正规数(和零)。 因此解释是,如果指数是非最小的,则有一个隐含的前导 1,如果指数最小,则没有,并且数字是次正规的。
*)更一般地,1≤中号<B对于任何碱基乙科学记数法。
来自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 。 这称为次正规数。 使用次正规数的目的是平滑最小正规数和零之间的差距。
认识到次正规数的表示精度低于正规数是非常重要的。 事实上,他们正在用较小的尺寸换取降低的精度。 因此,使用次正规数的计算将不会具有与正规数计算相同的精度。 因此,对次正规数进行大量计算的应用程序可能值得研究,以查看重新缩放(即,将数字乘以某个比例因子)是否会产生更少的次正规数和更准确的结果。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.