【问题标题】:Fast intersection of HashSet<int> and List<int>HashSet<int> 和 List<int> 的快速交集
【发布时间】:2020-08-16 05:34:16
【问题描述】:
我有一个HashSet<int> 和一个List<int>(Hashset 有大约 300 万个项目,List 有大约 300k 个项目)。
我目前使用
与它们相交
var intersected = hashset.Intersect(list).ToArray();
我想知道是否有更快的方法可以做到这一点。也许并行?
【问题讨论】:
标签:
c#
algorithm
performance
intersection
hashset
【解决方案1】:
HashSet 有一个方法IntersectWith,即optimized if intersection is performed between two hash sets。使用IntersectWith 方法,我们可以使用下一种方法将HashSet 和List 相交:
private static IEnumerable<int> Intersect(HashSet<int> hash, List<int> list)
{
HashSet<int> intersect = new HashSet<int>(list);
intersect.IntersectWith(hash);
return intersect;
}
我已经测量了(使用 Stopwatch)您的原始方法 (Linq Intersect)、@TheodorZoulias (HashSet Contains and HashSet Contains Parallel) 和我的方法 (HashSet IntersectWith) 提出的方法的性能。以下是结果:
------------------------------------------------------------------------
| Method | Min, ms | Max, ms | Avg, ms | StdDev, ms |
------------------------------------------------------------------------
| Linq Intersect | 135 | 274 | 150 | 17 |
| HashSet Contains | 25 | 44 | 26 | 2 |
| HashSet Contains Parallel | 12 | 53 | 13 | 3 |
| HashSet IntersectWith | 57 | 89 | 61 | 4 |
------------------------------------------------------------------------
从表中可以看出,最快的方法是HashSet Contains Parallel,最慢的是Linq Intersect。
这是用于衡量性能的complete source code。
【解决方案2】:
是的,您可以走得更快,因为您已经有了HashSet。 LINQ Intersect 使用 a generic algorithm,这实际上是在每次调用时从头开始重新创建 HashSet。这是一个更快的算法:
/// <summary>Yields all the elements of first (including duplicates) that also
/// appear in second, in the order in which they appear in first.</summary>
public static IEnumerable<TSource> Intersect<TSource>(IEnumerable<TSource> first,
HashSet<TSource> second)
{
foreach (TSource element in first)
{
if (second.Contains(element)) yield return element;
}
}
更新:这是上述想法的并行版本:
var intersected = list.AsParallel().Where(x => hashset.Contains(x)).ToArray();
我不希望它更快,如果有的话,因为工作量是too granular。调用 lambda 300,000 次的开销可能会掩盖并行性的任何好处。
同样不会保留结果的顺序,除非在查询中添加AsOrdered PLINQ 方法,进一步损害操作的性能。
【解决方案3】:
将大量整数存储为紧凑位集而不是HashSet 或List 可能会更快(至少如果您使用List 来存储唯一整数,就像HashSet )。从这个意义上说,有几种选择:
- 内置的
BitArray 以紧凑的方式存储每个位。例如,如果您要存储从 1 到 65000 的整数,BitArray 需要大约 8125 字节的内存(如果每个位存储为 8 位字节,则需要 65000 字节)。但是,如果最高设置位非常大(例如,30 亿),或者如果位集稀疏(有设置位和/或清除位的巨大区域),BitArray 可能不会非常节省内存。您可以使用Xor 方法将两个BitArrays 相交
- 压缩位集同样以紧凑的方式存储每个位,但也会压缩自身的一部分以进一步节省内存,同时仍保持集合操作(如交集)的效率。示例包括 Elias-Fano 编码、Roaring Bitmaps 和 EWAH。请参阅 graphs 在性能和内存方面比较压缩位集与未压缩 (
FixedBitSet) 的不同实现(请注意,它们比较 Java 实现,但它们在 .NET 案例中可能仍然有用)。