C# 在C语言中同时存储和读取大量小元素#

C# 在C语言中同时存储和读取大量小元素#,c#,arrays,multithreading,concurrency,hashset,C#,Arrays,Multithreading,Concurrency,Hashset,简言之 如果已经看到大量小字节数组,则需要检查它们,如果没有,则需要存储它们并移动到下一批。这是同时发生的。HashSet确实很神奇,但当元素超过100万时,它会完全崩溃(每个数组可以产生0、1或n个后续数组)。我们对删除元素不感兴趣,只是保持跟踪。什么样的数据结构足够灵活,具有良好的性能并可供多线程使用 更长 对于这个项目,我们需要存储很多特定状态的字节数组,以便跟踪我们看到的数组和没有看到的数组。该项目是在.NET框架的帮助下在C#中完成的。实际的程序是一个控制台应用程序。挑战在于使单线程参

简言之

如果已经看到大量小字节数组,则需要检查它们,如果没有,则需要存储它们并移动到下一批。这是同时发生的。HashSet确实很神奇,但当元素超过100万时,它会完全崩溃(每个数组可以产生0、1或n个后续数组)。我们对删除元素不感兴趣,只是保持跟踪。什么样的数据结构足够灵活,具有良好的性能并可供多线程使用

更长

对于这个项目,我们需要存储很多特定状态的字节数组,以便跟踪我们看到的数组和没有看到的数组。该项目是在.NET框架的帮助下在C#中完成的。实际的程序是一个控制台应用程序。挑战在于使单线程参考解决方案成为更快的多线程解决方案

最初,他们使用Trie数据结构来存储所有以前的状态,但我们发现在使用多个线程时,它的性能很差。相反,我们现在使用一个带有简单锁的,以防我们想写入的。我们发现它与FNV散列函数“Fowler/Noll/Vo(FNV)32位散列函数”配合使用非常好。与单线程参考实现相比,性能提升约300%

失败的最坏情况是:

  • 考虑6600万字节数组
  • 740万在我们的HashSet中结束(其余都是复制品)
  • 这就产生了700万个小字节数组散列,而6600万个小字节数组检查之前是否考虑过一个数组(通过散列并检查该散列是否已经存在)
编辑 我们在System.collections.Concurrent中尝试了这些集合,但问题是大多数集合的性能。有些人出价太高,有些人出价太少。理想情况下,我们只存储唯一的散列,这样我们就不会得到700万字节的数组。这就是为什么我们使用HashSet,它对于这个应用程序有着难以置信的性能,但是当添加量呈指数级增长时,速度会减慢很多

一些实际运行数据:

  • 考虑7001535字节数组,发现977689个重复,并将6023846添加到哈希集(第二复杂)
  • 考虑66478557字节数组,发现7460501个重复项,并将59018056添加到哈希集(最坏情况)
使用HashSet,上述两种情况都会产生以下结果:

  • 运行时间2017毫秒
  • 运行时间17010毫秒
所以我们在8.43倍的时间里做了大约9.49倍的工作,这是一个很好的缩放(略小于线性)。但还不够

使用ConcurrentDictionary(值为字节0),我们得到以下结果:

  • 运行时间2898毫秒
  • 运行时间32155毫秒
使用ConcurrentBag,我们得到以下结果:

  • 40000毫秒后终止
  • 不费心
在这种情况下,HashSet显然是赢家。还有一些跑步:

  • 考虑了704字节数组,发现了85个重复项,并将619添加到哈希集:运行时间799毫秒
  • 考虑9931字节数组,发现1183个重复项,并将8748添加到哈希集中;运行时间294毫秒
  • 考虑3890字节数组,发现603个重复,并将3287添加到哈希集;运行时间319毫秒
  • 考虑64字节数组,发现8个重复,并向哈希集中添加56个;运行时间288毫秒

当看到这些数字时,重要的是要知道继任者的产生可能不成功(哈哈)。上述情况被设计为在我们的程序中发现可能的错误。

< P>根据数据的分布,您可以考虑保留TIE方法,但是基于第一个字节(或一些其他更好的分布式字节,使用一些重新排序将其放在TIE中的第一个)分区。对于“分区字节”的每个值都有一个单独的锁。如果您选择的字节分布良好,这将大大减少锁争用,因为大多数情况下,您的各个线程将访问不同的独立尝试。

从概念上讲,哈希集听起来与您的尝试很匹配,但.NET的实现有一个致命缺陷:它不允许您设置初始容量。(例如,与C++的
ordered_set
不同,它允许您在构造时指定桶计数)。因此,当你反复达到收集容量时,你的大部分时间都花在了重新整理上。奇怪的是,他们不允许您这样做,因为中的注释表明调整大小会带来伤害

因此,让我们衡量一下调整大小/重新灰化对您的影响程度(使用8字节数组,粗略估计最坏情况):

(顺便说一句,这是在运行笔记本电脑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);
    }
}
…为了更大的胜利


那么,你的应用程序有没有办法在你的收藏上进行类似的热身呢?否则,您可能需要考虑创建/查找一个允许您配置初始容量的哈希集实现。

因为我受限于允许我发布的链接计数,如果您感兴趣的是有关赋值的更多上下文,则这里有一个参考实现:(单线程,使用Trie结构跟踪SEED状态)是的,这是大学的作业,不,我不是要求你做作业,只是一些指向正确方向的指针(因为我们自己已经做了BFS并行化)System.Collections.Concurrent.ConcurrentBag怎么样?提示:^^^^^这是Google btw中的第一个链接。您应该真正了解如何使用它。@JonathanCarroll我们实际上使用它来存储后续字节数组(作为下一批查看)。为此目的尝试使用包会使其比哈希集慢。您的意思是将字节数组的哈希存储在t中吗
New HashSet: 27914.5131
Warmed HashSet: 17683.5115
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