【问题标题】:Can I pop from a HashSet efficiently?我可以有效地从 HashSet 中弹出吗?
【发布时间】:2019-02-15 15:41:01
【问题描述】:

我的算法需要通过移除一个元素来迭代地收缩一个集合,并在每次迭代中对移除的元素和收缩集做一些事情。并且:

  • 我需要一个能够快速查找的真正集合,而不仅仅是包含独特元素的向量。
  • 元素的选择是任意的:算法的结果不依赖于访问的顺序。性能可能因选择而有很大差异,但假设我想要最简单的代码,并让集合本身来选择它可以有效删除的元素。
  • 顺便说一下,算法是the basic form of the Bron–Kerbosch algorithm。该算法的更智能版本运行速度更快(主要是),因为它们不会随意选择元素,我想了解这种努力有多少回报。

Python 集有一个 pop 成员,几乎可以做到这一点。在 Scala 和 Go 中,选择和删除散列集的“第一个”元素似乎工作正常(其中“第一个”对应于迭代器)。在 Rust 中,这类似于:

// split off an arbitrary element from a (non-empty) set
pub fn pop<T>(set: &mut HashSet<T>) -> T
where
    T: Eq + Clone + std::hash::Hash,
{
    let elt = set.iter().next().cloned().unwrap();
    set.remove(&elt);
    elt
}

与其他语言相比,这似乎是一个性能瓶颈。我benchmarked some implementations of a pop-like function on the playground 但没有一个表现良好。显然删除一个元素并不昂贵,但选择一个元素是:iter().next() 花费一大笔钱(*)。用retain 避免这种情况可以理解并没有帮助:它总是迭代整个集合。有其他选择吗?

(*) 仔细检查后,iter().next() 相当便宜,因为微基准测试是可以信任的。 Separate microbenchmarks 说从集合中选择任意元素的成本(在我的系统上以纳秒为单位):

| Type of set      | Number of elements in set instance
|                  | 100 | 10,000 | 1,000,000
| Rust HashSet     |   2 |      2 |         2
| Rust BTreeSet    |  11 |     12 |        13
| Go map[]struct{} |  27 |     31 |        94
| Python set       | 125 |    125 |       125

【问题讨论】:

  • 请注意,我使用的集合是整数,所以我没有考虑内存管理 - 我想根本不需要克隆来将元素移出集合。
  • @LukasKalbertodt 我试着解释一下
  • 在操场上运行基准测试是一个非常糟糕的主意。这是一个供所有人使用的单一共享 EC2 实例。有很多原因表明其中的任何数字都是可疑的。
  • @Shepmaster 这就是为什么需要一些样本;然而,根据我的经验,结果是相当一致的
  • “与其他语言相比,这是一个性能瓶颈。”你没有证明这一点。我也对此表示怀疑。

标签: rust hashset


【解决方案1】:

我使用的集合有整数

不要使用HashSetBTreeSet 具有更好、更一致的性能。

对于N = 100000...

BTreeSet

sequenced : 3065.098µs
pop_1     : 2941.876µs
pop_2     : 2927.429µs

HashSet

sequenced : 3091.454µs
pop_1     : 172547.080µs
pop_2     : 807182.085µs

【讨论】:

  • 不幸的是,我没有看到用 BTreeSet 替换 HashSet 的更好性能。在上面的基准测试中,这只缩小了集合,BTreeSet 确实是冠军。但是完整的算法在小集合上更糟,在大集合(一百万个整数)上慢几个数量级。分析表明它是每次迭代都完成的交叉点。可能是因为相交的集合通常非常不对称:HashSet.intersection 非常有效地完成了小集合与大集合的交集。
  • 通过交集的性能节约型实现,BTreeSet 的可伸缩性与 HashSet 一样好,并且至少在某些用途上更好。对于小型集,仍然会慢 50%,但对于某些算法,在大型集上更快。交集实现是 on github,希望有一天能在 Rust 中实现。
  • BTreeSet 交集has been merged in的性能更精明的实现@
【解决方案2】:

您的代码可以简化一点:

let elt = set.iter().next().cloned().unwrap();
set.take(&elt).unwrap()

如果您想从 HashSet 中删除所有元素,那么您应该使用 drain 迭代器 - 它非常有效。

Rust 标准库中的HashSet 并没有那么快。尝试用hashbrown crate 中的一个替换它。

【讨论】:

  • drain 不允许使用收缩集,对吧?它立即使集合不可访问和/或为空。
  • 没那么快——这是一个非常具有误导性的说法。暗示HashMap 的当前实现总是slow,但 hashbrown 中的新实现恰好是快er。甚至还有用 hashbrown 的实现替换标准库实现的工作,这最终可能会让这一点变得毫无意义。
  • 我不同意建议的代码更简单。与原来的相比,它现在有两种潜在的恐慌。
  • 建议的代码并不简单,因为我列出的代码在空集上也会出现恐慌,这与游乐场比较中的“更好”替代方案不同。在适当的 Rust 风格中,返回一个 Option,一旦你熟悉了它,它确实会简单一些。我想知道是否存在运行时性能差异,但 my benchmark on github 说它们是相同的。
  • 对于这里问题中的使用,hashbron 实现优于标准HashSet 和fnv crate 的FnvHashSet,但仍远不如标准BTreeSet 可扩展性。对于我评估的其他集合用法(主要是交集),它们都具有同样的可扩展性,hashbrown 是最慢的,FnvHashSet 是最快的。
【解决方案3】:

我猜想与Can I randomly sample from a HashSet efficiently? 中的建议相同:将集合复制为向量只是为了对其进行迭代,如"sequenced" solution in the benchmark 所示:

let seq: Vec<u32> = set.iter().cloned().collect();
for elt in seq {
    set.remove(&elt);

这意味着如果您只需要一次或几次收缩集合(选择任意元素),或者如果集合内容无法廉价克隆,则此答案不适用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-08-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-25
    • 1970-01-01
    • 2011-03-02
    • 2020-04-08
    相关资源
    最近更新 更多