为什么浮点数的整数表示形式提供对数的分段线性近似?

Why does the integer representation of a floating point number offer a piecewise linear approximation to the logarithm?

提问人:Adam Hyland 提问时间:3/18/2023 最后编辑:Adam Hyland 更新时间:3/24/2023 访问量:573

问:

如果你正在阅读有关1990年代图形学发展的新闻,你可能会关注Jim Blinn在IEEE计算机图形学与应用上的专栏,“Jim Blinn的角落”。在 1997 年夏天,你会打开一个名为“浮点技巧”的专栏。所有技巧都基于布林从史蒂夫·加布里埃尔和吉迪恩·尤瓦尔(Steve Gabriel)和吉迪恩·尤瓦尔(Gideon Yuval)那里传授的以下知识,他们是微软的两位老手:

"如果只处理正数,则浮点数的位模式(解释为整数)会为对数函数提供分段线性近似"

我的问题是,浮点数是什么导致了这种特征?

示范


// These are will result in undefined behavior
// and are not the 'right' way to do this
// but I am following Blinn 1997 here
int AsInteger(float f) {
    return * ( int * ) &f;
}

float AsFloat(int i) {
    return * ( float * ) &i;
}

// FISR demonstrating the logaritmic nature of the floating-point numbers
// The constant chosen is not "magic" but that which replaces the lost
// exponents from the shift of the number as an integer.
float BlinnISR(float x) {
    int i;
    float y;
    // 0x5F400000 = 1598029824
    // which is (AsInteger(1.0f) + (AsInteger(1.0f) >> 1))
    i = 0x5F400000 - ( AsInteger(x) >> 1);
    y = AsFloat(i);
    // Blinn uses 1.47 and 0.47 not 1.5 and 0.5 
    // which are what you would get from differentiating
    y = y * (1.47f - 0.47f * x * y * y);
    return y;
}

上面是 Blinn 示例代码的再现,展示了它如何实际工作于平方根反比。

请记住,1 / sqrt(x) 与 x(-1/2) 相同。因此,如果我们可以访问对数表,找到平方根的一种方法是取 x(-1/2) 的自然对数,它得到 -1/2 * ln(x)。如果将其求幂,则得到 1 / sqrt(x) = e((-1/2) * ln(x))。

向右移动浮点数的整数表示形式,可以近似地除以输入浮点数的对数的 2(这是我们的 1/2 * ln(x))。这是从恢复常数中减去的,这里只是 ,还不是“魔术”,所以我们基本上有 -1/2 * ln(x)。将其返回到浮点数作为我们的幂步骤。然后,我们有一个数字,可以作为牛顿方法的良好第一个输入。0x5F400000

它看起来像什么

从视觉上看,你可以在这里看到这种关系(Blinn 1997,第81页):

relationship of integer and floating point logarithm

Mitchell 1961 年(第 513 页,链接如下)中出现了类似的理想化图表:

logarithm and straight line approximation

Jean-Michel Muller,“Elementary Functions and Approximate Computing”,载于 Proceedings of the IEEE,第 108 卷,第 12 期,第 2137 页,2020 年 12 月,展示了同样的关系

logarithm and straight line approximation from Muller

适用情况

此特性并非 IEEE 754 浮点数所独有,尽管这是 Quake III Arena 使用标准 .但是,它将以抽象格式(如 Knuth 的教学格式)或其他格式(包括以下示例)工作。floatMIX

据我所知,这种关系最早记录在学术出版社中,约翰·米切尔(John Mitchell)在1961年出版的《使用二进制对数的计算机乘法和除法》(Computer Multiplication and Division Using Binary Logarithms)一书中用抽象的二进制对数系统演示了该方法。William Kahan 在 1961 年或 1962 年左右在 IBM 7090 上实施了类似的方案。我们也知道,部分由于 Noah Hellman 的硕士论文 Mitchell-Based Approximate Operations on Floating-Point Numbers,从 1974 年到 1980 年代中期为 BSD 4.3 创建 libm 之前,它也在 UNIX 中作为系统平方根实现。大约在同一时间,Kahan 和他的研究生 K-C Ng 使用这种方法制作了一个“魔术”平方根,但没有发布,该方法旨在不对它进行操作,但该方法将被减半并作为 32 位部分进行操作(参见相关的 stackoverflow 问题)。floatsdoubles

但是,它不适用于所有浮点格式。DEC 的 VAX 浮点格式将符号、指数和分数部分(一分为二)交错,因此它不会这样做。

我不是在问什么

我不关心(对于这个问题)工程师是如何了解这种关系的,或者它在哪里被利用了(参见 FISR)。我想知道它为什么存在。我问过的历史问题,哪一种与这个主题相关的问题可以在逆向计算堆栈交换中找到:在 C 中索引浮点位的早期示例类型双关语和 C 标准/编译器

我也不是在问如何做到这一点,尽管一个好的答案可能包括一个带有一些代码的说明性示例。但是,您应该首先看看 codegolf 上这个惊人的 FISR 问题,看看一些非常有趣的问题。

一个很好的答案

一个糟糕的答案可能是“任何使用滑尺的工作工程师都会理解这种关系”,尽管我可以想象一个很好的答案可能会诉诸滑尺。另一个糟糕的答案可能是“因为 Knuth 是这么说的”,而一个非常好的答案可能会在依赖于 TAOCP 第 2 卷第 4.2.4 节的解释中包括这种情绪(我没有页码,我有 O'Reilly 的分页可憎,它是印刷版第一版的第 240-247 页)。

一个好的答案应该清楚地了解这种关系的性质以及它为什么会出现。证明这种关系必须如此,这将是令人着迷的,尽管我可以预见到一些挑战。

我为什么要问

我正在写一本快速平方反比根的历史,有关它的信息是在 0x5f37642f.com 收集的。作为历史学家,调查这个问题的一部分意味着向社区提出这样的问题。我没有专业知识来自己提炼其中的大部分内容,因为我不是数学家或计算机科学家。

FISR基本上有三个有趣的部分:对数近似、“魔术”常数和牛顿方法。在这三者之间,当人们写到FISR时,他们写的是魔术常数,然后是牛顿方法,然后花很少的时间谈论近似。我从更广泛的“文献”中得到的感觉是,近似是广为人知的,但并不那么清楚。我从答案中学到的东西将帮助我理解它。

在这项工作产生的论文或演讲的致谢中,将注意到一个很好的答案。

我也在问,因为来吧,还没有人问过这个问题,我们已经在这里多久了?

stackoverflow 的相关问题和解答

数学 浮点 对数 近似

评论

2赞 Robert Dodier 3/18/2023
好吧,假设一个浮点数由一个指数和一个尾数组成,指数是一个以 2 为底的对数,尾数位连接到它。如果你想象指数后面的小数点,即除以 2 到尾数位数,你就得到了等于 (floor(log2(x))) 的东西。(stuff) 其中 stuff 从 0 到 1 -- 我认为这是你的分段线性解释。这并不依赖于IEEE的定义本身,只是数字是指数+尾数。
0赞 Mike 'Pomax' Kamermans 3/18/2023
此外,尽管IEEE浮点数与编程有关,但这仍然是100%的问题(也)在 math.stackexchange.com
0赞 Adam Hyland 3/18/2023
我可能会问那边,但有些人在这里发帖,我对他们的意见很感兴趣。
0赞 Adam Hyland 3/19/2023
尽管如此,@njuffa,我从一些人那里听说,滑动尺是一个很好的比喻。从来没有使用过一个,我不知道一种或另一种方式。

答:

8赞 Steve Summit 3/18/2023 #1

我将把我的答案限制在这个问题上,“为什么 浮点数的整数表示形式提供 对数的分段线性近似? 快速平方反比是一个引人入胜的话题,但我不这么认为 对它有足够的了解来谈论它。

但实际上很容易回答标题中的问题。首先,我将从“TL;DR“摘要:

这种近似之所以有效,是因为当一个数字是基数的精确幂时,当用指数表示法时,它的对数(微不足道地)正好是它的指数。像 IEEE-754 这样的浮点符号以 2 为基数的指数符号,指数几乎占据了格式中精度最高的位,只有符号位除外,符号位为 0 表示正数,因此可以忽略不计。所有精度较低的位(即在指数下方)都形成一个完美的线性级数,名义上从 000 到 999,部分原因是 IEEE-754 格式使用具有“隐藏”前导 1 位的归一化有效。

较长的解释从回顾开始:我们对是什么意思? 嗯,它是指数或幂函数的倒数。 指数说,十的二次方 (102) 是 100。所以(以 10 为底)对数函数 问诸如此类的问题,我们必须提高 10 个才能获得 数字 100?答案当然是2。

您还可以问诸如“我们必须提高 10 个才能获得 50号?这显然更难,因为 50 不是偶数 10 的幂。这背后有一些非常酷的数学,我就是这样 不打算试图解释,而是将事情提高到分数 幂的定义方式是 101.69897 是 50。 因此,50 的对数(以 10 为基数)是 1.69897。

然后你也不必将自己限制在 10 的幂。 数学家喜欢使用特殊数字 e (2.71828) 的幂,当然计算机程序员也喜欢(只是喜欢)来 使用 2 的幂。我们必须将 2 提高到什么功率才能 得到数字 32?每个计算机极客都知道它是 5。所以日志 32 的(基数 2)是 5。

然后是科学记数法。让我们以数字 12345 为例。 用科学记数法表示的一种方法是 1.2345 × 104.事实证明,这给了我们一个提示 以 10 为底的 12345 对数是多少。这将是 大于或等于 4,小于 5。(实际上大约是 4.09149。 只要我们选择以某种方式归一化的科学表示,其中有效部分 (俗称“尾数”)被选为 介于 1 和 10 之间。(也就是说,我们无法得到一个很好的估计 以这种方式的对数,如果我们看一下其他等价物 符号 0.12345 × 105 或 12.345 × 103.)

所以现在让我们转向计算机浮点表示。 这些通常使用(IEEE-754 肯定使用)base-2 (二进制)指数表示,即具有归一化 有效值乘以 2 的幂。例如 数字 1.125 表示为 1.125 × 20, 数字 25.25 表示为 1.578125 × 24, 数字 0.1875 表示为 1.5 × 2-3

而且,就像以 10 为基数一样,这些指数给了我们一个粗略的, 以 2 为底的对数的纯整数近似值 — 同样, 只要我们使用归一化有效数,在本例中介于 1 和 2 之间。 因此,对数2(25.25) 将介于 4 和 5 之间。

这也是我伏笔的时候了 一个超级特殊的秘密事实,暗示着这样一个事实, 我们将自己限制在归一化有效值上。 我说他们将是“在 1 到 2 之间”,但更多 准确地说,它们的范围从 1.00000 到 1.99999。 但请注意,无论有效值是多少, 对于以 2 为基数,第一个数字始终为 1。 一会儿,我们将看到我们能用它做些什么。

IEEE-754 二进制浮点表示全部由 一个符号位,后跟一定数量的指数位,后跟 按一定数量的有效位。例如,对于我们的号码 25.25,我们将有一个 0 位代表一个正号, 后跟指数 4 的一些表示形式,后跟 有效数 1.578125 的一些表示。

但现在我们得到了一些原始位的曙光 浮点数的解释可能与 用对数做。对于正数,我们不必担心 关于符号位(因为它是 0),接下来我们看到的是 将成为指数,我们已经看到 指数是 对数。所以剩下要问的是,我们将如何 在具有精确 以 2 为底的对数 (对数2(8) = 3,对数2(16) = 4, log2(32) = 5,以此类推) 以及介于两者之间的小数?

如果我给你等间距的数字,比如 100、200、300 并问 你填写中间的数字,你不会有麻烦: 您可以将 00 部分替换为 01、02、03、...,最多 99。 如果你对二进制数有所了解, 我问你对等间隔的二进制数做同样的事情,比如

00100000
01000000
01100000
10000000

你可以或多或少地做同样的事情,用连续的二进制数、、等替换部分。00000000010001000011

值得注意的是,我们即将看到一些东西 当我们在 IEEE-754 中编码有效值时,这种情况就会发生 浮点数。

让我们开始看一个实际的IEEE-754 浮点格式。我们将使用单精度 float32,以 保持数字合理的一半。此格式使用 1 位 符号 (S),指数 (e) 为 8 位,指数 (e) 为 23 位 significand (s):

S e e e e e e e e s s s s s s s s s s s s s s s s s s s s s s s

让我们开始弄清楚数字的表示形式 16.000 = 24 和 32.000 = 25 看起来像。

16 是 1.0 × 24。所以有效数是 1.0,而 指数为 4。这些将如何存储?我要 首先专注于意义。记住观察结果 第一个数字(小数点左边的数字) 总是 1?IEEE-754 的立场是,这个始终为 1 的前导位根本不需要存储。这 有效值为 1.0 的实际存储位将为 00000。 指数将是 4 的某种表示。所以我要去 像这样表示数字 16.000:

0 4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

这些“4”是近似值;我还没来得及 解释指数是如何编码的。

数字 32.000 是相似的——实际上,它是相同的,除了 对于指数:

0 5 5 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

但我正在建立的关键点是 介于 — 大于 16.0 但小于 32.0 的那些将全部 是变体

0 4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

除了 0 被所有连续的 23 位二进制文件替换 数字 — 所有 2^23 或 8388608 个 — 从 000 开始......001 到 111...111.

所以现在我们已经把所有的东西都准备好了,看看如何 “这 浮点数的整数表示形式提供 对数的分段线性近似”。我们已经展示了 16 到 32 之间的数字的二进制表示 从以某物开头的数字形成一个漂亮的线性斜坡 像 4 一样,到以 5 开头的数字。

我一直在回避指数如何编码的问题。 这是使用偏置方案完成的。对于 binary32,位模式 stored 是实际指数值加上 127。所以我们的指数 值 4 将存储为 4+127=131 的位模式,而 5 将存储为 132。所以 16.0 32.0 将是

0 1 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

其中我填写了编码指数 131 和 132 的二进制值,即 和 .现在我们可以看到实际的原始值1000001110000100

01000001100000000000000000000000和 、 和 、 或 1098907648 和 1107296256。010000100000000000000000000000000x418000000x42000000

这些数字与基数 2 没有任何明显的关系 16 和 32 的对数(即 4 和 5),但它们实际上是 非常接近。它们被偏差的影响所抵消,并且 它们在量级上是完全错误的。但这并不奇怪: 通过获取 32 位量并将它们视为整数,我们可以 得到大数字(确实是整数),但实际 这些对数的值应该是 4.0 和 5.0,我们希望 小数部分的可能性。因此,如果我们要治疗 这些大整数是某些对数的一些再现 其他的,较小的数字,它将涉及非常多的东西 就像定点表示一样。

推导该定点表示的细节是 直截了当:比例因子显然是 223 = 8388608,还有一个偏移量,对应于偏差 内置于 binary32 格式。该偏移量将1065353216 (127 × 223) 如果在缩放前使用,或在缩放后使用 127。

让我们看看这是否有效。我们得出了整数值 1098907648 和 1107296256 对应于 16.0 和 32.0。所以:

(1098907648 - 1065353216) / 8388608 = 4
(1107296256 - 1065353216) / 8388608 = 5

或者相反:

1098907648 / 8388608 - 127 = 4
1107296256 / 8388608 - 127 = 5

所以它正在工作!

有人说,一张图片胜过1000个字,早就该用这种方式来解释了。这是一张图表,并非巧合,它看起来很像你从Mitchell 1961发布的图表:

graph of log2(x), and piecewise linear approximation

红色曲线是实际的 base-2 对数函数。黑点是对数2(X) 为整数的位置。在这些点上,binary32 浮点值的原始位(解释为整数并适当缩放)正好等于对数2。然后,在蓝色中,我们有直接连接这些点的直线段。

回顾一下:如果我们取对应于 2 的精确幂的 binary32 浮点值的原始位,我们得到:

X binary32(十六进制) Binary32 (12月) 日志2
0.25 3e800000 1048576000 -2
0.5 3f000000 1056964608 -1
1 3f800000 1065353216 0
2 40000000 1073741824 1
4 40800000 1082130432 2
8 41000000 1090519040 3
16 41800000 1098907648 4
32 42000000 1107296256 5

2 的精确幂的有效值为 1.0(也就是说,这些数字都是 1.0 × 2N),这意味着它们在 IEEE-754 中被编码为所有 0(由于“隐藏的 1 位”属性)。 由于它们是正数,因此它们的符号位也是 0,唯一不是 0 的部分是指数。由于对于精确幂,指数是对数,因此对于这些数字,在移位和缩放之后,原始位模式的整数解释始终是正对数。(我认为“转移和缩放”的某些方面是神奇数字0x5f37642f的用武之地。

然后,对于所有不是 2 的精确幂,并且对数不是整数的数字,有效值的布局方式意味着原始值(如上图中的蓝线所示)在精确幂之间形成完美的线性插值。因此,在(几乎)所有情况下,原始位模式确实最终确实是“对数的分段线性近似”。(我说“几乎”是因为这个结果不适用于亚正态值以及特殊值 Inf 和 NaN。

对“为什么”这个问题的另一种解释是,“为什么IEEE-754的设计者要这样设置?我不认为这是故意的,也就是说,我不认为要求原始位模式(解释为整数)对应于以 2 为底的对数的分段线性近似。但是,鉴于以下三个相关事实:

  • 指数符号的定义基本上是对数的分段近似
  • IEEE-754 确实有一个目标,即保持正浮点值按其原始位值单调递增
  • IEEE-754 隐藏了有效数字的高阶“1”位;所有有效符号的格式均为 1.xxx,但只有 xxx 部分被存储为这样

“分段线性近似”结果相当整齐。

评论

0赞 Adam Hyland 3/19/2023
感谢您添加图片。我不想通过添加一个问题来弄乱问题,但看看你的问题,我想我会添加一个(可能来自一个旧的来源,很有趣)。现在通读你的答案。
0赞 Adam Hyland 3/19/2023
添加了问题中参考文献中的两张图片,以及我刚才添加的 J.M. Muller 章节中的一张图片。
3赞 Adam Hyland 3/20/2023
这是一个很好的答案,你为它所做的工作是显而易见的。我已经开始赏金,但我希望你从文字中看到,你在很大程度上是要打败的答案。我希望这个页面能成为我在 2009 年遇到 FISR 时希望拥有的参考。
0赞 Steve Summit 3/20/2023
@AdamHyland 谢谢,不用担心赏金。我对这个答案也不完全满意,我不介意看到一个更好(希望更简洁!)的答案。
5赞 Eric Postpischil 3/22/2023 #2

此答案讨论二进制浮点格式的正有限正态数。(该前提不适用于零、负数、无穷大、次正态数和 NaNs。

浮点数 y 表示为固定基数 b、整数指数 e 和有效 s、0 ≤ s < b 的 ±bes对于正常形式,1 ≤ s < b。对于二进制格式,b 为 2。对于正数,符号当然是 +。对于任何特定的浮点格式,e 上都有边界,s 的大小和精度(sb 基数)都有边界。对于这个答案的一般讨论,我们并不十分关心这些特定特征,尽管它们可能会在一些地方被引入。

给定 y = 2 e s,log 2 y = log 2 (2 e s) =e + log 2 s

在 IEEE 754 的“单精度”格式中,binary32 通常用于 ,普通数字 2es 被编码为 0 位表示符号,8 位编码为 e+127 的二进制,23 位编码为 (s−1)223 的二进制。将这 32 位重新解释为二进制数字(按上面列出的顺序从最高有效到最低有效)得到值 (e+127)223+(s−1)223 = (e+126+s)223float

我的问题是,浮点数是什么导致了这种特征?

正如我们在上面看到的,重新解释浮点数的编码,特别是 IEEE-754 二进制32,并不能给出对数的近似值,因为它完全超出了比例,并且偏向了偏移量。

但是,如果我们考虑调整 f(x) = x/2 23−127,则 f(y 重新解释为二进制) = f((e+126+s)2 23) = (e+126+s)2 23/2 23−127 = e+s−1。现在我们看到我们有 y 的指数 e;该值 e+s−1 至少接近对数2 y = e + 对数2 s。因此,它是对数2 y 的近似值。(另请注意,s−1 分别为 0 和 1,当 s = 1 和 2 时,与对数2 s 相同。因此,它是有效数所覆盖的区间内对数2 的近似值。

如果我们将 e 的每个值作为分段线性近似的一个部分,则近似值 e+s−1 是线性的,因为它拟合线性模型 e+s−1 = ay + b,其中 a = 2−e 和 b = e−1,正如我们所看到的,因为 ay + b = 2−e y + e − 1 = 2e(2 e s) + e − 1 = e+s−1。

关于调整函数 f(x) = x/223−127,您经常会在操作浮点数编码的代码中看到这样的方面——它会以各种方式加法或减法以解释指数和有效数的前 1 位的偏差,并且它会以各种方式移动以将指数场移动到所需的位置。

7赞 njuffa 3/22/2023 #3

问题中引用的文字与事实不符。更准确的版本是:正正向 IEEE-754 浮点数的位模式,解释为定点数并针对指数偏差进行调整,为二进制对数函数提供分段线性近似。

为了使该技术起作用,浮点格式必须是二进制的,并且有效位的最高有效位必须是隐式的,而不是实际存储的。此外,它必须将有效位存储在最低有效位的连续块中,并将指数存储在紧邻的连续高有效位块中。IEEE-754 之前的一些浮点格式满足这些要求,但许多格式不满足这些要求。

该技术之所以有效,是因为 IEEE-754 浮点格式使用对数表示,指数提供对数刻度,但有效值提供它们之间的线性刻度细分。这与滑尺不同,滑尺是完全对数的,即主要分区和子分区都遵循对数刻度。

如果我们有一个正的正态IEEE-754数,它的存储指数是真正的指数加上指数偏差127。在 [1,2] 中的有效位中,只存储 [0, 1) 中的小数部分,即 ,因为有效位的最高有效位是隐式的。该数字的值为 2e * (1 + f)。它的二进制对数是对数 2 (2 e * (1 + f)) = 对数 2 (2 e) + 对数2 (1 + f)。给定 的域,一个非常简单和粗略的近似值是 log 2 (1 + f) = f(请注意,这与端点的数学结果匹配),因此 log 2 (2 e * (1 + f)) ≈e + f。binary32yEesfs = 1 + fyf

换言之:减去指数偏差后,该数字表示 的 s8.23 不动点表示,表示 的二元对数的粗略近似值。要将其转换为浮点数,请除以 2 23 或乘以 2-23为了使这个过程在大多数现代处理器上更有效率,人们可以在过程的最后而不是一开始就减去指数偏差。这导致了以下示例性 ISO-C99 实现,最大绝对误差为 8.6e-2:yy

#include <stdint.h>
#include <string.h>

uint32_t float_as_uint32 (float x) 
{ 
    uint32_t r; 
    memcpy (&r, &x, sizeof r); 
    return r; 
} 

float fastest_log2 (float y)
{
    const int FP32_EXPO_BIAS = 127;
    const float two_to_m23 = 1.192092896e-7f; // 0x1.0p-23
    float x = (float)(int) float_as_uint32 (y);
    return x * two_to_m23 - (float) FP32_EXPO_BIAS;
}

在支持融合乘法加法运算且具有适当编译器设置的硬件上,最后一行应映射到某种风格的 FMA。fastest_log2()

评论

0赞 chux - Reinstate Monica 3/25/2023
好的答案,但“为了使该技术起作用,浮点格式必须是二进制的”和“有效位的最高有效位必须是隐式的,而不是实际存储的”,一般来说是不正确的。是的,有这 2 个限制确实有助于示例代码。
0赞 chux - Reinstate Monica 3/25/2023
一个尚未提及的限制:和的字节序需要相同。floatuint32_t
0赞 njuffa 3/26/2023
@chux-恢复莫妮卡 为了简单地将浮点数的位模式重新解释(例如在我的代码中)为近似于二进制对数的定点数(问题中的声明),浮点格式必须是二进制的,并且具有隐式前导位和组件按说明排序。如果不满足这些条件,则首先需要使用位操作来重新排列位,如果是 IBM 十六进制浮点格式,则对指数进行基数转换。这超出了所声称的范围。float_to_uint32()