数值计算 --- 平方根倒数快速算法(下)

news2024/12/25 2:34:43

平方根倒数快速算法(下) --- 向Greg Walsh致敬!

 

数值计算 --- 平方根倒数快速算法(上)_开根号倒数快速-CSDN博客文章浏览阅读712次,点赞31次,收藏30次。由于平方根倒数快速算法实在是太过经典,出于对code中magic number"0x5f3759df"的好奇,出于对John Carmack“what the fuck!”这一锐评的热爱,还有出于对于这一算法的原作者Greg Walsh的致敬。我写了这篇文章,作为该算法的自学笔记。_开根号倒数快速https://blog.csdn.net/daduzimama/article/details/142306873

数值计算 --- 平方根倒数快速算法(中)-CSDN博客文章浏览阅读752次,点赞17次,收藏27次。由于平方根倒数快速算法实在是太过经典,出于对code中magic number"0x5f3759df"的好奇,出于对John Carmack“what the fuck!”这一锐评的热爱,还有出于对于这一算法的原作者Greg Walsh的致敬。我写了这篇文章,作为该算法的自学笔记。https://blog.csdn.net/daduzimama/article/details/142444260

        在我的上一篇文章中,我停在了神秘数---5f3759df这一关乎到如何才能尽可能精确且快速的计算出用于NR-iteration初值的常数的讨论。在这篇文章中,我将继续讨论“5f3759df”这个数。这个让人着迷的神秘数究竟是怎么来的已经无从得知了(根据我在网上查到的现有资料中还没有发现)。但,既然code中明确用到了这个数,我在这里可以简单的“就数论数”。看看这个数究竟对初值的计算究竟有什么帮助。

        注意:为了便于行文顺畅,以及对照前后文看时不容易引起误解和混淆,我这里所有公式的编号都将延续上文公式的index。但会给这篇文章中的新公式给予新的index。本来这一系列的文章我最初都是放在一篇文章中写的,但那样的文章实在是太过于冗长,这才分成了上中下三篇。


1,再论log2(1+M)的近似

        为了说明这个问题,我们先回到上篇文章中的“log2(1+M)的近似”。在上一篇文章中我写道:

        由于M_{x}是一个0~1之间的小数。以M为自变量,当M=0~1时,以M为自变量的函数y=log2(1+M)与函数y=M的函数值差异很小。故而,有如下近似:

 {log_{2}}^{(1+M_{x})}\approx M_{x}when \; M_{x}=0\sim 1(式10)

基于这一近似,再加上后续一系列的推导依次得到: 

{log_{2}}^{x}\approx 1/2^{-23}\cdot (E_{x}\times 2^{23}+T_{x})-127(式12)

{log_{2}}^{x}\approx x_{B}/2^{23}-127(式14)

{log_{2}}^{a}=-1/2\cdot {log_{2}}^{x}(式16)

a_{B}=381\times 2^{22}-x_{B}/2(式17)

a_{B}=5f400000-x_{B}/2(式18)

        如果详细观察上述近似的函数图像可知,这种近似只有在函数两端的近似效果比较好,越是到函数中部,误差越大。

                 

        为了改善这一问题,我们对原来的近似做一个修正。令y=M的函数上移一部分,即,让函数加上一个很小的正数σ,得到如下函数和对应的函数图像(下图中的黄线):

y=M+\sigma


2,σ是多少才合适呢?

         

        基于上面的结论,我们为了尽可能均衡的分散计算误差,选择了用M+σ的近似替换原来的近似得到:

{log_{2}}^{(1+M_{x})}\approx M_{x}+\sigmawhen \; M_{x}=0\sim 1(式19)

原来的近似是:

 {log_{2}}^{(1+M_{x})}\approx M_{x}when \; M_{x}=0\sim 1(式10)

        Tips:为了加以区别,文中引用的所有前文中的公式,都沿用前文中所使用的背景颜色(淡红色黄色)。而,本文中新公式的背景色都是淡蓝色

        基于新的近似(式19),浮点数x的对数表达式(式9)变为:

{log_{2}}^{x}={log_{2}}^{(1+M_{x})}+E-127\approx M_{x}+\sigma +E_{x}-127

得到: 

{log_{2}}^{x}\approx M_{x}+\sigma +E_{x}-127(式20)

        同样的,把之前的整体替换(式8)代到(式20)中: 

M_{x}=T_{x}\cdot 2^{-23}(式8)

 (式20) 变为:

 {log_{2}}\approx M_{x}+\sigma+E_{x}-127=T_{x}\cdot 2^{-23}+\sigma+E_{x}-127=1/2^{23}\cdot (E_{x}\times 2^{23}+T_{x})+\sigma-127

 得到:

{log_{2}}\approx 1/2^{23}\cdot (E_{x}\times 2^{23}+T_{x})+\sigma-127(式21)

        再把浮点数x在计算机中的存储形式(式13)代入上式:

x_{B}=E_{x}\times 2^{23}+T_{x}(式13)

(式21) 变为:

{log_{2}}\approx 1/2^{23}\cdot (E_{x}\times 2^{23}+T_{x})+\sigma-127=1/2^{23}\cdot x_{B}+\sigma-127

得到:

{log_{2}}\approx x_{B}/2^{23}+\sigma-127(式22)

         把(式22)代入(式16)的左右两边:

{log_{2}}^{a}=-1/2\cdot {log_{2}}^{x}(式16)

        左边:

 {log_{2}}^{a}=a_{B}/2^{23}+\sigma-127

        右边:

-1/2\cdot {log_{2}}^{x}=-1/2\cdot (x_{B}/2^{23}+\sigma-127)

         最终(式16)变为:

a_{B}/2^{23}+\sigma-127=-1/2\cdot (x_{B}/2^{23}+\sigma-127)

得到: 

a_{B}=3\times (127-\sigma )\times 2^{22}-x_{B}/2(式23)

        令(式23)中的“3\times (127-\sigma )\times 2^{22}”等于十六进制的5f3759df

3\times (127-\sigma )\times 2^{22}=5f3759df_{16}

                

        最终求出函数的上移幅度σ: 

 \sigma =127-5f3759df_{16}/3/(2^{22})=127-1597463007_{10}/3/(2^{22})=0.04504656791687012

        把这一结果用matlab画出来得到新的近似函数图像和均匀分布后的误差:

         通过把原来的近似函数上移了一个约等于0.045的σ,近似函数与原函数的误差从原来的中间误差特别大而两端误差很小,变成了较为均衡的分布,并在两个点处达到最小。

        


3,还有比“5f3759df”更好的选择吗?

        由于这套算法被很多人使用和研究过,又因为这套代码最令人着迷的就是那个神秘数。因此,在我自己学习这套代码的时候,除了之前提到的粗略近似所使用的常数“5f400000”和Grag Walsh文中给出的“5f3759df”,还有一个比较著名的数学家Chris Lomont也提出了他认为较为理想的常数。并通过理论计算和穷举法分别找到了0x5f37642f和0x5f375a86。

        此外,在他的论文中,Lomont亦指出了64位的IEEE754浮点数(即双精度类型)所对应的魔术数字是0x5fe6ec85e7de30da,但后来的研究表明0x5fe6eb50c7aa19f9的结果精确度更高。McEniry得出的结果则是0x5FE6EB50C7B537AA,精度介于两者之间,Matthew Robertson给出的精度更高的值是0x5FE6EB50C7B537A9。

        这里我想通过3个数的实验来结束关于这一问题的讨论。并基于这三个数的实验看看“5f400000”,“5f3759df”,“5f37642f”和“5f375a86”这四个数究竟谁的精度更好。这里稍微提一嘴,由于“5f3759df”,“5f37642f”和“5f375a86”这三个数都小于“5f400000”,这说明前面三者干的都是一件事,都在把“5f400000”所对应的直线向上做平移。

        我们分别以x=2,3,4为例,即用上边四个常数分别求1/\sqrt{2},1/\sqrt{3},1/\sqrt{4},并且给出初值和迭代1,2,3次后的结果,看看究竟哪个常数好?

# include <stdio.h>
# include <math.h>

void rsqrt_5f400000(float number,float *y)
{
	long i;
	float x2;
	const float threehalfs = 1.5F;

	x2 = number * 0.5F;
	y[0] = number;
	i = *(long*)&y[0];                       // evil floating point bit level hacking
	i = 0x5f400000 - (i >> 1);
	y[0] = *(float*)&i;
	y[1] = y[0] * (threehalfs - (x2 * y[0] * y[0]));   // 1st iteration
	y[2] = y[1] * ( threehalfs - ( x2 * y[1] * y[1]) );   // 2nd iteration, this can be removed
	y[3] = y[2] * (threehalfs - (x2 * y[2] * y[2]));
}

void rsqrt_5f3759df(float number, float* y)
{
	long i;
	float x2;
	const float threehalfs = 1.5F;

	x2 = number * 0.5F;
	y[0] = number;
	i = *(long*)&y[0];                       // evil floating point bit level hacking
	i = 0x5f3759df - (i >> 1);
	y[0] = *(float*)&i;
	y[1] = y[0] * (threehalfs - (x2 * y[0] * y[0]));   // 1st iteration
	y[2] = y[1] * (threehalfs - (x2 * y[1] * y[1]));   // 2nd iteration, this can be removed
	y[3] = y[2] * (threehalfs - (x2 * y[2] * y[2]));
}

void rsqrt_5f37642f(float number, float* y)
{
	long i;
	float x2;
	const float threehalfs = 1.5F;

	x2 = number * 0.5F;
	y[0] = number;
	i = *(long*)&y[0];                       // evil floating point bit level hacking
	i = 0x5f37642f - (i >> 1);
	y[0] = *(float*)&i;
	y[1] = y[0] * (threehalfs - (x2 * y[0] * y[0]));   // 1st iteration
	y[2] = y[1] * (threehalfs - (x2 * y[1] * y[1]));   // 2nd iteration, this can be removed
	y[3] = y[2] * (threehalfs - (x2 * y[2] * y[2]));
}

void rsqrt_5f375a86(float number, float* y)
{
	long i;
	float x2;
	const float threehalfs = 1.5F;

	x2 = number * 0.5F;
	y[0] = number;
	i = *(long*)&y[0];                       // evil floating point bit level hacking
	i = 0x5f375a86 - (i >> 1);
	y[0] = *(float*)&i;
	y[1] = y[0] * (threehalfs - (x2 * y[0] * y[0]));   // 1st iteration
	y[2] = y[1] * (threehalfs - (x2 * y[1] * y[1]));   // 2nd iteration, this can be removed
	y[3] = y[2] * (threehalfs - (x2 * y[2] * y[2]));
}

int main() {
	float x = 2.0f;
	printf("input x=%f\n", x);
	printf("ideal result=%.7f\n", 1 / sqrt(x));

	float y[4] = {0};
	rsqrt_5f400000(x,y);
	printf("calc with 5f400000 = %.7f,%.7f,%.7f,%.7f\n", y[0], y[1], y[2], y[3]);
	rsqrt_5f3759df(x, y);
	printf("calc with 5f3759df = %.7f,%.7f,%.7f,%.7f\n", y[0], y[1], y[2], y[3]);
	rsqrt_5f37642f(x, y);
	printf("calc with 5f37642f = %.7f,%.7f,%.7f,%.7f\n", y[0], y[1], y[2], y[3]);
	rsqrt_5f375a86(x, y);
	printf("calc with 5f375a86 = %.7f,%.7f,%.7f,%.7f\n", y[0], y[1], y[2], y[3]);
    return 0;
}


 对1/\sqrt{2}而言:

        先看初值,“5f400000” 的精度明显要低于后面三者,但抛开精度不谈,这四个常数的近似结果也和理想值0.7071068都差的比较远。经过一次迭代后,由于“5f400000”初值的精度低,他一次迭代后的结果也很低。后面三者的精度也只停留在小数点后两位,其中,“5f3759df”的0.7069300是最接近0.7071068的。如果是按只迭代一次的原程序的话,计算到这里那还是“5f3759df”的精度是最高的,即,最接近正确答案0.7071068。

        但经过两轮迭代之后却是"5f375a86"的0.7071068精度最高。最后,经过三次迭代,4个常数都得到了和正确答案一样的结果。


 对1/\sqrt{3}而言:

        

        就初值来看依然是“5f400000”的精度最低,而且要比后三者低很多。这是符合预期的,毕竟这是最4者中最糙的一个近似。对后三者而言,“5f37642f”的0.5913724是最接近0.5773503。但整体看下来,三者的精度依然不理想。经过两次迭代后“5f3759df”的结果最接近标准答案,但整体的精度也仅能达到小数点后两位。


 对1/\sqrt{4}而言:

        这是一个比较特殊的数,直接能够得到精确答案0.5。从程序运行的结果上看“5f400000”这个常数对这种计算最敏感,计算初值的时候就直接得到了标准答案,后面的多次迭代也是标准答案。

        就后三者而言,“5f37642f”的初值0.4831862最接近0.5,但三者初值的精度依旧都不高。


小结: 

         

        经过我简单的实验,我得到如下结论:

1,四个常数初值的计算结果都不太理想(除了“5f400000”在计算结果为可整除的小数时)。因此,后续的NR-iteration是必不可少的。

2,由于源代码中只算了一次NR-iteration,单看这一次的结果,平均下来也就只能精确到小数点后两位。

3,实验中所用到的这四个常数,除了“5f400000”。基本上在迭代一次后都能达到差不多的效果,不存在那个比另一个精度高很多的情况。因此,Greg walsh在code中所使用的magic number---“5f3759df”能够出色的完成任务,即便是后续有“更好的常数”,在这个算法框架下相较于“5f3759df”也很难提高太多。

4,Greg walsh的这套平方根倒数快速算法固然快,也设计的非常精妙,同时用到了很多学科的知识点。但如果只用一次NR-iteration,计算结果的精度依然非常有限。


(全文完) 

--- 作者,松下J27

参考文献(鸣谢):

1,https://en.wikipedia.org/wiki/Newton%27s_method#Examples

2,什么代码让程序员之神感叹“卧槽”?改变游戏行业的平方根倒数算法_哔哩哔哩_bilibili

3,[算法] 平方根倒数速算法中的魔数0x5f3759df的来源 | GeT Left

4,揭秘·变态的平方根倒数算法

5,https://www.youtube.com/watch?v=p8u_k2LIZyo

6,计算机中的浮点数(一)_浮点表示法-CSDN博客

7,计算机中的浮点数(二)-CSDN博客 

8,Beyond3D - Origin of Quake3's Fast InvSqrt()

9,Beyond3D - Origin of Quake3's Fast InvSqrt() - Part Two

10,The Fast Inverse Square Root

11,How Fast Inverse Square Root actually works

版权声明:所有的笔记,可能来自很多不同的网站和说明,在此没法一一列出,如有侵权,请告知,立即删除。欢迎大家转载,但是,如果有人引用或者COPY我的文章,必须在你的文章中注明你所使用的图片或者文字来自于我的文章,否则,侵权必究。 ----松下J27  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2181451.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Temu、亚马逊如何建立稳固的测评系统、避免挂单?

在跨境电商的测评与补单过程中&#xff0c;许多卖家和测评工作室常常面临由于技术环境不足导致的下单成功率低的问题。尤其是新账号在首次下单时&#xff0c;往往会遇到F号或砍单的困扰&#xff0c;进而陷入频繁购买和账号损失的恶性循环。这不仅消耗了大量时间和精力&#xff…

【真实访问】那些选择土木专业的学生,后来怎么样了?

“你会让孩子报土木专业吗&#xff1f;” 7月15日&#xff0c;澎湃新闻在微博上发起线上调研&#xff0c;截至16日12时&#xff0c;8000多人参与了投票&#xff0c;结果显示近7000人选择“不会&#xff0c;天坑专业”。短短几年时间&#xff0c;土木工程专业的报考从“香饽饽”…

Android Studio 新版本 Logcat 的使用详解

点击进入官方Logcat介绍 一个好的Android程序员要会使用AndroidStudio自带的Logcat查看日志&#xff0c;会Log定位也是查找程序bug的第一关键。同时Logcat是一个查看和处理日志消息的工具&#xff0c;它可以更快的帮助开发者调试应用程序。 步入正题&#xff0c;看图说话。 点…

如何实现Mybatis自定义插件

背景 MyBatis的插件机制&#xff0c;也可称为拦截器&#xff0c;是一种强大的扩展工具。它允许开发者在不修改MyBatis框架源代码的情况下&#xff0c;通过拦截和修改MyBatis执行过程中的行为来定制和增强功能。 MyBatis插件可以拦截四大核心组件的方法调用&#xff1a;Executor…

建筑中的文化表达与地方特色:演绎地域之魂

在浩瀚的城市风貌中&#xff0c;每一座建筑都是文化的载体&#xff0c;无声地讲述着地域的故事与精神。建筑不仅需要满足功能需求&#xff0c;更应成为文化传承与创新的舞台。本文旨在深度剖析建筑设计如何在尊重与弘扬地方文化的基础上&#xff0c;巧妙融合现代元素&#xff0…

828华为云征文|华为云 Flexus X实例之家庭娱乐中心搭建

话接上文《828华为云征文&#xff5c;华为云Flexus X实例初体验》&#xff0c;这次我们利用手头的 Flexus X 实例来搭建家庭影音中心和密码管理环境。 前置环境 为了方便小白用户甚至运维人员&#xff0c;我觉得现阶段的宝塔面板 和 1Panel 都是不错的选择。我这里以宝塔为例…

数通 1

通信&#xff1a;需要介质才能通信电话离信号塔&#xff08;基站&#xff09;越远&#xff0c;信号越弱。信号在基站之间传递。你离路由器越远&#xff0c;信号越差。一个意思 比如想传一张图片&#xff0c;这张图片就是数据载荷 网关&#xff0c;分割两个网络。路由器可以是网…

全网最详细——OpenFlow 协议分析

目录 1 OpenFlow 交换机 2 流表 3 OpenFlow 通道 4 任务描述 5 任务要求 6 任务实施 7 任务验收 1 OpenFlow 交换机 Open Flow 交换机可以分成流表和安全通道两部分。在 Open Flow协议规范中&#xff0c;控制器可以给交换机下发流表项来指导交换机处理匹配流表项的数据包…

ELK--收集日志demo

ELK--收集日志demo 安装ELK日志收集配置启动容器springboot配置测试 之前项目多实例部署的时候&#xff0c;由于请求被负载到任意节点&#xff0c;所以查看日志是开多个终端窗口。后来做了简单处理&#xff0c;将同一项目的多实例日志存入同一个文件&#xff0c;由于存在文件锁…

如何从相机的记忆棒(存储卡)中恢复丢失照片

当您意识到不小心从存储卡中删除了照片&#xff0c;或者错误地格式化了相机的记忆棒时&#xff0c;这些是您会大喊的前两个词。这是一种常见的情况&#xff0c;每个人在他们的一生中都会面临它。幸运的是&#xff0c;有一些方法可以从相机的 RAW 记忆棒&#xff08;存储卡&…

【专题总结】【一文解决】C++多继承下的构造函数执行顺序

多继承下的构造函数执行顺序 派生类构造函数执行顺序如下 ①调用基类构造函数→调用顺序按它们被继承时【从左至右】被说明的次序 ②调用子对象的构造函数→调用顺序按它们在【类中说明次序】 ③调用派生类的构造函数 【典型题1】13浙工大卷二读程序4题 【分析】下面①classC:p…

C语言指针详解与应用(不断更新)

指针简介 指针(Pointer)是C语言的一个重要知识点&#xff0c;其使用灵活、功能强大&#xff0c;是C语言的灵魂 指针与底层硬件联系紧密&#xff0c;使用指针可操作数据的地址&#xff0c;实现数据的间接访问 指针生活实例化 指针的本质是地址&#xff0c;在生活中比如你取快…

当年掏空身体的9款怀旧软件,满满回忆杀

有个网站掀起了一股怀旧软件的风潮&#xff0c;让人惊喜地发现&#xff0c;尽管许多软件已不再更新&#xff0c;但时至今日&#xff0c;部分软件依然能够正常运行。 想当年&#xff0c;电脑价格贵的很&#xff0c;一但有机会接触电脑&#xff0c;那就是全神贯注&#xff0c;以…

仕考网:国考省考考试内容区别

国考和省考备考内容有一定的相似之处&#xff0c;具体考哪些内容你了解多少?中仕为大家分享一下吧! 题量&#xff1a; ①国考&#xff1a;行测一般有130-135道题目; ②省考&#xff1a;题量大多在120道左右&#xff0c;胳臂省份不同; 常识判断&#xff1a; ①国考&#x…

latex打出邮箱图标和可点击的orcidID

如图所示&#xff1a; 邮箱的打法 \usepackage{bbding} \inst{(}\Envelope\inst{)}orcidID的打法 \newcommand{\myorcidID}[1]{\href{https://orcid.org/#1}{\includegraphics[width8pt]{res/orcid.png}}} \captionsetup[algorithm]{skip5pt} \definecolor{customblue}{RGB}{…

使用 Colly 在 Golang 中进行网页抓取的步骤

什么是 Colly&#xff1f; Go 是一种用途广泛的语言&#xff0c;它拥有可以完成几乎所有工作的包和框架。 今天&#xff0c;我们将使用一个名为 Colly 的框架&#xff0c;它是一个用 Go 语言编写的、高效且强大的网页抓取框架&#xff0c;用于从网络上抓取数据。它提供了一个…

IPD的定义和三大重组

目前&#xff0c;业界对IPD的一般理解是&#xff1a;IPD——Integrated Product Development&#xff08;集成产品开发&#xff09;是一套领先的、成熟的产品开发的管理思想、模式和方法。它是根据大量成功的产品开发管理实践总结出来的&#xff0c;并被大量实践证明的高效的研…

Oracle 配置恢复目录catalog

一.介绍 Oracle中使用RMAN备份的数据我们分为两类 RMAN知识库数据库的数据块 Oracle默认把 RMAN知识库 放在目标数据库的控制文件中&#xff0c;在以后进行恢复的时候 我们要先读知识库的信息然后才能恢复。 但这样就产生了一个问题&#xff0c;知识库放在了控制文件上&#xf…

Whisper的使用

whisper的下载路径&#xff1a;https://github.com/openai/whisper需要安装以下的包。要求python的版本在3.9以上&#xff1a;如果当前python环境在3.9以下&#xff0c;可以换whisper的版本。点一下 releases 按钮。可以下载其他版本。使用whisper的时候需要其他包的安装。记住…

TypeScript 算法手册 - 【冒泡排序】

文章目录 TypeScript 算法手册 - 冒泡排序1. 冒泡排序简介1.1 冒泡排序定义1.2 冒泡排序特点 2. 冒泡排序步骤过程拆解2.1 比较相邻元素2.2 交换元素2.3 重复过程 3. 冒泡排序的优化3.1 提前退出3.2 记录最后交换位置案例代码和动态图 4. 冒泡排序的优点5. 冒泡排序的缺点总结 …