在上篇随笔“浅谈 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); } } }
测试结果如下所示:
首先使用 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'));
然后重新运行测试程序,结果如下:
结果用时从 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); } }
下面是优化之前测试结果:
优化之后:
可以看出,调用 ToString 方法将一个有一亿位数字的整数输出,耗时从原来的 99.41 秒下降到优化后的 10.12 秒,效果很是显著。不但如此,优化后的 ToString 方法内存占用也大为减少。