【问题标题】:HashSet performance Add vs Contains for existing elementsHashSet 性能 Add vs Contains 现有元素
【发布时间】:2009-03-09 21:42:53
【问题描述】:

由于某种原因,当元素已经存在于HashSet 中时,HashSet 上的Add 操作似乎比Contains 操作慢。

这是证据:

    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 对已经存在的元素更快?

注意:我正在使用来自另一个 SO 问题的 Stopwatch 扩展名。

    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%。

【问题讨论】:

  • 对于 32 位测试,你是在 32 位机器上运行,还是在虚拟机上运行,​​还是通过指定 x86 目标来编译和运行 WOW64 下的 32 位框架?我不知道这会有所不同,但这是可能的。
  • 我在虚拟机中进行了 32 位测试
  • 您是否尝试过将测试包含添加功能移到添加上方然后测量时间?你会发现差异可以忽略不计..
  • 在我的测试用例中,移动这两种方法后,Add 块有时比 Contains Add 运行得更快。

标签: c# performance hashset


【解决方案1】:

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,我不能肯定地说。

【讨论】:

  • 一个有趣的问题是为什么这在 x64 上这么慢?
【解决方案2】:

有趣的是,在我的机器(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 内部发生了一些奇怪的事情,导致了这些波动。

【讨论】:

  • 天哪,如果你不能相信秒表,你还能相信谁 :) 我只是重复了所有的测试,得到的结果与我原来的一致,在发布模式下差距甚至更大。
  • 这可能与我拥有 X64 版本的框架有关,在 VM 中进行测试
  • 确认问题似乎与 X64 相关
【解决方案3】:

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

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

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

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

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

35036174
35153818

35225763
34862330

35047377
35033323

【讨论】:

  • 不,刚刚在 VS 之外的版本中运行,我发现 contains 的速度要快 15%
  • 我的机器有一个很大的不同是,我运行的是 Vista X64,可以在 32 位的虚拟机中快速测试
  • 另外我的机器是四核的,这可能意味着 GC 的运行方式
  • 我现在使用的是 Vista x86 双核。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-15
  • 2015-03-26
  • 2017-03-31
  • 2015-10-16
  • 1970-01-01
  • 2020-08-10
相关资源
最近更新 更多