引言
在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,只有最后一位可能有误差。