Python设置交集比Rust HashSet交集更快

Aka*_*all 23 python hashset rust

这是我的Python代码:

len_sums = 0
for i in xrange(100000):
    set_1 = set(xrange(1000))
    set_2 = set(xrange(500, 1500))
    intersection_len = len(set_1.intersection(set_2))
    len_sums += intersection_len
print len_sums
Run Code Online (Sandbox Code Playgroud)

这是我的Rust代码:

use std::collections::HashSet;

fn main() {
    let mut len_sums = 0;
    for _ in 0..100000 {
        let set_1: HashSet<i32> = (0..1000).collect();
        let set_2: HashSet<i32> = (500..1500).collect();
        let intersection_len = set_1.intersection(&set_2).count();
        len_sums += intersection_len;
    }
    println!("{}", len_sums);
}
Run Code Online (Sandbox Code Playgroud)

我相信这些大致相当.我得到以下表现结果:

time python set_performance.py
50000000

real    0m11.757s
user    0m11.736s
sys 0m0.012s
Run Code Online (Sandbox Code Playgroud)

rustc set_performance.rs -O       
time ./set_performance 50000000

real    0m17.580s
user    0m17.533s
sys 0m0.032s
Run Code Online (Sandbox Code Playgroud)

(建立cargo--release给出相同的结果).

我意识到Python set是用C实现的,所以预计会很快,但我没想到它会比Rust更快.难道不必做Rust不会做的额外类型检查吗?

也许我在编译Rust程序的过程中遗漏了一些东西,我应该使用其他任何优化标志吗?

另一种可能性是代码并不是真正等效的,Rust正在做不必要的额外工作,我错过了什么吗?

Python版本:

In [3]: import sys

In [4]: sys.version
Out[4]: '2.7.6 (default, Jun 22 2015, 17:58:13) \n[GCC 4.8.2]'
Run Code Online (Sandbox Code Playgroud)

Rustc版本(我知道1.6已经发布)

$ rustc --version
rustc 1.5.0 (3d7cd77e4 2015-12-04)
Run Code Online (Sandbox Code Playgroud)

我正在使用cargo,我的系统架构是x86_64.

blu*_*uss 20

当我将集合构建移出循环并且仅重复交集时,对于这两种情况,Rust当然比Python 2.7更快.

我只是在阅读Python 3 (setobject.c),但Python的实现有一些事情要做.

它使用两个Python集合对象使用相同的散列函数的事实,因此它不会重新计算散列.Rust HashSet的哈希函数具有实例唯一键,因此在交集期间,它们必须使用另一个集合的哈希函数从一个集合中重新发送键.

另一方面,Python必须调用动态密钥比较函数,例如PyObject_RichCompareBool每个匹配的哈希,而Rust代码使用泛型,并将专门化哈希函数和比较代码i32.i32在Rust中散列an的代码看起来相对便宜,并且删除了大部分散列算法(处理比4个字节更长的输入).


它似乎是 Python和Rust分开的集合的构造.事实上,不仅仅是构造,还有一些重要的代码可以用来破坏Rust HashSet.(这可以改进,提出错误:#31711)

  • *Rust HashSets 的散列函数具有实例唯一键,因此在交集期间,它们必须将一组中的键与另一组的散列函数重新散列。* =&gt; 这可以优化吗?我正在考虑可能在`BuilderHashDefault`上有一个方法,或者只是*比较*在`HashSet`/`HashMap`的两个实例之间的所述构建器,以在可能的情况下优化散列重新计算。这样,您可以在需要执行交集/并集/...的集合上使用相同的构建器或等效的构建器。 (3认同)

She*_*ter 17

性能问题归结为默认的散列实现的HashMapHashSet.Rust的默认哈希算法是一个很好的通用目标,它也可以防止某些类型的DOS攻击.但是,它对于非常小或非常大量的数据不起作用.

一些分析显示,make_hash<i32, std::collections::hash::map::RandomState>占总运行时间的约41%.从Rust 1.7开始,您可以选择使用哪种哈希算法.切换到FNV哈希算法会大大加快程序的速度:

extern crate fnv;

use std::collections::HashSet;
use std::hash::BuildHasherDefault;
use fnv::FnvHasher;

fn main() {
    let mut len_sums = 0;
    for _ in 0..100000 {
        let set_1: HashSet<i32, BuildHasherDefault<FnvHasher>> = (0..1000).collect();
        let set_2: HashSet<i32, BuildHasherDefault<FnvHasher>> = (500..1500).collect();
        let intersection_len = set_1.intersection(&set_2).count();
        len_sums += intersection_len;
    }
    println!("{}", len_sums);
}
Run Code Online (Sandbox Code Playgroud)

在我的机器上,与Python的9.203相比,这需要2.714秒.

如果进行相同的更改以将集合构建移出循环,则与Python代码的3.093相比,Rust代码需要0.829秒.


Ste*_*ein 7

抛开这些,当您以错误的方式相交一个微小的对象时,Python会超越Rust的早期版本。例如在操场上的这段代码

use std::collections::HashSet;
fn main() {
    let tiny: HashSet<i32> = HashSet::new();
    let huge: HashSet<i32> = (0..1_000).collect();
    for (left, right) in &[(&tiny, &huge), (&huge, &tiny)] {
        let sys_time = std::time::SystemTime::now();
        assert_eq!(left.intersection(right).count(), 0);
        let elapsed = sys_time.elapsed().unwrap();
        println!(
            "{:9}ns starting from {:4} element set",
            elapsed.subsec_nanos(),
            left.len(),
        );
    }
}
Run Code Online (Sandbox Code Playgroud)

当使用1.32或更早版本的Rust(而不是当前版本)运行时,这表明您确实要在两个集合中的较小者上调用交集方法(即使在临界情况下,一个集合为空)。通过调用此函数而不是交集方法,我获得了不错的性能提升:

fn smart_intersect<'a, T, S>(
    s1: &'a HashSet<T, S>,
    s2: &'a HashSet<T, S>,
) -> std::collections::hash_set::Intersection<'a, T, S>
where
    T: Eq + std::hash::Hash,
    S: std::hash::BuildHasher,
{
    if s1.len() < s2.len() {
        s1.intersection(s2)
    } else {
        s2.intersection(s1)
    }
}
Run Code Online (Sandbox Code Playgroud)

Python中的方法平等地对待两个集合(至少在3.7版中)。

PS这是为什么?假设小集合Sa具有A个项目,大集合Sb具有B个项目,则花Th时间来哈希一个键,Tl(X)时间来定位具有X个元素的集合中的哈希键。然后:

  • Sa.intersection(&Sb) 成本A *(Th + Tl(B))
  • Sb.intersection(&Sa) 成本B *(Th + Tl(A))

假设哈希函数很好并且存储桶足够多(因为如果我们担心交集的性能,那么我们应该确保集合有效地开始),那么Tl(B)应该与Tl(A ),或者至少Tl(X)的缩放比例应远小于设置大小的线性比例。因此,决定成本的是A与B。

PS存在相同的问题和解决方法is_disjoint,也有一些问题union(复制大集合并添加一些元素要便宜,比复制小集合并添加很多元素要便宜,但幅度不大)。由于合并了拉取请求,因此从Rust 1.35开始,这种差异消失了。

  • 您还可以查看是否可以为此向标准库提交PR。似乎是足够安全的更改,适合所有人。 (5认同)