计算自然对数的快速算法

时间:2023-01-15 18:35:40

引言

在1982年,Tateaki. Sasaki 和 Yasumasa Kanada 发表了一篇论文:Practically Fast Multiple-Precision Evaluation of LOG(x)。在这篇只有四页的论文中,他们介绍了一个计算自然对数的快速算法。

实现该算法的 C# 程序

我们知道,.NET Framework Class Library 中的 System.Math.Log 方法只能对 dobule 数据类型计算自然对数。现在我们为 decimal 数据类型实现一个 Log 扩展方法,如下所示:

 1 using System;
2
3 namespace Skyiv.Extensions
4 {
5 static class DecimalExtensions
6 {
7 static readonly decimal pi2 = 6.283185307179586476925286766559m;
8 static readonly decimal ln10 = 2.30258509299404568401799145468m;
9 static readonly decimal eps2 = 0.00000000000001m; // 1e-14
10 static readonly decimal eps1 = eps2 * eps2; // 1e-28
11
12 public static decimal Sqrt(this decimal x)
13 {
14 if (x < 0) throw new ArgumentException("Must be nonnegative");
15 if (x == 0) return 0;
16 var y = (decimal)Math.Sqrt((double)x);
17 return (y + x / y) / 2;
18 }
19
20 public static decimal Log10(this decimal x)
21 {
22 return Log(x) / ln10;
23 }
24
25 public static decimal Log(this decimal x)
26 {
27 if (x <= 0) throw new ArgumentException("Must be positive");
28 if (x == 1) return 0;
29 var k = 0;
30 for (; x > 0.1m; k++) x /= 10;
31 for (; x <= 0.01m; k--) x *= 10;
32 return k * ln10 - NegativeLog(x);
33 }
34
35 static decimal NegativeLog(decimal q)
36 { // q in ( 0.01, 0.1 ]
37 decimal r = q, s = q, n = q, q2 = q * q, q1 = q2 * q;
38 for (var p = true; (n *= q1) > eps1; s += n, q1 *= q2)
39 r += (p = !p) ? n : -n;
40 decimal u = 1 - 2 * r, v = 1 + 2 * s, t = u / v;
41 decimal a = 1, b = Sqrt(1 - t * t * t * t);
42 for (; a - b > eps2; b = Sqrt(a * b), a = t) t = (a + b) / 2;
43 return pi2 / (a + b) / v / v;
44 }
45 }
46 }

在上述程序中:

  • 第 12 至 18 行实现 Sqrt 扩展方法。使用牛顿迭代法,仅迭代一次 (第 17 行)。这是因为第 16 行使用 Math.Sqrt (double) 方法给出的初值的精度已经达到 15,而 decimal 的精度为 28,迭代一足够了。
  • 第 25 至 33 行实现 Log 扩展方法。该方法先将参数 x 变换到 ( 0.01, 0.1 ] 区间中 ( 第 29 至 31 行),然后调用 NegativeLog 方法进行计算。
  • 第 35 至 44 行的 NegativeLog 方法就是我们算法的核心,使用 椭圆θ函数 ( 第 37 至 40 行 ) 和 算术几何平均法 ( 第 41 至 43 行 ) 来快速计算自然对数。该算法的原理请参阅参考资料[1]。
  • 第 7 行是事先计算出来的 2π 的值,在第 43 行中使用。
  • 第 8 行是事先计算出来的 ln10 的值,在第 32 行和 22 行中使用。
  • 我们的程序中使用 ln10 的值,而参考资料[1]中使用 ln2 的值。这是因为 decimal 使用十进制比较好。而一般的 double 使用二进制比较好。
  • 第 10 行和第 9 行的 eps1 = 10-28 和 eps2 = 10-14 使用十进制控制计算精度。而参考资料[1]中使用二进制控制计算精度,即 2-p 和 2-p/2

算法的性能

在上述程序中加入一些调试语句,就可以在程序运行时报告一些重要变量的值,结果如下所示:

q : 0.1000000000000000000000000000 k: 2
r : 0.0999000009999999000000001000
s : 0.1001000010000001000000001000
u : 0.8001999980000001999999998000
v : 1.2002000020000002000000002000 loop1: 4
b0: 0.8957696680606997488130397041
a : 0.9471678269798758062616205879
b : 0.9471678269798660936897543331 loop2: 3
NL: 2.3025850929940456840179914544
Ln: 2.3025850929940456840179914550

q : 0.0100000000000000000000000001 k: 28
r : 0.0099999900000000010000000001
s : 0.0100000100000000010000000001
u : 0.9800000199999999979999999998
v : 1.0200000200000000020000000002 loop1: 2
b0: 0.3845443947629648387969403232
a : 0.6556979528724023734005530455
b : 0.6556979528723956496570849678 loop2: 4
NL: 4.6051701859880913680359828988
Ln: 59.867212417845187784467777833

上面是对 10 和 100000000000000000000000001 分别调用 Log 方法计算自然对数的调试结果:

  • k 是 Log 方法执行到第 32 行时的值。其他变量都是第 35 至 44 行中 NegativeLog 方法执行时的值。
  • loop1 是第 38 至 39 行的 for 循环的执行次数。
  • loop2 是第 42 行的 for 循环执行次数。
  • q, r, s, u, v, a, b 都是计算终了时的值。
  • b0 是 b 的初值 (第 41 行)。a 的初值是 1 。
  • NL 是 NegativeLog 方法的返回值,即参数 q 的自然对数的相反数。
  • Ln 是 Log 方法的返回值,即参数 x 的自然对数。
  • 从上述调试结果可以看出,这个算法是非常高效的。算法中的两个 for 循环执行次数一般都不超过 4。

测试程序

下面是调用 decimal 数据类型的 Sqrt、Log 和 Log10 扩展方法的测试程序:

 1 using System;
2 using Skyiv.Extensions;
3
4 class Tester
5 {
6 static void Main()
7 {
8 foreach (var x in new decimal[] { 4 / decimal.MaxValue,
9 0.0000001m, 0.1m, 1, 10, 100000000, decimal.MaxValue })
10 {
11 Console.WriteLine("x : " + x);
12 Console.WriteLine("sqrt: " + x.Sqrt());
13 Console.WriteLine("ln : " + x.Log());
14 Console.WriteLine("lg : " + x.Log10());
15 Console.WriteLine();
16 }
17 }
18 }

运行结果如下所示:

work$ dmcs Tester.cs DecimalExtensions.cs
work$ mono Tester.exe
x : 0.0000000000000000000000000001
sqrt: 0.00000000000001
ln : -64.472382603833279152503760731
lg : -28.000000000000000000000000000

x : 0.0000001
sqrt: 0.0003162277660168379331998894
ln : -16.118095650958319788125940182
lg : -6.9999999999999999999999999996

x : 0.1
sqrt: 0.3162277660168379331998893545
ln : -2.3025850929940456840179914544
lg : -0.9999999999999999999999999999

x : 1
sqrt: 1
ln : 0
lg : 0

x : 10
sqrt: 3.1622776601683793319988935445
ln : 2.3025850929940456840179914550
lg : 1.0000000000000000000000000001

x : 100000000
sqrt: 10000
ln : 18.420680743952365472143931638
lg : 8.000000000000000000000000000

x : 79228162514264337593543950335
sqrt: 281474976710656.00000000000000
ln : 66.542129333754749704054283660
lg : 28.898879583742194740518933893

从中可以看出,这个算法的运算精度能够达到 27,只有最后一位可能有误差。

参考资料

  1. CiNii Articles: Practically Fast Multiple-Precision Evaluation of LOG(X)
  2. Wikipedia: Natural logarithm
  3. Wikipedia: Arithmetic-geometric mean
  4. Wikipedia: Newton's method
  5. MSDN: Decimal 结构 (System)