【问题标题】:Fastest way to populate a Hashset填充哈希集的最快方法
【发布时间】:2013-12-10 22:45:08
【问题描述】:

我需要定期遍历大量对象并维护其中特定字符串属性的唯一值。

我正在使用 Hashset 来保存唯一值,但想知道检查 Hashset 中是否存在值是否更有效,或者只是尝试添加所有值?

【问题讨论】:

    标签: c# performance hashset


    【解决方案1】:

    由于Jon Hanna 所述的原因,您的测试是一个糟糕的测试,并且没有给您准确的结果。当您在内部调用 Add 时,HashSet 会调用 AddIfNotPresent,而 AddIfNotPresent 所做的第一件事是检查对象是否存在(代码来自 ILSpy)

    public bool Add(T item)
    {
        return this.AddIfNotPresent(item);
    }
    
    private bool AddIfNotPresent(T value)
    {
        if (this.m_buckets == null)
        {
            this.Initialize(0);
        }
        int num = this.InternalGetHashCode(value);
        int num2 = num % this.m_buckets.Length;
        int num3 = 0;
        for (int i = this.m_buckets[num % this.m_buckets.Length] - 1; i >= 0; i = this.m_slots[i].next)
        {
            if (this.m_slots[i].hashCode == num && this.m_comparer.Equals(this.m_slots[i].value, value))
            {
                return false;
            }
            num3++;
        }
        //(Snip)
    

    因此,通过执行Contains 然后Add,您可以检查对象是否存在两次。如果存储桶中有很多项目,它正在检查这可能会导致显着的性能损失。

    【讨论】:

    • 是的,我的第二次尝试与您的解释一致,推断 Add() 大约占用了调用 Contains() 时间的 107%。
    【解决方案2】:

    由于我最初的答案通常被嘲笑,我又试了一次。

    Int32 maxUniques = 1;
    Int32 collectionSize = 100000000;
    Random rand = new Random();
    
    while (maxUniques <= collectionSize)
    {
        List<Int32> bigCollection = new List<Int32>();
        bigCollection.Capacity = collectionSize;
    
        for (Int32 count = 0; count < collectionSize; count++)
            bigCollection.Add(rand.Next(maxUniques));
    
        HashSet<Int32> uniqueSources = new HashSet<Int32>();
    
        Stopwatch watch = new Stopwatch();
        watch.Start();
    
        foreach (Int32 num in bigCollection)
        {
            if (!uniqueSources.Contains(num))
                uniqueSources.Add(num);
        }
    
        Console.WriteLine(String.Format("With {0,10:N0} unique values in a set of {1,10:N0} values, the time taken for conditional add: {2,6:N0} ms", uniqueSources.Count, collectionSize, watch.ElapsedMilliseconds));
    
        uniqueSources = new HashSet<Int32>();
        watch.Restart();
    
        foreach (Int32 num in bigCollection)
        {
            uniqueSources.Add(num);
        }
    
        Console.WriteLine(String.Format("With {0,10:N0} unique values in a set of {1,10:N0} values, the time taken for simple add:      {2,6:N0} ms", uniqueSources.Count, collectionSize, watch.ElapsedMilliseconds));
        Console.WriteLine();
    
        maxUniques *= 10;
    }
    

    它给出了以下输出:

    在一组 100,000,000 个值中有 1 个唯一值,条件加法所需的时间:2,004 毫秒 在一组 100,000,000 个值中有 1 个唯一值,简单加法所需的时间:2,540 毫秒

    在一组 100,000,000 个值中有 10 个唯一值,条件加法所需的时间:2,066 毫秒 在一组 100,000,000 个值中使用 10 个唯一值,简单加法所需的时间:2,391 毫秒

    在一组 100,000,000 个值中有 100 个唯一值,条件加法所需的时间:2,057 毫秒 在一组 100,000,000 个值中有 100 个唯一值,简单加法所需的时间:2,410 毫秒

    在一组 100,000,000 个值中有 1,000 个唯一值,条件加法所需的时间:2,011 毫秒 在一组 100,000,000 个值中有 1,000 个唯一值,简单加法所需的时间:2,459 毫秒

    在一组 100,000,000 个值中有 10,000 个唯一值,条件加法所需的时间:2,219 毫秒
    在一组 100,000,000 个值中有 10,000 个唯一值,简单加法所需的时间:2,414 毫秒

    在一组 100,000,000 个值中有 100,000 个唯一值,条件加法所需的时间:3,024 毫秒
    在一组 100,000,000 个值中有 100,000 个唯一值,简单加法所需的时间:3,124 毫秒

    在一组 100,000,000 个值中有 1,000,000 个唯一值,条件加法所需的时间:8,937 毫秒
    在一组 100,000,000 个值中有 1,000,000 个唯一值,简单相加所需的时间:9,310 毫秒

    在一组 100,000,000 个值中有 9,999,536 个唯一值,条件加法所需的时间:11,798 毫秒
    在一组 100,000,000 个值中有 9,999,536 个唯一值,简单相加所需的时间:11,660 毫秒

    在一组 100,000,000 个值中有 63,199,938 个唯一值,条件加法所需的时间:20,847 毫秒
    在一组 100,000,000 个值中有 63,199,938 个唯一值,简单加法所需的时间:20,213 毫秒

    这对我来说很好奇。

    最多 1% 的添加,调用 Contains() 方法比一直按 Add() 方法更快。对于 10% 和 63%,只使用 Add() 会更快。

    换一种说法:
    1 亿次 Contains() 比 9900 万次 Add() 快
    1 亿次 Contains() 比 9000 万次 Add() 慢

    我调整了代码,尝试以 100 万个增量尝试 100 万到 1000 万个唯一值,发现拐点在 7-10% 左右,结果不是结论性的。

    因此,如果您预计要添加的值少于 7%,则首先调用 Contains() 会更快。超过 7%,只需调用 Add()。

    【讨论】:

    • 如果您先致电SimpleAdd,比率是否保持不变?
    • 我自己跑了一遍,我很惊讶,即使我在没有调试器的情况下执行发布模式,我也得到了和你一样的结果,切换测试顺序也是如此。但是我不相信 7% 是一个硬性和快速的数字,THashSet&lt;T&gt; 中使用的 GetHashCode 函数的质量和速度将极大地影响它翻转的点。同样换个说法,当您尝试插入的所有项目中有 93% 已存在于集合中时,调用 Contains 会更快;我很难想到许多非人为的例子,我会有如此高的重复率。
    【解决方案3】:

    当我输入问题时,有人会问我为什么不自己测试它。所以我自己测试了一下。

    我创建了一个包含 126 万条记录和 21 个唯一源字符串的集合,并通过以下代码运行它:

    HashSet<String> uniqueSources = new HashSet<String>();
    
    Stopwatch watch = new Stopwatch();
    watch.Start();
    
    foreach (LoggingMessage mess in bigCollection)
    {
        uniqueSources.Add(mess.Source);
    }
    
    Console.WriteLine(String.Format("Time taken for simple add: {0}ms", watch.ElapsedMilliseconds));
    
    uniqueSources.Clear();
    watch.Restart();
    
    foreach (LoggingMessage mess in bigCollection)
    {
        if (!uniqueSources.Contains(mess.Source))
            uniqueSources.Add(mess.Source);
    }
    
    Console.WriteLine(String.Format("Time taken for conditional add: {0}ms", watch.ElapsedMilliseconds));
    

    结果是:

    简单加法耗时:147ms

    条件加法耗时:125ms

    因此,至少对于我的数据而言,检查是否存在并不会减慢速度,它实际上会稍微快一些。不过,无论哪种方式,差异都非常小。

    【讨论】:

    • 这不是一个很好的测试。 1.你没有尝试足够的项目,因为147ms不足以隐藏噪音,第一次jittingHashSet&lt;string&gt;.Add的成本等等。 2. 您调用Clear() 将清空集合,但使内部表比第一次创建时更大,因此它不必增长,这是.Add 的最大成本之一。
    • 您还希望在一系列命中/未命中率范围内运行测试,因为在 1% 的新增中最有效的方法可能与 100% 的新增非常不同。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-02
    • 2012-03-30
    • 2021-04-08
    • 2012-06-12
    相关资源
    最近更新 更多