【发布时间】:2013-12-10 22:45:08
【问题描述】:
我需要定期遍历大量对象并维护其中特定字符串属性的唯一值。
我正在使用 Hashset 来保存唯一值,但想知道检查 Hashset 中是否存在值是否更有效,或者只是尝试添加所有值?
【问题讨论】:
标签: c# performance hashset
我需要定期遍历大量对象并维护其中特定字符串属性的唯一值。
我正在使用 Hashset 来保存唯一值,但想知道检查 Hashset 中是否存在值是否更有效,或者只是尝试添加所有值?
【问题讨论】:
标签: c# performance hashset
由于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,您可以检查对象是否存在两次。如果存储桶中有很多项目,它正在检查这可能会导致显着的性能损失。
【讨论】:
由于我最初的答案通常被嘲笑,我又试了一次。
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,比率是否保持不变?
T 在HashSet<T> 中使用的 GetHashCode 函数的质量和速度将极大地影响它翻转的点。同样换个说法,当您尝试插入的所有项目中有 93% 已存在于集合中时,调用 Contains 会更快;我很难想到许多非人为的例子,我会有如此高的重复率。
当我输入问题时,有人会问我为什么不自己测试它。所以我自己测试了一下。
我创建了一个包含 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
因此,至少对于我的数据而言,检查是否存在并不会减慢速度,它实际上会稍微快一些。不过,无论哪种方式,差异都非常小。
【讨论】:
HashSet<string>.Add的成本等等。 2. 您调用Clear() 将清空集合,但使内部表比第一次创建时更大,因此它不必增长,这是.Add 的最大成本之一。