简而言之
如果已经看到很多小的Byte数组,如果没有存储它们并移动到下一批,则需要检查它们。这同时发生。当元素超过100万时,HashSet会产生奇迹,但会完全崩溃(每个数组都可以产生0,1或n个后继者)。我们对删除元素不感兴趣,只是跟踪。什么数据结构足够灵活,具有良好的性能并可由多个线程使用?
长
对于这个项目,我们需要存储大量某些状态的字节数组,以便跟踪我们看到的数组和不看的数组。该项目是在.NET框架的帮助下在C#中完成的。实际的程序是一个控制台应用程序。挑战在于使单线程参考解决方案成为更快的多线程参考解决方案。
最初他们使用Trie数据结构来存储所有先前的状态,但我们发现它在使用多个线程时表现不佳。相反,我们现在使用带有简单锁定的HashSet,以防我们想要写入它。我们发现它与this FNV散列函数,#34; Fowler / Noll / Vo(FNV)32位散列函数"一起工作得非常好。与单线程参考实现相比,性能提升了大约300%。
失败的最坏情况是:
修改 我们在System.Collections.Concurrent中尝试了这些集合,问题在于我们在大多数集合中获得的性能。有些提供太多,有些提供太少。理想情况下,我们只存储唯一的哈希,因此我们最终不会有700万字节的数组。这就是我们使用HashSet的原因,它为这个应用程序提供了令人难以置信的性能,但是当增加成倍增长时,速度会慢下来。
一些实际运行数据:
使用HashSet,可以为上述两种情况产生以下结果:
所以我们在8.43倍的时间内完成了大约9.49倍的工作量,这是一些好的缩放(略小于线性)。但不够。
使用ConcurrentDictionary(值为字节0),我们得到以下结果:
使用ConcurrentBag我们得到以下结果:
在这种情况下,HashSet是明显的赢家。还有一些运行:
重要的是要知道,在查看这些数字时,后续者的生成可能会失败(哈哈)。上述情况旨在发现我们计划中可能存在的错误。
答案 0 :(得分:1)
从概念上讲,HashSet听起来像是你想要做的很好的匹配,但.NET的实现有一个致命的缺陷:它不会让你设置你的初始容量。 (例如,与C ++的ordered_set
不同,它允许您在构造时指定存储桶计数)。因此,当您反复使用该系列的容量时,您的大部分时间都花在了重复上。奇怪的是他们不允许你这样做,因为reference source中的评论表明调整大小会伤害。
因此,让我们测量调整大小/重新散列对您造成多大伤害(使用8字节数组,粗略估计最坏情况):
static void Main(string[] args)
{
const int COUNT = 66478557;
const int UNIQUE_COUNT = 59018056;
// create a bunch of 8-byte arrays:
var arrays = new List<byte[]>(COUNT);
for (long i = 0; i < COUNT; ++i)
arrays.Add(BitConverter.GetBytes(i % UNIQUE_COUNT));
// the HashSet we'll be abusing (i'll plug in a better comparer later):
var hs = new HashSet<byte[]>(EqualityComparer<byte[]>.Default);
//var hs = new HashSet<byte[]>(new ByteArrayComparer());
var sw = Stopwatch.StartNew();
for (int i = 0; i < COUNT; ++i)
hs.Add(arrays[i]);
sw.Stop();
Console.WriteLine("New HashSet: " + sw.Elapsed.TotalMilliseconds);
// clear the collection (doesn't reset capacity):
hs.Clear();
// Do the adds again, now that the HashSet has suitable capacity:
sw.Restart();
for (int i = 0; i < COUNT; ++i)
hs.Add(arrays[i]);
sw.Stop();
Console.WriteLine("Warmed HashSet: " + sw.Elapsed.TotalMilliseconds);
}
我在具有足够容量的“热身”哈希集上显示了近2倍的加速:
New HashSet: 27914.5131
Warmed HashSet: 17683.5115
(顺便说一句,这是在运行笔记本电脑级i5的英特尔NUC上。)
好的,现在让我们加快哈希实现:
class ByteArrayComparer : IEqualityComparer<byte[]>
{
public int GetHashCode(byte[] obj)
{
long myLong = BitConverter.ToInt64(obj, 0);
// just XOR's upper and lower 4 bytes:
return myLong.GetHashCode();
}
private EqualityComparer<byte[]> _defaultComparer = EqualityComparer<byte[]>.Default;
public bool Equals(byte[] a1, byte[] a2)
{
return _defaultComparer.Equals(a1, a2);
}
}
结果:
New HashSet: 5397.449
Warmed HashSet: 2013.0509
......获得更大的胜利!
那么你的应用程序有什么方法可以在你的收藏中做这样的热身吗?否则,您可能需要考虑创建/查找允许您配置初始容量的HashSet实现。
答案 1 :(得分:0)
根据数据的分布情况,您可能会考虑保留Trie方法,但基于第一个字节(或其他更好分布的字节进行分区,使用一些重新排序将其置于第一个&#39; in Trie),对“分区字节”的每个值都有一个单独的锁。如果您选择的字节分布均匀,这将大大减少锁争用,因为大多数情况下您的各种线程将访问不同的独立Tries。