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

问题描述 投票:9回答:3

这是我的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

这是我的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);
}

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

time python set_performance.py
50000000

real    0m11.757s
user    0m11.736s
sys 0m0.012s

rustc set_performance.rs -O       
time ./set_performance 50000000

real    0m17.580s
user    0m17.533s
sys 0m0.032s

(用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]'

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

$ rustc --version
rustc 1.5.0 (3d7cd77e4 2015-12-04)

我正在使用ubuntu 14.04,我的系统架构是x86_64。

python rust hashset
3个回答
14
投票

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

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

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

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


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


12
投票

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

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

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);
}

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

如果使用相同的changes to move the set building out of the loop,则与Python代码的3.093相比,Rust代码需要0.829秒。


4
投票

抛开一边,当你用一个微小的和一个巨大的集合错误地交叉时,Python会比以前版本的Rust竞争。例如。这code on playground

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(),
        );
    }
}

当使用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)
    }
}

Python中的方法同等地处理这两组(至少在3.7版本中)。

PS这是为什么?假设小集合S1有A项,大集合S2有B项,需要时间来散列一个密钥,Tl(X)时间来定位具有X个元素的集合中的散列密钥。然后:

  • S1.intersection(&S2)费用A *(Th + Tl(B))
  • S2.intersection(&S1)费用B *(Th + Tl(A))

假设哈希函数是好的并且桶很多(因为如果我们担心交集的性能,那么我们应该确保这些集合开始是有效的)那么Tl(B)应该与T1(A)相同)或者至少T1(X)应该比设定尺寸线性地缩小得多。因此,A与B决定了操作的成本。

PS同样的问题和解决方法为is_disjointunion有点(复制大集合并添加一些元素比它更便宜,比复制小集合并添加很多,但不是很大)。 A pull request被合并,所以这种差异将随着时间的推移而消失。

© www.soinside.com 2019 - 2024. All rights reserved.