14

Add出于某种原因,对 a的操作似乎HashSetContains在元素已经存在于HashSet.

这是证据:

    Stopwatch watch = new Stopwatch();
    int size = 10000;
    int iterations = 10000;


    var s = new HashSet<int>();
    for (int i = 0; i < size; i++) {
        s.Add(i);
    }

    Console.WriteLine(watch.Time(() =>
    {
        for (int i = 0; i < size; i++) {
            s.Add(i);
        }
    }, iterations));

    s = new HashSet<int>();
    for (int i = 0; i < size; i++) {
        s.Add(i);
    }

    // outputs: 47,074,764

    Console.WriteLine(watch.Time(() =>
    {
        for (int i = 0; i < size; i++) {
            if (!s.Contains(i))
                s.Add(i);
        }
    }, iterations));

    // outputs: 41,125,219

为什么ContainsAdd现有元素更快?

注意:我正在使用Stopwatch另一个 SO question 的扩展。

    public static long Time(this Stopwatch sw, Action action, int iterations) {
        sw.Reset();
        sw.Start();
        for (int i = 0; i < iterations; i++) {
            action();
        }
        sw.Stop();

        return sw.ElapsedTicks;
    }

更新:内部测试表明,大的性能差异仅发生在 x64 版本的 .NET 框架上。使用 32 位版本的框架 Contains 似乎以相同的速度运行(事实上,在某些测试运行中,带有 contains 的版本似乎运行速度慢了一个百分点)在 X64 版本的框架上,带有 contains 的版本似乎运行速度快约 15%。

4

3 回答 3

11

AddIfNotPresent 执行包含不执行的附加除法。看一下包含的 IL:

IL_000a:  call       instance int32 class System.Collections.Generic.HashSet`1<!T>::InternalGetHashCode(!0)
  IL_000f:  stloc.0
  IL_0010:  ldarg.0
  IL_0011:  ldfld      int32[] class System.Collections.Generic.HashSet`1<!T>::m_buckets
  IL_0016:  ldloc.0
  IL_0017:  ldarg.0
  IL_0018:  ldfld      int32[] class System.Collections.Generic.HashSet`1<!T>::m_buckets
  IL_001d:  ldlen
  IL_001e:  conv.i4
  IL_001f:  rem
  IL_0020:  ldelem.i4
  IL_0021:  ldc.i4.1
  IL_0022:  sub
  IL_0023:  stloc.1

这是计算哈希码的存储桶位置。结果保存在本地内存位置 1。

AddIfNotPresent 做了类似的事情,但它也将计算的值保存在位置 2,以便如果项目不存在,它可以将项目插入到该位置的哈希表中。这样做会保存,因为稍后在查找该项目的循环中修改了其中一个位置。无论如何,这是 AddIfNotPresent 的相关代码:

IL_0011:  call       instance int32 class System.Collections.Generic.HashSet`1<!T>::InternalGetHashCode(!0)
  IL_0016:  stloc.0
  IL_0017:  ldloc.0
  IL_0018:  ldarg.0
  IL_0019:  ldfld      int32[] class System.Collections.Generic.HashSet`1<!T>::m_buckets
  IL_001e:  ldlen
  IL_001f:  conv.i4
  IL_0020:  rem
  IL_0021:  stloc.1
  IL_0022:  ldarg.0
  IL_0023:  ldfld      int32[] class System.Collections.Generic.HashSet`1<!T>::m_buckets
  IL_0028:  ldloc.0
  IL_0029:  ldarg.0
  IL_002a:  ldfld      int32[] class System.Collections.Generic.HashSet`1<!T>::m_buckets
  IL_002f:  ldlen
  IL_0030:  conv.i4
  IL_0031:  rem
  IL_0032:  ldelem.i4
  IL_0033:  ldc.i4.1
  IL_0034:  sub
  IL_0035:  stloc.2

无论如何,我认为额外的划分是导致 Add 比 Contains 花费更多时间的原因。乍一看,似乎可以排除额外的差异,但如果不花更多时间破译 IL,我不能肯定地说。

于 2009-03-09T23:12:51.300 回答
1

有趣的是,在我的机器(Dell Latitude D630,双核 2.2 Ghz)上,除非我在null测试前针对某个动作运行秒表,否则我在两个测试中得到的结果几乎相同。例如:

我使用您在问题中给出的确切代码运行测试:

Without Contains(): 8205794
With Contains():    8207596

如果我以这种方式修改代码:

后:

Stopwatch watch = new Stopwatch();
int size = 10000;
int iterations = 10000;

添加:

watch.Time(null, 0);

我的结果变成:

Without Contains(): 8019129
With Contains():    8275771

在我看来,这似乎是在Stopwatch导致这些波动的内部发生了一些奇怪的事情。

于 2009-03-09T22:20:38.693 回答
1

我的猜测是您从 Visual Studio 运行了测试,导致AddIfNotPresentinto的内联Add被抑制,因此您在方法调用中看到了额外级别的间接结果。

如果我从命令行编译并运行以删除任何 VS 诡计...

> csc /o+ /t:exe Program.cs
> Program.exe

...那么没有性能差异。

样本输出(代表大量测试):

35036174
35153818

35225763
34862330

35047377
35033323
于 2009-03-09T22:29:40.200 回答