文章目錄
在上篇隨筆“淺談 GetHashCode”中,我實現了一個新的 Skyiv.Numeric.BigInteger.GetHashCode 方法:
public override int GetHashCode(){ int n = sign; for (int i = data.Length - 1; i >= 0; i -= 4) { int m = data[i]; if (i > 0) m |= (data[i - 1] << 8); if (i > 1) m |= (data[i - 2] << 16); if (i > 2) m |= (data[i - 3] << 24); n = m ^ (n + (n << 5) + (n >> 0x1b)); } return n * 0x5d588b65;}
上述代碼比較優雅,但是在 for 迴圈中的三個 if 語句其實可以提到 for 迴圈外面的,如下所示:
public override int GetHashCode(){ int n = sign, i; for (i = data.Length - 1; i >= 4; i -= 4) n = (data[i] | (data[i - 1] << 8) | (data[i - 2] << 16) | (data[i - 3] << 24)) ^ (n + (n << 5) + (n >> 27)); if (i >= 0) { int m = data[i]; if (i > 0) m |= (data[i - 1] << 8); if (i > 1) m |= (data[i - 2] << 16); if (i > 2) m |= (data[i - 3] << 24); n = m ^ (n + (n << 5) + (n >> 27)); } return n * 0x5d588b65;}
雖然以上代碼不那麼優雅,有重複的“壞味道”,但是效率更高了。我們來實際測試一下吧:
using System;using System.Diagnostics;using BigInteger = Skyiv.Numeric.BigInteger;namespace Skyiv{ class TestMain { static void Main(string[] args) { try { var stopwatch = Stopwatch.StartNew(); var s = new string('7', (args.Length > 0) ? int.Parse(args[0]) : 1000000); var n = BigInteger.Parse(s); stopwatch.Stop(); WriteLine("Parse Digits", s.Length, stopwatch.Elapsed); TestGetHashCode(n, true); TestGetHashCode(n, false); } catch (Exception ex) { Console.WriteLine(ex); } } static void TestGetHashCode(BigInteger n, bool isFast) { var stopwatch = Stopwatch.StartNew(); var hash = n.GetHashCode(isFast); stopwatch.Stop(); WriteLine(isFast ? "Fast HashCode" : "Simple HashCode", hash, stopwatch.Elapsed); } static void WriteLine(string item, int value, TimeSpan span) { Console.WriteLine("{0,15}: {1,14:N0} Elapsed: {2,11:F7}", item, value, span.TotalSeconds); } }}
測試結果如下所示:
E:\CS\BigInteger> BigInteger 600000000 Parse Digits: 600,000,000 Elapsed: 254.8271638 Fast HashCode: 353,915,569 Elapsed: 1.5341352Simple HashCode: 353,915,569 Elapsed: 1.7766916
首先使用 BigInteger.Parse 方法產生一個有六億位元字的整數(用時 254.83 秒),然後:
- 調用新的 GetHashCode 方法,傳回值是 353,915,569,用時 1.53 秒。
- 調用舊的 GetHashCode 方法,傳回值是 353,915,569,用時 1.77 秒。
可見,新的 GetHashCode 方法的效率的確更高,但是提高的幅度也不多,而且還損失了代碼的優雅性。
怎麼 BigInteger.Parse 方法耗時這麼多?去看看來源程式:
public static BigInteger Parse(string s){ if (s == null) return null; if (s.Length == 0) return 0; BigInteger z = new BigInteger(); z.sign = (sbyte)((s[0] == '-') ? -1 : 1); if (s[0] == '-' || s[0] == '+') s = s.Substring(1); int r = s.Length % Len; z.data = new byte[s.Length / Len + ((r != 0) ? 1 : 0)]; int i = 0; if (r != 0) z.data[i++] = byte.Parse(s.Substring(0, r)); for (; i < z.data.Length; i++, r += Len) z.data[i] = byte.Parse(s.Substring(r, Len)); z.Shrink(); return z;}
這個 Parse 方法中耗時最多的就是 for 迴圈,於是,改寫這個 for 迴圈,用簡單的數學計算代替費時的 byte.Parse 方法,如下所示:
for (; i < z.data.Length; i++, r += Len) z.data[i] = (byte)((s[r] - '0') * 10 + (s[r + 1] - '0'));
然後重新運行測試程式,結果如下:
E:\CS\BigInteger> BigInteger 600000000 Parse Digits: 600,000,000 Elapsed: 11.0663091 Fast HashCode: 353,915,569 Elapsed: 1.5217181Simple HashCode: 353,915,569 Elapsed: 1.7744348
結果用時從 254.83 秒下降到 11.07 秒,效率得到了極大地提高,而且代碼還保持了優雅。這是一次非常成功的最佳化。
這兩次測試的 CPU 佔用和記憶體使用量情況如下所示:
接著,我們來看看 ToString 方法:
public override string ToString(){ var sb = new StringBuilder(); if (sign < 0) sb.Append('-'); sb.Append((data.Length == 0) ? 0 : (int)data[0]); for (var i = 1; i < data.Length; i++) sb.Append(data[i].ToString("D" + Len)); return sb.ToString();}
這也可以最佳化如下:
public override string ToString(){ if (data.Length == 0) return "0"; var sb = new StringBuilder(Length, Length); sb.Length = Length; var k = 0; if (sign < 0) sb[k++] = '-'; if (data[0] >= 10) sb[k++] = (char)(data[0] / 10 + '0'); sb[k++] = (char)(data[0] % 10 + '0'); for (var i = 1; i < data.Length; i++) { sb[k++] = (char)(data[i] / 10 + '0'); sb[k++] = (char)(data[i] % 10 + '0'); } return sb.ToString();}public int Length{ get { return (data.Length == 0) ? 1 : (((sign < 0) ? 1 : 0) + ((data[0] < 10) ? -1 : 0) + data.Length * Len); }}
下面是最佳化之前測試結果:
E:\CS\BigInteger> BigInteger 100000000ToString Digits: 100,000,000 Elapsed: 99.4138987 Fast HashCode: 1,470,973,525 Elapsed: 0.2504019Simple HashCode: 1,470,973,525 Elapsed: 0.2944836
最佳化之後:
E:\CS\BigInteger> BigInteger 100000000ToString Digits: 100,000,000 Elapsed: 10.1196895 Fast HashCode: 1,470,973,525 Elapsed: 0.2530704Simple HashCode: 1,470,973,525 Elapsed: 0.2957576
可以看出,調用 ToString 方法將一個有一億位元字的整數輸出,耗時從原來的 99.41 秒下降到最佳化後的 10.12 秒,效果很是顯著。不但如此,最佳化後的 ToString 方法記憶體佔用也大為減少。
參考資料:
- 再談 BigInteger - 使用快速傅裡葉變換
- 淺談 BigInteger
- 淺談 GetHashCode