c# - 从 HashSet 中选择“任何”项目非常慢 - 如何快速做到这一点?
问题描述
我目前正在用贪婪算法做很多工作——他们不关心索引等,他们只在组/集合上工作。但是我发现我 85% 的执行时间都花在了尝试从 HashSet 中挑选一个项目上。
根据 MSDN 文档:
HashSet 类提供高性能的集合操作。集合是不包含重复元素且其元素没有特定顺序的集合。
...这似乎适用于除“获取值”之外的所有内容。
我试过了:
- ElementAt(0) - 非常慢,据称是因为 HashSet 生成一个临时排序,对所有内容进行排序,然后返回首先出现的内容
- 首先 - 非常慢(大概它正在做同样的事情)
我所期望的:
- AnyItem() - 从 HashSet 中返回一个项目,没有任何保证。可能是第一个,可能是最后一个,可能是随机的(但不要指望那个)。
......但我似乎无法弄清楚这样做的方法。
EDIT2:稍微更详细的源代码,以说明为什么 HashSet 中缺少此功能的一些琐碎解决方法无济于事,并希望更好地说明为什么 HashSet 在所有其他方面都是正确的类:
HashSet<MyClass> candidates;
HashSet<MyClass> unvisited;
... // fill unvisited with typically: 100,000 to 10 million items
... // fill candidates with at least 1 item, potentially 100,000's of items
while( candidates.Count > 0 && unvisited.Count > 0 )
{
var anyItem = candidates.First();
while( ! CanProcess( anyItem ) ) // CanProcess probable checks some set intersections
{
candidates.Remove( anyItem );
if( candidates.Count > 0 )
anyItem = candidates.First();
else
{
anyItem = null;
break;
}
}
if( anyItem == null ) // we've run out of candidates
break;
// For the algorithm: "processing" anyItem has a side-effect of
// transferring 0 or more items from "unvisited" into "candidates"
var extraCandidates = Process( anyItem, unvisited );
// ... Process probably does some set intersections
... // add all the extraCandidates to candidates
... // remove all the extraCandidates from unvisited
candidates.Remove( anyItem );
}
即:具有多组的典型贪心算法:一组“下一次迭代的起点”,以及一组或多组(这里我只展示了一组)“尚未处理且以某种方式连接的数据”到/从起点到达”。
......那里的一切都很快,唯一慢的是“第一个”电话 - 我没有理由拿第一个,我可以拿任何,但我需要拿点东西!
解决方案
似乎HashSet
该类并未针对其第一项被重复删除的场景进行优化。该类内部保留的空间在每次移除后不会减少,而是将相应的插槽标记为空。类的枚举器枚举所有内部槽,并在找到非空槽时产生一个值。当 a 的内部空间HashSet
变得稀疏时,这可能会变得非常低效。例如HashSet
,曾经拥有 1,000,000 个元素并已缩减为单个元素的 a 必须枚举 1,000,000 个插槽,然后才能生成存储在其单个非空插槽中的元素:
var set = new HashSet<int>(Enumerable.Range(1, 1_000_000));
set.ExceptWith(Enumerable.Range(1, 999_999));
var item = set.First(); // Very slow
这是一个不容易解决的问题。TrimExcess
一种解决方案是在每批删除后调用该方法。此方法通过分配新的插槽数组、将现有数组中的项目复制到新数组、最后丢弃旧数组来最小化类内部保留的空间。这是一项昂贵的操作,因此TrimExcess
过于频繁的调用可能会成为您应用程序的新瓶颈。
另一种解决方案可能是使用不受此问题影响的第三方实现。例如,Rock.Collections库包含一个OrderedHashSet
类,该类将项目保持在添加顺序中。它通过以链表方式连接内部插槽来实现这一点。类不仅可以正常枚举,也可以逆序枚举。我没有测试它,但很可能调用First
应该是 O(1) 操作。
下面是一个使用反射来欺骗内置枚举器从随机索引而不是索引 0 开始枚举槽的解决方案。它提供了可接受的性能,但它存在已知的反射问题(开销,前向兼容性等)。静态GetRandom
方法位于通用静态类中,以便FieldInfo
为每种类型分别缓存信息T
。
public static class HashSetRandomizer<T>
{
private static FieldInfo _lastIndexField;
private static FieldInfo _indexField;
private static ThreadLocal<Random> _random;
static HashSetRandomizer()
{
const BindingFlags FLAGS = BindingFlags.NonPublic | BindingFlags.Instance;
_lastIndexField = typeof(HashSet<T>).GetField("m_lastIndex", FLAGS) // Framework
?? typeof(HashSet<T>).GetField("_lastIndex", FLAGS); // Core
_indexField = typeof(HashSet<T>.Enumerator).GetField("index", FLAGS) // Framework
?? typeof(HashSet<T>.Enumerator).GetField("_index", FLAGS); // Core
_random = new ThreadLocal<Random>(() => new Random());
}
public static T GetRandom(HashSet<T> source, Random random = null)
{
if (source == null) throw new ArgumentNullException(nameof(source));
random = random ?? _random.Value;
if (_lastIndexField == null)
throw new NotSupportedException("FieldInfo lastIndex not found.");
if (_indexField == null)
throw new NotSupportedException("FieldInfo index not found.");
if (source.Count > 0)
{
int lastIndex = (int)_lastIndexField.GetValue(source);
if (lastIndex > 0)
{
var randomIndex = random.Next(0, lastIndex);
using (var enumerator = source.GetEnumerator())
{
_indexField.SetValue(enumerator, randomIndex);
if (enumerator.MoveNext()) return enumerator.Current;
}
}
foreach (var item in source) return item; // Fallback
}
throw new InvalidOperationException("The source sequence is empty.");
}
}
用法示例。项目从 a 中随机删除HashSet
,直到集合为空。
var set = new HashSet<int>(Enumerable.Range(1, 1_000_000));
while (set.Count > 0)
{
var item = HashSetRandomizer<int>.GetRandom(set); // Fast
set.Remove(item);
}
即使使用这种方法,删除最后几个项目仍然很慢。
推荐阅读
- c++ - 如何使用 mingw 使 g++ 命令在 VSCode 中工作
- javascript - 为什么不是单个表单元素在控制器中没有返回值?
- reactjs - 在 react redux 应用程序中手动导航时状态丢失
- ios - 在 iPhone 5s 上合并音频到视频的问题
- html - 使用 NODE.JS 和 html5 的低延迟(50 毫秒)视频流
- c# - C# CIL stloc.1 问题
- javascript - 如何随着进度条的增长而改变它的颜色
- wordpress - 我在上传文件夹中的文件刚刚获得 IIS_USER 的特殊权限
- node.js - 如何减少 html-pdf 生成报告的时间?
- ruby-on-rails - 我应该将我的 amazon s3 存储桶放在哪个地区?