你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【经验之谈】基于STM32G071实战测试平方根倒数速算法FISR详解的经验分享

[复制链接]
STMCU小助手 发布时间:2022-12-10 19:00
家介绍经典C语言平方根倒数速算法(或称Fast Inverse Square Root)。详细推导魔法数字0x5f3759df究竟从何而来,并最终在MCU上对比该算法和<math.h>中double sqrt(double)方法的性能表现并给出使用建议。本文不涉及高深的数学变换或复杂的计算机原理,但希望读者具备一些C语言基础;此外任何相关领域的积累都可能有助于内容理解。
7 }  X( E# n+ _  }% u
2 J. H" S) r5 K( M希望本文能帮助到有需要的朋友。
0 m( }, A" j& B
7 j% _) D1 h$ Z  m( S9 P* V平方根倒数简介9 j7 _0 k" s) y1 y2 J
求平方根倒数在向量单位化操作中很常见,先举个例子,假设在二维平面内有一向量:
/ i- r' ?2 O6 Z/ X4 @


5 x7 ~: t( G- |# L' W, W1 [将该向量单位化,即将向量模长缩放为1,用向量除以它的模,可以表达成下式:
. h3 l+ K. Z. W  I


) y+ }% Q: s7 t此处,单位向量的坐标,即是原向量坐标和模长倒数的乘积。
8 _* L! H" E# I7 ]: [' q9 m一般而言,对任一数x,

即被称作平方根倒数。6 n' ~  T; r# X
" H2 i7 @  \* x! s9 D
C语言内求平方根倒数, i8 Z* B% ?# n/ Y: j

C语言内求平方根属于常规操作,因为<math.h>中已经提供了double sqrt(double)函数给大家调用,该函数接收一个double型变量,并返回其平方根。部分场合使用float类型,如果是我,我会这么写:

  1. #include <math.h>
    % y, G4 G1 I" A8 X8 W# y8 M$ S! E

  2.   D. x6 D$ Q+ i& V
  3. float x =0;
    4 r; s6 v% Z& ~7 L% X

  4. 9 a: `  a3 r* _7 g- Q2 Z1 [
  5. float inverse_square_root =0;3 j9 l( h% S9 W6 |5 ~; k' H

  6. 2 l% _; e, G9 {/ w; h) T" [( j: t
  7. inverse_square_root =1/sqrt(x);
复制代码
0 q  h8 C) P- _, i

) c( a1 B! l1 H; d

事实上,大多数计算机处理加法和乘法时更快,对于除法和开方运算,速度相对较慢。定义在<math.h>中的sqrt()方法其系统开销是比较大的,尤其在部分MCU上,有时不能很好地满足系统实时性要求。


6 n* k6 B, j" D7 A0 U! w3 ^: y

速算法介绍! u$ A8 A+ g" T
该情景与上世纪末PC算力较低的情况颇有几分相似,而在那段PC内存按MB计算的岁月里,优秀的C语言程序员们找到了该问题的解决方法:平方根倒数速算法(Fast Inverse Square Root),据说该算法最早由GaryTarolli在SGI Indigo开发中使用,对起源感兴趣的朋友可自行探究。
+ }( d  L9 ^- N/ \( N3 i平方根倒数速算法是一个单独的函数实现,代码如下:
  1. float FInvSqrtRoot(float x)/ }4 x* j, ^% f9 n

  2. ! K1 u. C/ K7 s% \$ @
  3. {
    4 H8 [2 r6 Y" U- E; b0 a. G

  4. , s( [  ^3 N- ?2 l1 P
  5.     long i = 0;! X( H& `; f9 U$ k

  6.   ~# {# O, g+ v
  7.     float y, halfx;* f, A/ O4 W) G& z

  8. 8 m8 k- T& _. D; P/ R5 }
  9.     halfx = 0.5 * x;* {* _5 g9 U2 S2 t$ ~

  10. ) m+ E2 p, H$ V0 ~. S( v
  11.     i = *(long*) &x;                 // 按照整型方法操作浮点数# T8 n+ c0 |. N: @: a" W# {
  12. 0 w- ?* ^/ N( i* l( e% V
  13.     i = 0x5f3759df - (i >> 1);       // 魔法?( @- {! m5 M# `& X

  14. - ?! t+ G3 i# o+ a6 [* j' N# d
  15.     y = *(float*) &i;                // 获得平方根倒数近似值( ]9 W& ]/ f6 {5 c
  16. ) T8 k9 |4 x- o6 Z" v: b
  17.     y = y * (1.5 - halfx * y * y);   // 一次牛顿迭代- {6 i' O! V* p2 L- p7 \  d
  18. * `+ T& Q, ]1 P$ I. j5 ?8 T. c7 n
  19.     return y;9 q# b$ r  b# D- g' T
  20. 4 z! |4 ^5 B: n, M
  21. }
复制代码

& i0 r6 }' i3 S# v9 J) E' }: W. P$ z7 H: k


8 V% R( f. h! h# o

该函数接收一个float型变量,并返回其平方根倒数。

! e4 u& {# I, r+ V# U

Step1.奇怪的地址类型转换

  1. i = *(long *) &x;                // 按照整型方法操作浮点数
复制代码
3 B1 P% b( z' I$ h4 c: W! y1 E

代码第四行将浮点数x按照整型数据操作,这里的操作方法与类型转换有很大不同;类型转换将内存中按照浮点数标准表示的值近似为最接近的整数,并按照整数标准储存在内存中,像这样:i=(long)x;要理解*(long *) &x与(long)x的不同,需要了解浮点数在计算机中的表示方法。: ~  g! K: S: k' J- R) ]% M
浮点数在计算机中使用类似科学计数法的形式储存,float类型的浮点数使用32bit表示,结构如下:


3 Z8 r. m1 G5 V. Z: L


2 T1 z( y  c; `+ ]+ y

最高位,第31位,为符号位,若S=0,该浮点数为正数;若S=1,该浮点数为负数。
1 C* h: y+ g8 g, [0 ?4 K接下来的8位数字,为指数部分,指数E范围为[0,255],为可以表示负指数,规定在E的范围基础上-127,即[-127,128];例如E=3表示2^(3-127)=2^(-124)。) U1 L2 V* f, R! i" M
接下来的23位数字,为尾数部分,即有效数字部分。由于二进制下有效数字必为1.xxx的形式,整数部分必为1,因此无需使用第22位表示整数1,而是直接表示小数点后一位,整数部分不在尾数M中体现,只在计算时加上;即M=0101…表示二进制的1.0101。以上,float型浮点数在内存中的存放形式已经明确。5 Q- J8 |7 z: K/ N* Z4 a3 x' a
现假设有一float型浮点正数x,其在内存中按照上述方式存放,即:

则x表示的浮点数值为:

注意平方根倒数速算法只接收正数,所以全文只讨论x为正浮点数的情况。

此时再回来看i=*(long *) &x这行代码;

第一步;取x的地址,注意此时x的地址指向一个浮点数,内存中对应的数据为:E*2^23+M

第二步;将该地址转换为指向long整型的地址,注意转换的是地址类型而不是数据,现在内存中的数据仍然是:E*2^23+M

第三步,取该地址指向的数据,赋给i;此时程序将按照整型标准去读取这个数据。因此,可以将i理解为x对应内存数据的整型解释。现在可以对i进行整型支持的操作,比如接下来要用到的位移操作。

准备工作已经完成,现在进入算法的核心,即施放魔法:

  1. i = 0x5f3759df - (i >> 1);       // 魔法?
复制代码
( s4 F% u: m: F  D) r: z0 O5 V0 x

要理解这行代码,需要一点点数学铺垫;我们先给x表示的浮点数取个对数:

其中,M表示尾数,M/2^23范围在[0,1)之间;在极限理论中,若x属于[0,1],有以下关系:

即在x接近0或1的条件下,log2(1+x)接近x;很明显当x不靠近0或1时,误差会变大,如下图左侧所示:

如果在等式右侧加入一个微小量u,可以使得x+u在[0,1]范围内近似log2(1+x),如上图右侧所示:

所以有:

因此,

; n) x8 {- K) R+ ]

/ }8 ?+ S/ m* z' ?9 _) n

此时再倒回去看浮点数x在内存中的存放形式,即:E*2^23+M;

因此可以认为浮点数的对数形式与其内存存放形式相似,只是添加了缩放和平移。我们知道:

令:

所以有:

将上式展开:

& }5 K9 S6 m5 q0 e% t* ?6 ~/ t4 P

稍作整理:

这太棒了,因为浮点数在内存中的存放形式,即:E*2^23+M

而上式中

则相当于对其内存形式右移1位,即

由此可知,魔法数字0x5f3759df就是包含u在内的修正项:

至此我们已经完成了魔法代码的解析:

这太棒了,因为浮点数在内存中的存放形式,即:E*2^23+M

而上式中

则相当于对其内存形式右移1位,即

由此可知,魔法数字0x5f3759df就是包含u在内的修正项:

至此我们已经完成了魔法代码的解析:

  1. i = 0x5f3759df - (i >> 1);       // 魔法
复制代码
5 r. X+ M8 Y( k5 e4 n

i就是x对应内存数据的整型解释,将i右移1位相当于乘以二分之一,也就是1/2*(Ex*2^23+Mx);

等式右侧的运算结果,就是

,也就是

注意此时程序认为i是整型变量,所以需要下一行代码再将其转换为浮点型;

y = *(float *) &i;              // 获得平方根倒数估计值

此时y即是x的平方根倒数。

* y6 p9 y3 p9 u! }

Step3.优化结果
* p+ M+ o9 H% x% q由于上述推导中,引入了修正系数u,所以y只是近似解。
  @/ B+ L1 X  w* d# E2 G
. |% N' M/ V9 H9 d* I# A: x- o& Q接下来使用牛顿法(Newton Method)进行一次迭代,使y更接近准确值。简单来说,使用牛顿法,可以从一个近似解获得一个更接近准确答案的解;牛顿法的核心思想是迭代,对于我们正在讨论的问题,属于已知x求y的类型,即
% q  B/ i, A5 o" j1 r" {% ]# N4 T
; G. B1 M+ a, ]+ R7 s# u$ O

0 g1 u2 F- r6 j% h3 J2 L& Y

* h6 P- t- m4 p' N假设已经获取一个近似解y1,则可以通过一次迭代求取更优解y2:6 ]* B$ Q6 Y) A) ~# s+ A. t( D- [

# W. g6 q, E2 D0 M* |0 i  F1 A

0 N: J" \) k- K5 r
' ~: S% m/ t# a
神奇的是计算y2仅需要乘法和减法运算,于是有如下代码:
+ N8 o: j4 H' U, d: \) Y  O4 F4 a% N: [5 ^, Y, \/ ~- Q
y = y * (1.5 - halfx * y * y);   // 一次牛顿迭代* }/ G) k5 r+ _' u& [7 U. [/ O
4 [1 o' [% S5 j' Z4 N1 _- N% p! `, x7 b3 o4 E
该行代码返回优化后的平方根倒数,也就是最终运算结果。5 O- m. ?7 ?( K( c
& D5 O% e: A3 u$ _( l
性能测试( z! h9 u7 j; j
接下来我会在STM32G071上测试Fast Inverse Square Root算法与<math.h>中提供的double sqrt(double)算法的性能,并尝试以此证明它为何被尊为经典。STM32G071运行在64MHz下,编译器和调试器优化分别为-O0和-g3。% R' H  W8 m+ {6 |+ o
使用uint32_t替代long型:
, O; ]! i. X) Q1 p
    ' ~# ~9 s/ ?7 c! B
    1. float FInvSqrtRoot(float x)( @% G; ?) ]2 q* k) U& E

    2. ! `+ C* a; n4 Z; `* t( Z( ^
    3. {
      ' y+ ?% V' F9 }+ @' J7 Z3 @

    4. " K8 [' N- m4 U+ J+ t$ g
    5.     uint32_t i = 0;6 Q+ R, s# M* p" t  c  W# f
    6. 3 _- M7 G8 T& U7 t) T
    7.     float y, halfx;7 A: X0 k; C( _0 x2 L

    8. + i7 g% T/ q( i4 l, H+ I( w+ b2 d
    9.     halfx = 0.5 * x;
      , o3 T* ~2 M0 t3 \6 C3 j

    10. # y: x) V9 L* S* c
    11.     i = *(uint32_t*) &x;             // 按照整型方法操作浮点数7 H  w/ V* }( a% R3 q, ]' n3 `( f
    12. % ^, l* V  O- I& q! h
    13.     i = 0x5f3759df - (i >> 1);       // 施放魔法
      : w/ b3 A# G2 {& `9 e2 |  A

    14. 0 ~9 _4 d! @9 _( D+ c
    15.     y = *(float*) &i;                // 获得平方根倒数近似值
      . a" D' F8 \3 {8 x, J$ }

    16. " ], Y7 L: A# b0 I  D
    17.     y = y * (1.5 - halfx * y * y);   // 一次牛顿迭代
      6 u0 P1 T  h: j( W" t

    18. , [/ l8 b' f! e$ Z
    19.     return y;
      % d( o. p- H. |# W5 \0 N! r# ?

    20. 4 L! ]6 y) ]2 Q( }
    21. }
    复制代码
/ e' T( r! G; G4 X9 i2 ~
7 o2 R+ v/ Z5 y: y4 N' q+ J  d8 }
因为STM32G071的RAM大小为36KB,先构造了8000个float数据的数组,大小为32KB;构造方法如下:
    , e" `  Z+ l* r2 _$ v6 c' N
    2 F7 g# V! \$ R; h+ i4 M
    / R; x9 [2 z0 |( j: _) A
    1. float test_data[8000] ={ 0 };
      * R$ S( Z; Z0 T) _5 T

    2. 1 G% S# ^" u5 m* d$ F. c
    3. for (uint16_t i = 0; i < 8000; i++)# i6 ?+ p" w1 K& L$ s! N
    4. ! F4 Z) j) S& O4 P9 C  }9 `2 K. }
    5.   test_data[i] = i * 1000 + (float)i / 1000;
    复制代码
4 P! l, S4 [! s0 n
使用两种不同算法处理上述8000个数据,测试代码如下(注意此处仅作演示,测试方法不一定严谨):
. @/ y' r4 S" B( D+ |
' H( F4 s9 r/ v  E
  1. //平方根倒数速算法
    & m5 G9 |$ l! l2 k
  2. 7 s- L1 Z4 R& o
  3. printf("FISR Test Start!\r\n");
    1 g5 O# a/ T/ y' f3 v$ @8 ~' s6 ^, t

  4. 5 w& N( [& o' r- [
  5. ticks = 0;//定时器计数,1ms/tick
    - a7 M1 t4 G, [, P9 |, N  d

  6. 9 r, ^: i1 x5 e4 W, {/ r
  7. HAL_TIM_Base_Start_IT(&htim1);
    ; K* O  Z* u; K. a* F
  8. + q- k# z' I* M2 x. C$ T5 H
  9. for (uint16_t i = 0; i < 8000; i++)- F6 b6 M( n1 ?0 O8 |* U3 p  [, e! E0 l
  10. 4 R4 v& \* r4 C) |' }' D% i
  11.   sroot=FInvSqrtRoot(test_data[i]);2 |7 t% r- K0 K1 e. ~* t

  12. & R6 t5 O! D2 ^
  13. HAL_TIM_Base_Stop_IT(&htim1);) F/ D5 }8 R9 W, p! Q; |
  14. 0 `* V: n1 X/ w  }
  15. printf("FISR Test Stop!\r\n");9 U7 Z, A& g/ \! B1 T
  16. : {9 j& h, B; x9 w
  17. printf("FISR Time Elapsed:%f\r\n", ((float) ticks) / 1000);
    ; |6 n5 ?% _" B  e5 l2 N, M% Q
  18. , {- D# {. L  l5 H& j5 U3 q1 d4 w

  19. & u0 I( d( Y. v+ }. K- h
  20. , z+ i7 j& Z; Y& R% C6 Q  g2 N
  21. //传统1/sqrt()
    + G! d  [1 B/ x8 i0 z
  22. 1 `( i  i5 N6 h( _, j
  23. printf("SQRT Test Start!\r\n");' G1 v. p  [( }+ G+ O2 h& k1 p* A

  24. 4 ?& V) e! y  H7 x! A' J
  25. ticks = 0;
    # _5 `7 O) `5 i( \2 z
  26. 1 S( Q& h( i! P0 ?1 V$ \0 u
  27. HAL_TIM_Base_Start_IT(&htim1);) }9 h; A8 Z0 q# Z) R8 a  B9 h
  28. $ R/ y) e! I3 l6 j* S
  29. for (uint16_t i = 0; i < 8000; i++)/ h7 H9 g! m, J" c" H
  30. / [1 Y/ K# [! O) \
  31.   sroot=1 / sqrt(test_data[i]);/ x' T  A6 c# k1 h0 X' a( s' I: \6 V

  32. 1 \9 |2 G. C3 t* q  y
  33. HAL_TIM_Base_Stop_IT(&htim1);
    ; T1 L0 \, x7 e& {* {4 u  s& A! x4 y

  34. 9 t0 o& {3 B& r
  35. printf("SQRT Test Stop!\r\n");* T3 @( U: ^. z, b- m) y$ @0 s) S
  36. " b  K% F3 ^' |4 `9 }( x. ~) w' K
  37. printf("SQRT Time Elapsed:%f\r\n", ((float) ticks) / 1000);
复制代码
6 f# |4 T. J4 \1 X2 X
在上述测试环境下,FInvSqrtRoot耗时181ms,1 / sqrt()耗时325ms,FInvSqrtRoot比1 / sqrt()快大约79%。由于sroot是float型,sqrt()返回double型,所以1 / sqrt()测试时有额外的类型转换损耗;将sroot更改为double型,则类型转换损耗由FInvSqrtRoot承担;该状态下,FInvSqrtRoot耗时187ms,1 / sqrt()耗时315ms,FInvSqrtRoot比1 / sqrt()快大约68%。注意FInvSqrtRoot求取的仅是平方根倒数的近似值,和1 / sqrt()比对,误差约在1%以内。
3 ?2 |0 `0 S) c0 J* a! I" E+ G, O
$ d) P. O) S0 L
魔改:平方根速算法
0 p- C0 l, b1 D9 D根据上述原理,可将平方根倒数速算法修改为平方根速算法,代码如下:& l; ?/ e% C8 G" V) M  x
  }* g( S8 @$ p) B5 o# |& y* A6 K
  1. float FSqrtRoot(float x) {( K; m* x; p) G* V1 R9 ?; t* q2 P
  2. + x$ H; k3 F1 ^. X$ ?
  3.     uint32_t i = 0;2 G' C; N; Z% O2 I

  4. & E- ~4 k  J8 n* u( \  j0 }
  5.     float y;2 [% g4 Q- s: ?. U' \: d
  6. " l& F+ D. l# [: j1 a9 h# a
  7.     i = *(uint32_t *) &x;            // 按照整型方法操作浮点数  [# l( L  a# z9 n( \

  8. - R- N8 @$ W0 |# w) l
  9.     i = 0x1fbd1df5 + (i >> 1);       // 施放魔法0 G$ h( n. V8 E, o7 ^) P
  10. / i* a8 h8 @( {$ e
  11.     y = *(float *) &i;               // 获得平方根近似值- q2 U, h6 G( n& U$ R: y
  12. 1 U$ \3 B7 d" x+ Y; K: Y8 {" a
  13.     y = 0.5 * (y + x / y);           // 一次牛顿迭代6 o0 a  o. ?9 g  }0 b, R9 `4 A: l

  14. / \$ X+ @. |$ {$ m
  15.     return y;
    ' A- K. p, r- z, c1 d$ P: s9 y( p! y: R
  16. , K0 S. X" z- P9 w+ j" i
  17. }
复制代码
. [* ]# s# |: H" ?) x# L6 o
在上述测试环境下,FSqrtRoot耗时109ms,sqrt()耗时221ms,FInvSqrtRoot比sqrt()快大约100%。由于sroot是float型,sqrt()返回double型,所以sqrt()测试时有额外的类型转换损耗;将sroot更改为double型,则类型转换损耗由FSqrtRoot承担;该状态下,FSqrtRoot耗时115ms,sqrt()耗时208ms,FInvSqrtRoot比sqrt()快大约80%。- l) j" {9 L& q- X/ \
感兴趣的朋友可自行推导实验,算法性能也请根据实际需求比对。
2 W) @# F$ v& P& m; z---------------------
% G. u2 z0 `# {. d7 m! d6 Q, |( O作者:Litthins

; H! N- l6 X  j
" X( v- K; k4 W- j* _4 K: \6 g; K/ H6 ^2 H, z
收藏 评论0 发布时间:2022-12-10 19:00

举报

0个回答

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版