银河

SKYIV STUDIO

  博客园 :: 首页 :: 博问 :: 闪存 :: :: :: 订阅 订阅 :: 管理 ::
  268 随笔 :: 2 文章 :: 2606 评论 :: 48 引用

公告

在上篇随笔“浅谈 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.5341352
Simple 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.5217181
Simple 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 100000000
ToString Digits:    100,000,000  Elapsed:  99.4138987
  Fast HashCode:  1,470,973,525  Elapsed:   0.2504019
Simple HashCode:  1,470,973,525  Elapsed:   0.2944836

优化之后:

E:\CS\BigInteger> BigInteger 100000000
ToString Digits:    100,000,000  Elapsed:  10.1196895
  Fast HashCode:  1,470,973,525  Elapsed:   0.2530704
Simple HashCode:  1,470,973,525  Elapsed:   0.2957576

可以看出,调用 ToString 方法将一个有一亿位数字的整数输出,耗时从原来的 99.41 秒下降到优化后的 10.12 秒,效果很是显著。不但如此,优化后的 ToString 方法内存占用也大为减少。

 

参考资料:

  1. 再谈 BigInteger - 使用快速傅里叶变换
  2. 浅谈 BigInteger
  3. 浅谈 GetHashCode
posted on 2010-04-22 00:56 银河 阅读(...) 评论(...) 编辑 收藏