平方根(倒数)快速算法

平方根倒数快速算法

平方根常出现在游戏的图形计算中,尤其是求一个向量的基向量时,还会以倒数的形式出现,所以如何快速求解平方根一度成为了游戏引擎优化中的一个难题。

本文会先介绍原理,最后放出实现的代码,包括手动实现和使用内联汇编一步到位。
如果对汇编语言感兴趣,欢迎移步2万字汇编语言学习笔记指令集和现代x86汇编

引入:约翰卡马克的代码

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

	x2 = number * 0.5F;
	y  = number;
	i  = * ( long * ) &y;                       // evil floating point bit level hacking(邪恶的浮点数位运算黑科技)
	i  = 0x5f3759df - ( i >> 1 );               // what the fuck?(这是什么鬼?)
	y  = * ( float * ) &i;
	y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration (第一次迭代)
//    y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed(第二次迭代,可以删除)

	return y;
}

原理

当知道一个数的值的时候,你就知道这个数的值;当知道一个数的精确近似值时,你就知道这个数的精确近似值。(原地tp)

牛顿迭代法需要迭代的次数取决于给定的近似值的近似程度,所以需要想办法给出更加精确的近似值。

浮点数

IEEE754规格化浮点数

在这里插入图片描述

有效数字是从小数点后第一位开始的,小数点前隐含一个1,即

对任意浮点数 x ,总有 x = ( − 1 ) S i g n a t u r e ⋅ ( 1 + m ) ⋅ 2 E − B 其中 c o n s t   B = 127 对任意浮点数x,总有x=(-1)^{Signature}\cdot(1+m)\cdot2^{E-B}\\ 其中const\ B = 127 对任意浮点数x,总有x=(1)Signature(1+m)2EB其中const B=127
上图中可以得知符号部分0指数01111100数字部分(1+0.01000000000000000000000),即0.15625

求平方根转化为求对数

y = S q r t ( x ) ,即 y = x 1 2 log ⁡ 2 y = log ⁡ 2 x 1 2 = 1 2 log ⁡ 2 x 先尝试对 log ⁡ 2 x 进行变换 把 x = ( − 1 ) S i g n a t u r e ⋅ ( 1 + m ) ⋅ 2 E − B 且 S i g n a t u r e = 0 带入 log ⁡ 2 x ,得 log ⁡ 2 x = log ⁡ 2 [ ( 1 + m ) ⋅ 2 E − B ] 用 m b i t 表示 m 的二进制 (这里由于 m 表示的是一个小数,而 m 的二进制本身可以被视为整数,所以需要区分开来) log ⁡ 2 x = log ⁡ 2 [ ( 1 + m b i t 2 23 ) ⋅ 2 E − B ] log ⁡ 2 x = log ⁡ 2 ( 1 + m b i t 2 23 ) + E − B 令 t = m b i t 2 23 ,则 t ∈ [ 0 , 1 ] f ( t ) = log ⁡ 2 ( 1 + t ) 在 t ∈ [ 0 , 1 ] 时可近似视为 f ( t ) = t 故 log ⁡ 2 x ≈ m b i t 2 23 + E − B = 1 2 23 ⋅ ( 2 23 ⋅ E + m b i t ) − B 此时凑出来的 2 23 非常巧妙,可以视为给 E 补 23 个 0 这里不用把 E 与 E 的二进制值区分开是因为 E 的意义与其二进制值表示的意义都是整数,是相同的 同时, m b i t 刚好是 23 位,两者相加,可以视为整个 x 的二进制表示 log ⁡ 2 x = 1 2 23 x b i t − B ( 1 ) 把 ( 1 ) 式代回 z = 1 2 ⋅ log ⁡ 2 x ,得 z = 1 2 ⋅ ( 1 2 23 x b i t − B ) 即 1 2 23 y b i t − B = 1 2 ⋅ ( 1 2 23 x b i t − B ) 整理,得 y b i t = 1 2 x b i t + 2 22 ⋅ B y = Sqrt(x),即y = x^{\frac{1}{2}}\\ \log_2y = \log_2x^{\frac{1}{2}}=\frac{1}{2}\log_2x\\ \\ 先尝试对\log_2x进行变换\\ 把x=(-1)^{Signature}\cdot(1+m)\cdot2^{E-B}且Signature=0带入\log_2x,得\\ \log_2x=\log_2[(1+m)\cdot2^{E-B}]\\ 用m_{bit}表示m的二进制\\ _{(这里由于m表示的是一个小数,而m的二进制本身可以被视为整数,所以需要区分开来)}\\ \log_2x=\log_2[(1+\frac{m_{bit}}{2^{23}})\cdot2^{E-B}]\\ \log_2x=\log_2(1+\frac{m_{bit}}{2^{23}})+E-B\\ 令t=\frac{m_{bit}}{2^{23}},则t\in[0,1]\\ f(t)=\log_2(1+t)在t\in[0,1]时可近似视为f(t)=t\\ 故\log_2x\approx\frac{m_{bit}}{2^{23}}+E-B=\frac{1}{2^{23}}\cdot(2^{23}\cdot E + m_{bit})-B\\ 此时凑出来的2^{23}非常巧妙,可以视为给E补23个0\\ _{这里不用把E与E的二进制值区分开是因为E的意义与其二进制值表示的意义都是整数,是相同的}\\ 同时,m_{bit}刚好是23位,两者相加,可以视为整个x的二进制表示\\ \log_2x=\frac{1}{2^{23}}x_{bit}-B\qquad\qquad(1)\\ \\ 把(1)式代回z=\frac{1}{2}\cdot\log_2x,得z=\frac{1}{2}\cdot(\frac{1}{2^{23}}x_{bit}-B)\\ 即\frac{1}{2^{23}}y_{bit}-B=\frac{1}{2}\cdot(\frac{1}{2^{23}}x_{bit}-B)\\ 整理,得y_{bit}=\frac{1}{2}x_{bit}+2^{22}\cdot B\\ y=Sqrt(x),即y=x21log2y=log2x21=21log2x先尝试对log2x进行变换x=(1)Signature(1+m)2EBSignature=0带入log2x,得log2x=log2[(1+m)2EB]mbit表示m的二进制(这里由于m表示的是一个小数,而m的二进制本身可以被视为整数,所以需要区分开来)log2x=log2[(1+223mbit)2EB]log2x=log2(1+223mbit)+EBt=223mbit,则t[0,1]f(t)=log2(1+t)t[0,1]时可近似视为f(t)=tlog2x223mbit+EB=2231(223E+mbit)B此时凑出来的223非常巧妙,可以视为给E230这里不用把EE的二进制值区分开是因为E的意义与其二进制值表示的意义都是整数,是相同的同时,mbit刚好是23位,两者相加,可以视为整个x的二进制表示log2x=2231xbitB(1)(1)式代回z=21log2x,得z=21(2231xbitB)2231ybitB=21(2231xbitB)整理,得ybit=21xbit+222B

此时得到了平方根,再在运算中直接除以这个数就可以了

直接求平方根倒数

运算除法的开销比运算乘法高,能否直接求平方根倒数后再进行乘法呢?
y = x − 1 2 log ⁡ 2 y = − 1 2 log ⁡ 2 x 把 ( 1 ) 式代入,得 1 2 23 ⋅ y b i t − B = − 1 2 ⋅ ( 1 2 23 ⋅ x b i t − B ) 整理,得 y b i t = 3 B ⋅ 2 22 − 1 2 x b i t 到这里就是最终形式了 y=x^{-\frac{1}{2}}\\ \log_2y = -\frac{1}{2}\log_2x\\ 把(1)式代入,得\\ \frac{1}{2^{23}}\cdot y_{bit}-B=-\frac{1}{2}\cdot(\frac{1}{2^{23}}\cdot x_{bit}-B)\\ 整理,得y_{bit}=3B\cdot2^{22}-\frac{1}{2}x_{bit}\\ 到这里就是最终形式了 y=x21log2y=21log2x(1)式代入,得2231ybitB=21(2231xbitB)整理,得ybit=3B22221xbit到这里就是最终形式了

代入数据

float

y b i t = 381 ⋅ 2 22 − 1 2 x b i t y_{bit}=381\cdot2^{22}-\frac{1}{2}x_{bit} ybit=38122221xbit

对应的值为0x5F40 0000

double

推广到双精度浮点型变量

y b i t = 3069 ⋅ 2 51 − 1 2 x b i t y_{bit}=3069\cdot2^{51}-\frac{1}{2}x_{bit} ybit=306925121xbit

对应的值为0x5FE8 0000 0000 0000

调整

为什么上述计算得到的值为0x5F40 0000,而不是之前提到的代码中的0x5F37 59DF呢

因为在把 log ⁡ 2 \log_2 log2近似为一条直线时,产生的误差仅在两端较小,中间较大,为了减小这一误差,便进行了一定的调整,使得结果更贴近真实值

double类型的值有待精确,但是通过单精度浮点型计算得出的结果已经够用了,0x5FE6 8000 0000 0000,这个值的出来的结果很可能不够精确,需要多迭代一次

后续处理

在上述计算过程中使用了近似处理,误差是不可避免的,因此一般还会再进行一次牛顿迭代法求解

牛顿迭代法

y = f ( x ) 上一点 ( x n , f ( x n ) ) 在该点处作切线,其斜率 k = f ′ ( x n ) 点斜式 : y − f ( x n ) = f ′ ( x n ) ( x − x n ) 令 y = 0 ,得 x n + 1 = x n − f ( x n ) f ′ ( x n ) 在求平方根这一实例中, f ( x ) = x n 2 − i n p u t , f ′ ( x ) = 2 x n x n + 1 = x n − x n 2 − i n p u t 2 x n x n + 1 = 1 2 ( x n + i n p u t x n ) 在求平方根倒数这一实例中, f ( x ) = 1 x 2 − i n p u t , f ′ ( x ) = − 2 x − 3 x n + 1 = x n − 1 x n 2 − i n p u t − 2 x n 3 = x n + x n − i n p u t ⋅ x n 3 2 x n + 1 = x n ⋅ ( 3 2 − 1 2 i n p u t ⋅ x n 2 ) y=f(x)上一点(x_n,f(x_n))\\ 在该点处作切线,其斜率k=f'(x_n)\\ 点斜式:y-f(x_n)=f'(x_n)(x-x_n)\\ 令y=0,得\\ x_{n+1}=x_n-\frac{f(x_n)}{f'(x_n)}\\ \\ 在求平方根这一实例中,f(x)=x^2_n-input,f'(x)=2x_n\\ x_{n+1}=x_n-\frac{x^2_n-input}{2x_n}\\ x_{n+1}=\frac{1}{2}(x_n+\frac{input}{x_n})\\ 在求平方根倒数这一实例中,f(x)=\frac{1}{x^2}-input,f'(x)=-2x^{-3}\\ x_{n+1}=x_n-\frac{\frac{1}{x^2_n}-input}{-\frac{2}{x^3_n}}=x_n+\frac{x_n-input\cdot x^3_n}{2}\\ x_{n+1}=x_n\cdot(\frac{3}{2}-\frac{1}{2}input\cdot x_n^2) y=f(x)上一点(xn,f(xn))在该点处作切线,其斜率k=f(xn)点斜式:yf(xn)=f(xn)(xxn)y=0,得xn+1=xnf(xn)f(xn)在求平方根这一实例中,f(x)=xn2inputf(x)=2xnxn+1=xn2xnxn2inputxn+1=21(xn+xninput)在求平方根倒数这一实例中,f(x)=x21input,f(x)=2x3xn+1=xnxn32xn21input=xn+2xninputxn3xn+1=xn(2321inputxn2)

平方根

表达式已经在上述的计算中得到了: y b i t = 1 2 x b i t + 2 22 ⋅ B y_{bit}=\frac{1}{2}x_{bit}+2^{22}\cdot B ybit=21xbit+222B

代入数据,得

y b i t = 1 2 x b i t + 0 x 1 F C 00000 y_{bit}=\frac{1}{2}x_{bit}+0x1FC00000 ybit=21xbit+0x1FC00000(float)

y b i t = 1 2 x b i t + 0 x 1 F F 8000000000000 y_{bit}=\frac{1}{2}x_{bit}+0x1FF8000000000000 ybit=21xbit+0x1FF8000000000000(double)

没有严格的计算误差,直接参考了0x5F40 0000与0x5F37 59DF的差值,得到0x1FB7 59DF

普渡大学的数学家Chris Lomont看了以后觉得有趣,决定要研究一下卡马克弄出来的这个猜测值有什么奥秘。Lomont也是个牛人,在精心研究之后从理论上也推导出一个最佳猜 测值,和卡马克的数字非常接近, 0×5f37642f。卡马克真牛,他是外星人吗?传奇并没有在这里结束。Lomont计算出结果以后非常满意,于是拿自己计算出的起始值和卡马克的神秘数字做比赛,看看谁的数字能够更快更精确的求得平方根。结果是卡马克赢了… 谁也不知道卡马克是怎么找到这个数字的。最后Lomont采用暴力方法一个数字一个数字试过来,终于找到一个比卡马克数字要好上那么一丁点的数字, 虽然实际上这两个数字所产生的结果非常近似,这个暴力得出的数字是0×5f375a86。

扩展

对数实现

泰勒展开

ln ⁡ x = ln ⁡ [ ( 1 + m ) ⋅ 2 E − B ] = ln ⁡ ( 1 + m ) + ( E − B ) ln ⁡ 2 ln ⁡ ( 1 + m ) = ln ⁡ [ 2 × 2 2 × ( 1 + m ) ] = 1 2 ln ⁡ 2 + ln ⁡ [ 2 2 × ( 1 + m ) ] [ 2 2 × ( 1 + m ) ] ∈ [ 2 2 , 2 ] 泰勒展开 ln ⁡ ( 1 + m ) = 1 2 ln ⁡ 2 + Σ i = 1 ∞ [ ( − 1 ) i + 1 ⋅ 1 i ⋅ ( t − 1 ) i ] , 其中 t = 2 2 × ( 1 + m ) ln ⁡ x = 1 2 ln ⁡ 2 + Σ i = 1 ∞ [ ( − 1 ) i + 1 ⋅ 1 i ⋅ ( t − 1 ) i ] + ( E − B ) ln ⁡ 2 \ln x=\ln[(1+m)\cdot2^{E-B}]=\ln(1+m)+(E-B)\ln2\\ \ln(1+m)=\ln[\sqrt{2}\times\frac{\sqrt{2}}{2}\times(1+m)]=\frac{1}{2}\ln2+\ln[\frac{\sqrt{2}}{2}\times(1+m)]\\ [\frac{\sqrt{2}}{2}\times(1+m)]\in[\frac{\sqrt{2}}{2},\sqrt{2}]\\ 泰勒展开\\ \ln(1+m)=\frac{1}{2}\ln2+\Sigma_{i=1}^\infin[(-1)^{i+1}\cdot\frac{1}{i}\cdot(t-1)^i],\quad 其中t=\frac{\sqrt{2}}{2}\times(1+m)\\ \ln x=\frac{1}{2}\ln2+\Sigma_{i=1}^\infin[(-1)^{i+1}\cdot\frac{1}{i}\cdot(t-1)^i]+(E-B)\ln2\\ lnx=ln[(1+m)2EB]=ln(1+m)+(EB)ln2ln(1+m)=ln[2 ×22 ×(1+m)]=21ln2+ln[22 ×(1+m)][22 ×(1+m)][22 ,2 ]泰勒展开ln(1+m)=21ln2+Σi=1[(1)i+1i1(t1)i],其中t=22 ×(1+m)lnx=21ln2+Σi=1[(1)i+1i1(t1)i]+(EB)ln2

但由于泰勒展开的时间复杂度稍稍搞了一点,所以不是在此讨论的对象

快速获得近似值

log ⁡ 2 x = log ⁡ 2 ( 1 + m ) + E − B 如果 1 + m 越趋近于 1 ,那么结果越准确,越方便估算 不妨在此引入根号,反正基于上面的讨论,我们能尝试快速获得根号值 log ⁡ 2 x = 2 log ⁡ 2 1 + m + E − B 此时再进行近似操作,有 log ⁡ 2 x ≈ 2 ( 1 + m − 1 ) + E − B = 2 1 + m + E − B − 2 log ⁡ 2 x = 4 log ⁡ 2 1 + m + E − B log ⁡ 2 x ≈ 4 ( 1 + m − 1 ) + E − B = 4 1 + m + E − B − 4 \log_2x=\log_2(1+m)+E-B\\ 如果1+m越趋近于1,那么结果越准确,越方便估算\\ 不妨在此引入根号,反正基于上面的讨论,我们能尝试快速获得根号值\\ \log_2x=2\log_2\sqrt{1+m}+E-B\\ 此时再进行近似操作,有\\ \log_2x\approx2(\sqrt{1+m}-1)+E-B=2\sqrt{1+m}+E-B-2\\ \\ \\ \log_2x=4\log_2\sqrt{\sqrt{1+m}}+E-B\\ \log_2x\approx4(\sqrt{\sqrt{1+m}}-1)+E-B=4\sqrt{\sqrt{1+m}}+E-B-4 log2x=log2(1+m)+EB如果1+m越趋近于1,那么结果越准确,越方便估算不妨在此引入根号,反正基于上面的讨论,我们能尝试快速获得根号值log2x=2log21+m +EB此时再进行近似操作,有log2x2(1+m 1)+EB=21+m +EB2log2x=4log21+m +EBlog2x4(1+m 1)+EB=41+m +EB4

关于近似操作,当 x ∈ [ 1 , 2 ] x\in[1,2] x[1,2]时,总有 log ⁡ 2 x ≥ x − 1 \log_2x\geq x-1 log2xx1,也就是这一操作会得到偏小的值

在实际计算时,获得的根号值偏小,结果会比预计的值偏小,再次开根号获得的值依旧偏小,结果会比开一次根号的值更小,而再开一次根号的开销虽然依旧被缩小了,但还是比较大,所以实际计算中使用开一次根号的值即可

综上,这种方式只能获得整数部分准确的近似值,小数部分从第一位开始就不够准确了,但是我们可以尝试使用迭代方法将结果变准确

代码

实现

#pragma once

namespace math
{
    inline float __fastcall sqrt(float x) {
        const int magic = 0x1FB75A86;
        int bits = *(int*)&x;
        float y;

        bits = magic + (bits >> 1);
        y = *(float*)&bits;
        y = 0.5 * (y + x / y);
        y = 0.5 * (y + x / y);
        return y;
    }
    inline double __fastcall sqrt(double x) {
        // 这个值可以参考下方的值,来变得更精确
        const long long magic = 0x1FF75FFF00000000;
        long long bits = *(long long*)&x;
        double y;

        bits = magic + (bits >> 1);
        y = *(double*)&bits;
        y = 0.5 * (y + x / y);
        y = 0.5 * (y + x / y);
        y = 0.5 * (y + x / y);
        return y;
    }
    inline float __fastcall fastInvSqrt(float x) {
        const int magic = 0x5F375A86;
        const float threehalfs = 1.5, halfX = x * 0.5;
        unsigned int bits = *(unsigned int*)&x;
        float y;

        bits = magic - (bits >> 1);
        y = *(float*)&bits;
        y = y * (threehalfs - (halfX * y * y));
        return y;
    }
    inline double __fastcall rsqrt(double x) {
        const long long magic = 0x5FE6EB50C7B537AA;
        const double threehalfs = 1.5, halfX = x * 0.5;
        unsigned long long bits = *(unsigned long long*) & x;
        double y;

        bits = magic - (bits >> 1);
        y = *(float*)&bits;
        y = y * (threehalfs - (halfX * y * y));
        y = y * (threehalfs - (halfX * y * y));
        return y;
    }
    inline float __fastcall approximateLog2(float x) {
        unsigned int bits = *(unsigned int*)&x; // get bits
        unsigned char E = bits >> 23; // get E
        bits = ((bits << 9) >> 9) + 0x3F800000; // get 1 + m
        float t = *(float*)&bits; // put 1+m in float
        return 2.0f * sqrt(t) + E - 129; // \log_2x\approx2(\sqrt{1+m}-1)+E-B=2\sqrt{1+m}+E-B-2

        // no need to do
        // \log_2x\approx4(\sqrt{\sqrt{1+m}}-1)+E-B=4\sqrt{\sqrt{1+m}}+E-B-4
        // return 4.0f * fastSqrt(fastSqrt(t)) + E - 131;
    }
    inline double __fastcall approximateLog2(double x) {
        unsigned long long bits = *(unsigned long long*) & x;
        unsigned short E = bits >> 52;
        bits = ((bits << 12) >> 12) + 0x3FF0000000000000;
        double t = *(double*)&bits;
        return 2.0 * sqrt(t) + E - 1025;
    }
}

最快实现

最坏的情况下花费时间和上面差不多

// intel语法实现(Clang、ICC)
// GNU要使用AT&T语法,我不是很熟悉AT&T语法

namespace math {
    // C++哪有汇编快啊[doge]
    inline float __fastcall regSqrt(float x) {
    	__asm {
        	vsqrtss xmm0, xmm0, xmm0
        	vmovss x, xmm0
    	}
    	return x;
	}
    inline double __fastcall regSqrt(double x) {
    	__asm {
        	vsqrtsd xmm0, xmm0, xmm0
        	vmovsd x, xmm0
    	}
    	return x;
	}
    inline float regRSqrt(float x) {
        // SSE, AVX
        __asm {
            rsqrtss xmm0, xmm0
            vmovss x, xmm0
        }
        return x;
    }
    // 没有双精度浮点平方根的寄存器实现
}

不知道能不能直接使用内联汇编进行计算,感觉会有寄存器相关的竞争,保守一点还是调用函数)

  • 35
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值