确定给定的浮点数arg是否正常,即是 零,低于正常,无限,也不是NaN。
数字为零,无限或NaN很清楚它意味着什么。但它也说低于正常。什么时候是一个数字次正常?
答案 0 :(得分:59)
在IEEE754标准中,浮点数表示为二进制科学记数法, x = M ×2 e 。这里 M 是尾数, e 是指数。在数学上,你总是可以选择指数,使1≤ M < 2. *但是,由于在计算机表示中指数只能有一个有限范围,有些数字大于零,但小于1.0×2 e min 子> 。这些数字是 subnormals 或 denormals 。
实际上,尾数的存储没有前导1,因为对于次正规数(和零),总是存在前导1,除之外。因此,解释是如果指数是非最小的,则存在隐含的前导1,如果指数是最小的,则没有,并且数字是次正规的。
*)更一般地,1≤ M < B 用于任何base- B 科学记数法。
答案 1 :(得分:25)
来自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 。但是,如果我们放松了前导位的约束 一个,那么我们实际上可以代表相同的较小数字 空间。以十进制为例,我们可以表示0.1 * 10 -100 。这个 被称为次正规数。具有次正规数的目的 是为了平滑最小正常数和零之间的差距。
认识到表示次正规数是非常重要的 精度低于正常数字。事实上,他们正在交易 尺寸越小,精度越低。因此使用的计算 次正规数不会具有相同的精度 计算正常数字。这样的应用程序 对正常数字的重要计算可能是值得的 调查以确定是否重新缩放(即将数字乘以 一些比例因子)会产生更少的次正规,并且更准确 结果
答案 2 :(得分:24)
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
再次位于点的右侧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)
的一半这就是我们所说的次法线是大小和精度之间的折衷。
可运行的C示例
现在让我们玩一些实际的代码来验证我们的理论。
在几乎所有当前和台式机中,C float
表示单精度IEEE 754浮点数。
我的Ubuntu 18.04 amd64 Lenovo P51笔记本电脑尤其如此。
在这种假设下,所有断言都将传递给以下程序:
subnormal.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 ++还公开了一些额外的次规范相关功能,这些功能在<limits>
中的C中不易使用,例如:
denorm_min
:返回类型T的最小正次正规值在C ++中,hole API是每种浮点类型的模板,并且更好。
实施
x86_64和ARMv8直接在C代码转换成的硬件上实现IEEE 754。
在某些实现中,次法线似乎不如法线快:Why does changing 0.1f to 0 slow down performance by 10x?在ARM手册中已有提及,请参见此答案的“ ARMv8详细信息”部分。
ARMv8详细信息
ARM Architecture Reference Manual ARMv8 DDI 0487C.a manual 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
好奇吗?我在以下位置写过一些东西: