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

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

[复制链接]
STMCU小助手 发布时间:2022-12-10 19:00
家介绍经典C语言平方根倒数速算法(或称Fast Inverse Square Root)。详细推导魔法数字0x5f3759df究竟从何而来,并最终在MCU上对比该算法和<math.h>中double sqrt(double)方法的性能表现并给出使用建议。本文不涉及高深的数学变换或复杂的计算机原理,但希望读者具备一些C语言基础;此外任何相关领域的积累都可能有助于内容理解。3 p) [& S# \% w% g
- ]  s) @, Y. T0 h( s1 L$ @
希望本文能帮助到有需要的朋友。
+ M5 e7 w' q, B; D2 P. ]6 h2 D
9 k9 z" A* o! }6 n平方根倒数简介2 `) _6 D: L# W2 |, S4 K% K% i
求平方根倒数在向量单位化操作中很常见,先举个例子,假设在二维平面内有一向量:
3 D% U' d8 ?5 @1 O3 h+ v


9 G' X" J* b) x8 U将该向量单位化,即将向量模长缩放为1,用向量除以它的模,可以表达成下式:
8 Y: z4 K/ x' U& w

  [1 ]8 o' N3 q$ F9 @
此处,单位向量的坐标,即是原向量坐标和模长倒数的乘积。( y0 x: R" B% z8 A
一般而言,对任一数x,

即被称作平方根倒数。9 Z# R0 F9 T) p6 |

/ T* ]( y7 t! `4 x9 {! `C语言内求平方根倒数' v8 n/ t  W9 c" p* O6 ?* [1 _

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

  1. #include <math.h>' G# |' n; c9 p! d! J
  2. 6 v( Q" a$ i9 i) H( T/ b' R) j
  3. float x =0;
    & S6 R. J; z; T) e8 u. s

  4. 4 o8 N" e' l- l7 J) g  j4 a; P
  5. float inverse_square_root =0;
    : _9 D% a: S0 B/ T, d

  6. ; |3 A  f8 D' |0 T* S: [
  7. inverse_square_root =1/sqrt(x);
复制代码

6 W1 h9 N7 T+ l+ J9 p3 ~* M5 p! s: j. e, R

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


2 g1 W. y$ _* h

速算法介绍
8 i% O; c" ^- ~& @( R9 w该情景与上世纪末PC算力较低的情况颇有几分相似,而在那段PC内存按MB计算的岁月里,优秀的C语言程序员们找到了该问题的解决方法:平方根倒数速算法(Fast Inverse Square Root),据说该算法最早由GaryTarolli在SGI Indigo开发中使用,对起源感兴趣的朋友可自行探究。. \1 x0 [# q+ N
平方根倒数速算法是一个单独的函数实现,代码如下:
  1. float FInvSqrtRoot(float x); B* ~9 X( U" e9 p: i* j* M

  2. ' X  P0 T  U% z3 Z, I3 {
  3. {0 r0 n7 Z& `/ }, @- O  t

  4. ; X2 z# N1 ]( M$ T& ~8 {
  5.     long i = 0;
    5 |" B5 ?. J' P+ Y
  6. & Q1 G7 H7 \& j8 Z9 k1 R  \
  7.     float y, halfx;! u4 }% z, {3 K' S1 D
  8. 6 s0 `! e) S% L; f! }8 y, k
  9.     halfx = 0.5 * x;2 m/ h5 \: C9 z

  10. 6 P; e% k) ?. d1 O4 ?5 d
  11.     i = *(long*) &x;                 // 按照整型方法操作浮点数
    " d- F! z) m, l0 X: G" h

  12. 4 O9 f7 [, {' y6 r
  13.     i = 0x5f3759df - (i >> 1);       // 魔法?
    : Q! C8 K+ A% V* m3 z1 L- c! q

  14. $ _" Y% i3 ?2 t
  15.     y = *(float*) &i;                // 获得平方根倒数近似值. h* b7 x& I) @6 [" e; m

  16. 2 [1 r# `6 i9 g/ {8 n9 s  L
  17.     y = y * (1.5 - halfx * y * y);   // 一次牛顿迭代/ j1 y- p+ V8 N. M
  18. ! x" n; J" K. T9 ^4 P9 F$ F2 M  y: h
  19.     return y;' a, T: W2 U' w1 U% J. s3 ~" \% m3 T
  20. 2 b9 H& Y9 g( K5 @. z
  21. }
复制代码
8 J! t, N; A5 v2 j# z5 w
# F2 a! b8 b" Z3 }1 ]& H# @1 t& f

. Q' N! l1 N% g) R

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


* H" K# R) K2 v) C

Step1.奇怪的地址类型转换

  1. i = *(long *) &x;                // 按照整型方法操作浮点数
复制代码

1 Q5 h/ u7 Y& i; H1 d* k

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

% @% ]( E* p* f, l; G


# }" O# {6 Q8 t* U7 S0 ]2 Y( W

最高位,第31位,为符号位,若S=0,该浮点数为正数;若S=1,该浮点数为负数。9 o& o: `$ \  Q  c) @& I
接下来的8位数字,为指数部分,指数E范围为[0,255],为可以表示负指数,规定在E的范围基础上-127,即[-127,128];例如E=3表示2^(3-127)=2^(-124)。. U7 r) e/ R: A
接下来的23位数字,为尾数部分,即有效数字部分。由于二进制下有效数字必为1.xxx的形式,整数部分必为1,因此无需使用第22位表示整数1,而是直接表示小数点后一位,整数部分不在尾数M中体现,只在计算时加上;即M=0101…表示二进制的1.0101。以上,float型浮点数在内存中的存放形式已经明确。7 R. u$ f7 D5 K- h& y9 q; [
现假设有一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);       // 魔法?
复制代码
) R; H1 f- Y- {6 U* s; w

要理解这行代码,需要一点点数学铺垫;我们先给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),如上图右侧所示:

所以有:

因此,


% O1 t$ _2 `* s/ A+ n9 a1 Y; v( I  I/ ~& V/ [6 s( F, A

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

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

令:

所以有:

将上式展开:

6 l$ M. _- @7 p. `

稍作整理:

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

而上式中

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

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

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

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

而上式中

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

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

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

  1. i = 0x5f3759df - (i >> 1);       // 魔法
复制代码
  [- [' W9 h. s8 u1 S. j

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

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

,也就是

即

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

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

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

( j; K7 {% R7 W

Step3.优化结果) ^: U7 Y( o5 a& w  i
由于上述推导中,引入了修正系数u,所以y只是近似解。
& w1 }% p% m4 E9 [1 S, n
% A7 c" ^1 Z* I接下来使用牛顿法(Newton Method)进行一次迭代,使y更接近准确值。简单来说,使用牛顿法,可以从一个近似解获得一个更接近准确答案的解;牛顿法的核心思想是迭代,对于我们正在讨论的问题,属于已知x求y的类型,即
6 N- M5 a- a6 Q% A8 U( `9 N7 E9 V5 b+ C+ p7 D% [


9 ^5 B4 ?$ ?1 q' Z8 h3 d* f/ g( Q& j, [2 z- w; F8 Q& k& n
假设已经获取一个近似解y1,则可以通过一次迭代求取更优解y2:
  g, X: k0 ?, v$ m6 w* ~0 O  U2 k' r. X( s1 a


/ v) W# `8 j5 ~% A( q  u
. c, e! \/ _  `/ R3 j6 G神奇的是计算y2仅需要乘法和减法运算,于是有如下代码:
# n) A) ?1 l2 w5 @" B
7 M4 O& V, I. p( V* s$ Yy = y * (1.5 - halfx * y * y);   // 一次牛顿迭代7 R7 \: D9 ^6 o  ]% f$ t/ _9 A& l
  w2 W* J+ i# H
该行代码返回优化后的平方根倒数,也就是最终运算结果。& A0 B5 @$ S, T2 _/ W
& O8 g3 K6 E2 i0 s
性能测试6 _# E: f' Y7 @. V3 f: y
接下来我会在STM32G071上测试Fast Inverse Square Root算法与<math.h>中提供的double sqrt(double)算法的性能,并尝试以此证明它为何被尊为经典。STM32G071运行在64MHz下,编译器和调试器优化分别为-O0和-g3。! n/ o/ t4 i* c/ a  `: W( Z  R
使用uint32_t替代long型:

+ k, g; O8 J/ H+ K, R

    , i3 t; ?% x9 c* d  m, t! ~5 A& V: P
    1. float FInvSqrtRoot(float x)# ~0 Y* T: n6 F" I/ v, t. X
    2. # q' C$ ^, q9 W( x+ K
    3. {
      / ~/ v3 Y3 |, D) y5 \

    4. * i7 `6 v; R: g- f9 g+ X& M3 i9 ^
    5.     uint32_t i = 0;
      8 v& u( J: a) k0 Q9 ]/ R

    6. / \4 f& p  y1 b+ L
    7.     float y, halfx;6 Z: {  p2 g- `. Q, E6 h" ^, W$ C) n
    8. : c6 J" h3 ?5 k* X# j
    9.     halfx = 0.5 * x;
      2 v. k, I. r- r) L3 `
    10. 7 m5 B2 W+ k5 P+ a
    11.     i = *(uint32_t*) &x;             // 按照整型方法操作浮点数
      8 F% U) v  p. i/ N/ w3 F
    12. - o  I$ x! b, w* I# ]+ o
    13.     i = 0x5f3759df - (i >> 1);       // 施放魔法
      $ S$ a% l$ t- Z+ B* u5 s+ I

    14. 6 X. S+ ?7 X- k# S! `1 V4 Z7 b
    15.     y = *(float*) &i;                // 获得平方根倒数近似值$ z' m) O$ p! G# U  R2 E1 M3 I0 J/ U

    16. ) `1 w& `) i# A/ G9 }% h% B8 r
    17.     y = y * (1.5 - halfx * y * y);   // 一次牛顿迭代) y9 G/ |# ?; y' x- O

    18. : i3 f  R% p+ h: t
    19.     return y;" |# m' f8 I( {+ O! S$ F; W
    20. 1 K( R! ~7 r% P  b
    21. }
    复制代码

/ x$ ~: b8 `& Z6 A) Q9 U7 X! I
0 L% I# c9 ?  z9 j9 h0 c/ b因为STM32G071的RAM大小为36KB,先构造了8000个float数据的数组,大小为32KB;构造方法如下:

    ) T6 s* I$ ^: _+ D+ }7 H2 T& J) t7 m6 J7 z: K; W
    , D0 \9 S/ j/ A
    1. float test_data[8000] ={ 0 };
      ! ~7 u' Z% N8 B: {7 A

    2. * ?0 E6 L4 \  @4 @7 P, \
    3. for (uint16_t i = 0; i < 8000; i++)
      - d3 r# ^+ ]" |* }* z, b
    4. " d7 R( f4 \0 j% j* ~
    5.   test_data[i] = i * 1000 + (float)i / 1000;
    复制代码

/ \6 `% ]6 C& n& G使用两种不同算法处理上述8000个数据,测试代码如下(注意此处仅作演示,测试方法不一定严谨):
0 r& Q; d2 [: z7 ?' J
/ J" K8 G1 I" c$ L, q$ E# W4 J7 f
  1. //平方根倒数速算法
    3 e; C, M0 B& O' g( {+ Z
  2. 9 y, w, w$ }1 o8 ], `5 s
  3. printf("FISR Test Start!\r\n");
    % f9 r2 _% G+ @& S! P2 }- P7 h
  4. 7 c% R( w  }% V! J3 B5 Y5 V
  5. ticks = 0;//定时器计数,1ms/tick2 \& g, F! Q' T6 B& `/ F; @. M
  6. / k6 @" K8 D* T* h
  7. HAL_TIM_Base_Start_IT(&htim1);! t& p7 Q/ d- A! V/ A( F
  8. 4 e3 E2 W: E9 R1 @. n7 T( ?$ E
  9. for (uint16_t i = 0; i < 8000; i++)4 A$ e* z- L9 k# ?: s6 T

  10. ! y; b: h2 ~: e, }- T
  11.   sroot=FInvSqrtRoot(test_data[i]);
    * ?$ v; D6 v  k# c$ X
  12. 2 F4 C/ Z$ ^, v/ p# d" A6 I7 b1 l2 ?
  13. HAL_TIM_Base_Stop_IT(&htim1);
    % @# H' X* q- a; L7 g/ c

  14. 4 a" d# J9 a/ Y! B1 e6 g
  15. printf("FISR Test Stop!\r\n");8 G* ]& v' L" f' F

  16. % D2 z0 a4 h! P- C0 S  ]+ q" U4 O  t
  17. printf("FISR Time Elapsed:%f\r\n", ((float) ticks) / 1000);
    + Q9 ?" w" n( P+ S  o# T

  18. + s- q8 _4 }- s. L2 B' Q
  19. 2 L9 R5 x& D: s2 u* ~
  20. / v7 ?9 N- K5 o5 {
  21. //传统1/sqrt()
      p! K- |" C2 f0 V/ c, m2 g

  22. # }9 v# R. V* _3 G3 U
  23. printf("SQRT Test Start!\r\n");
    0 {" J+ {- O5 p; u5 T3 q

  24. # [3 }( d1 h$ u
  25. ticks = 0;
    # C5 a6 W6 n2 e: y) R2 B$ v
  26. 5 u" ~7 w; Z8 D( A% Z; Q
  27. HAL_TIM_Base_Start_IT(&htim1);3 ^# ^" ~1 z, K+ {+ Y
  28. , f2 F* w; \1 ~, i/ A1 @
  29. for (uint16_t i = 0; i < 8000; i++)
    / ], l3 U, N. ^# X3 |

  30. # v2 T+ C3 Q  L- B6 a  B
  31.   sroot=1 / sqrt(test_data[i]);
    / Q! V1 f" L6 H; a
  32. + ?5 W1 T6 O  p
  33. HAL_TIM_Base_Stop_IT(&htim1);7 n1 W9 a6 e1 T# y8 D, T* p
  34. 1 r% w% n7 e2 p
  35. printf("SQRT Test Stop!\r\n");2 U  G8 }# P# @. v7 }2 j

  36. , W) f* d; i. w- i8 O4 j2 i0 W
  37. printf("SQRT Time Elapsed:%f\r\n", ((float) ticks) / 1000);
复制代码
. s! S+ ~+ ]0 J% M0 J
在上述测试环境下,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%以内。0 h/ I6 t( C+ n2 ~7 L# w
6 N( N; T7 l' N5 u0 h) U
魔改:平方根速算法3 |1 X9 N* P! t7 C* F/ e6 m1 @
根据上述原理,可将平方根倒数速算法修改为平方根速算法,代码如下:
6 R. Y/ |: |7 L
  P9 s, n7 P7 R4 k7 \
  1. float FSqrtRoot(float x) {
    $ y5 {' I/ n% O1 a
  2. ' b# o. c# I$ b" @9 f; G) x
  3.     uint32_t i = 0;1 V" }9 g5 O/ h
  4. ( ^& @& ]2 W; g0 h/ w/ [4 _% g! T
  5.     float y;/ H6 V. c! q$ z  G) ?  O8 J6 I

  6. " d2 D5 i' G( e) _4 D
  7.     i = *(uint32_t *) &x;            // 按照整型方法操作浮点数
    6 y' b# ]! C% E0 |; z8 s3 o
  8. 0 S. c4 L. O1 p- C. h/ e
  9.     i = 0x1fbd1df5 + (i >> 1);       // 施放魔法7 {4 |( v0 p3 Z8 A% p3 U! ?
  10. 4 j; i$ ^! q! |8 x
  11.     y = *(float *) &i;               // 获得平方根近似值
    0 r2 F  k2 [# V2 @! G) p0 f% Z

  12. " J2 H- A2 |: u, \0 e
  13.     y = 0.5 * (y + x / y);           // 一次牛顿迭代0 C5 R- P! r/ e" N2 ]) |* C% Q* X( [

  14. : e* U# b$ B8 v: j% f4 g
  15.     return y;& S( G) @. B& u7 }1 z

  16. # n* e- h9 F3 S3 R& s
  17. }
复制代码
7 r+ x, d  u- A2 B3 W! Z
在上述测试环境下,FSqrtRoot耗时109ms,sqrt()耗时221ms,FInvSqrtRoot比sqrt()快大约100%。由于sroot是float型,sqrt()返回double型,所以sqrt()测试时有额外的类型转换损耗;将sroot更改为double型,则类型转换损耗由FSqrtRoot承担;该状态下,FSqrtRoot耗时115ms,sqrt()耗时208ms,FInvSqrtRoot比sqrt()快大约80%。: K  l8 W$ g8 C6 ~( `/ `. y( u
感兴趣的朋友可自行推导实验,算法性能也请根据实际需求比对。0 R7 Q1 z/ K* D: V3 P8 N
---------------------- N& h# U+ A) |+ g- ^# I9 f
作者:Litthins
+ ^" G7 c9 k( P, o" f, c9 Z8 g
( N; K. |! L* H: R

4 A, {# l  i3 b
赞 收藏 评论0 发布时间:2022-12-10 19:00

举报

0个回答

所属标签

相似技术帖

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版