How can I improve this square root method?
我知道这听起来像是一个家庭作业,但事实并非如此。最近,我对用于执行某些数学运算的算法很感兴趣,例如正弦、平方根等。目前,我正在尝试用C语言编写计算平方根的巴比伦方法。
到目前为止,我有:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public static double SquareRoot(double x) { if (x == 0) return 0; double r = x / 2; // this is inefficient, but I can't find a better way // to get a close estimate for the starting value of r double last = 0; int maxIters = 100; for (int i = 0; i < maxIters; i++) { r = (r + x / r) / 2; if (r == last) break; last = r; } return r; } |
它工作得很好,每次都会产生与.NET框架的math.sqrt()方法完全相同的答案。不过,正如您可能猜测的那样,它比本机方法慢(大约慢800个节拍)。我知道这个特定的方法永远不会比本机方法更快,但是我只是想知道是否有任何优化我可以做。
我立即看到的唯一优化是计算将运行100次,即使在确定了答案之后(此时,r始终是相同的值)。所以,我添加了一个快速检查,看看新计算的值是否与先前计算的值相同,并中断循环。不幸的是,它在速度上没有太大的差别,但似乎是正确的选择。
在你说"为什么不直接用math.sqrt()来代替呢?"…我这样做是为了学习,并不打算在任何生产代码中实际使用这个方法。
首先,不是检查等式(r==last),而是检查收敛性,其中r接近last,其中close由任意epsilon定义:
1 2 | eps = 1e-10 // pick any small number if (Math.Abs(r-last) < eps) break; |
正如你链接到的维基百科文章提到的那样——你不能用牛顿的方法有效地计算平方根——相反,你使用对数。
1 2 3 4 5 6 7 | float InvSqrt (float x){ float xhalf = 0.5f*x; int i = *(int*)&x; i = 0x5f3759df - (i>>1); x = *(float*)&i; x = x*(1.5f - xhalf*x*x); return x;} |
这是我最喜欢的快速平方根。实际上,它是平方根的倒数,但是如果你想倒数,你可以在后面倒数……我不能说如果你想要平方根而不是倒数平方根,它是否更快,但是它还是很酷。http://www.beyond3d.com/content/articles/8/
你要做的是执行牛顿的求根方法。所以你可以使用一些更有效的根查找算法。你可以在这里开始搜索。
使用您的方法,每次迭代都会使正确位的数目加倍。
使用一个表来获取初始的4位(例如),第一次迭代后将有8位,第二次迭代后将有16位,第四次迭代后需要的所有位(因为
对于表查找,可以从输入中提取[0.5,1]中的尾数和指数(使用frexp之类的函数),然后规范化[64256]中的尾数[使用适当的2的乘方。
1 2 | mantissa *= 2^K exponent -= K |
在此之后,您的输入编号仍然是
编辑。我在下面放了一些C++代码来说明这一点。带改进测试的op函数sr1需要2.78s来计算2^24平方根;我的函数sr2需要1.42s,硬件sqrt需要0.12s。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | #include <math.h> #include <stdio.h> double sr1(double x) { double last = 0; double r = x * 0.5; int maxIters = 100; for (int i = 0; i < maxIters; i++) { r = (r + x / r) / 2; if ( fabs(r - last) < 1.0e-10 ) break; last = r; } return r; } double sr2(double x) { // Square roots of values in 0..256 (rounded to nearest integer) static const int ROOTS256[] = { 0,1,1,2,2,2,2,3,3,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,11,11,11,11,11, 11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,12,12,12,12,12,12,12,12,12,12,12,12, 12,12,12,12,12,12,12,12,12,12,12,12,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13, 13,13,13,13,13,13,13,13,13,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14, 14,14,14,14,14,14,14,14,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15, 15,15,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16 }; // Normalize input int exponent; double mantissa = frexp(x,&exponent); // MANTISSA in [0.5,1[ unless X is 0 if (mantissa == 0) return 0; // X is 0 if (exponent & 1) { mantissa *= 128; exponent -= 7; } // odd exponent else { mantissa *= 256; exponent -= 8; } // even exponent // Here MANTISSA is in [64,256[ // Initial value on 4 bits double root = ROOTS256[(int)floor(mantissa)]; // Iterate for (int it=0;it<4;it++) { root = 0.5 * (root + mantissa / root); } // Restore exponent in result return ldexp(root,exponent>>1); } int main() { // Used to generate the table // for (int i=0;i<=256;i++) printf(",%.0f",sqrt(i)); double s = 0; int mx = 1<<24; // for (int i=0;i<mx;i++) s += sqrt(i); // 0.120s // for (int i=0;i<mx;i++) s += sr1(i); // 2.780s for (int i=0;i<mx;i++) s += sr2(i); // 1.420s } |
将除法2替换为位移位不太可能产生如此大的差异;考虑到除法是一个常量,我希望编译器足够聪明,可以为您做到这一点,但您也可以尝试看看。
通过提前退出循环,您更有可能获得改进,因此要么将新的r存储在变量中并与旧的r进行比较,要么将x/r存储在变量中并在执行加法和除法之前将其与r进行比较。
您可以返回r,而不是中断循环,然后返回r。可能不会显著提高性能。
将"/2"替换为"*0.5"会使我的计算机速度提高约1.5倍,但当然速度不如本机实现快。
为了学习的目的,我也一直在研究这个问题。你可能对我试过的两个修改感兴趣。
第一种方法是在X0中使用一阶泰勒级数近似:
1 2 3 4 5 6 7 8 9 10 11 12 13 | Func<double, double> fNewton = (b) => { // Use first order taylor expansion for initial guess // http://www27.wolframalpha.com/input/?i=series+expansion+x^.5 double x0 = 1 + (b - 1) / 2; double xn = x0; do { x0 = xn; xn = (x0 + b / x0) / 2; } while (Math.Abs(xn - x0) > Double.Epsilon); return xn; }; |
第二个是尝试三阶(更昂贵),迭代
1 2 3 4 5 6 7 8 9 10 11 | Func<double, double> fNewtonThird = (b) => { double x0 = b/2; double xn = x0; do { x0 = xn; xn = (x0*(x0*x0+3*b))/(3*x0*x0+b); } while (Math.Abs(xn - x0) > Double.Epsilon); return xn; }; |
我创建了一个助手方法来为函数计时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public static class Helper { public static long Time( this Func<double, double> f, double testValue) { int imax = 120000; double avg = 0.0; Stopwatch st = new Stopwatch(); for (int i = 0; i < imax; i++) { // note the timing is strictly on the function st.Start(); var t = f(testValue); st.Stop(); avg = (avg * i + t) / (i + 1); } Console.WriteLine("Average Val: {0}",avg); return st.ElapsedTicks/imax; } } |
最初的方法更快,但同样有趣的是:)
既然您说下面的代码不够快,请尝试以下操作:
1 2 3 4 | static double guess(double n) { return Math.Pow(10, Math.Log10(n) / 2); } |
它应该非常准确,希望速度快。
这里是这里描述的初始估计的代码。看起来不错。使用这段代码,然后您还应该迭代,直到值在差异的epsilon内收敛。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | public static double digits(double x) { double n = Math.Floor(x); double d; if (d >= 1.0) { for (d = 1; n >= 1.0; ++d) { n = n / 10; } } else { for (d = 1; n < 1.0; ++d) { n = n * 10; } } return d; } public static double guess(double x) { double output; double d = Program.digits(x); if (d % 2 == 0) { output = 6*Math.Pow(10, (d - 2) / 2); } else { output = 2*Math.Pow(10, (d - 1) / 2); } return output; } |
定义一个公差,并在随后的迭代落在该公差范围内时尽早返回。
好吧,本机sqrt()函数可能没有用c_实现,它很可能是用一种低级语言实现的,而且肯定会使用一种更有效的算法。因此,尝试匹配它的速度可能是徒劳的。
但是,对于只为heckuvit优化函数,您链接的维基百科页面建议"开始猜测"为2 ^层(d/2),其中d表示数字中的二进制位数。您可以尝试一下,我看不出还有多少其他可以在您的代码中进行显著优化的地方。
你可以试试
而不是/2(也可以在另一个地方按2)。它可能会给你一点优势。我也会把
现在检查一下。
编辑:修正了>到>的问题,但它不适用于双打,所以永远不要。这100辆车的内衬没有给我提速。