Python设置交集比Rust HashSet交集更快
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python设置交集比Rust HashSet交集更快相关的知识,希望对你有一定的参考价值。
这是我的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)
[GCC 4.8.2]'
Rustc版本(我知道1.6已经发布)
$ rustc --version
rustc 1.5.0 (3d7cd77e4 2015-12-04)
我正在使用ubuntu 14.04
,我的系统架构是x86_64。
当我将集合构建移出循环并且仅重复交集时,对于这两种情况,Rust当然比Python 2.7更快。
我只是在阅读Python 3 (setobject.c),但Python的实现有一些事情要做。
它使用两个Python集合对象使用相同的散列函数的事实,因此它不会重新计算散列。 Rust HashSet
s为其哈希函数提供了实例唯一键,因此在交集期间,它们必须使用另一个集合的哈希函数从一个集合中重新发送键。
另一方面,Python必须为每个匹配的哈希调用动态密钥比较函数,如PyObject_RichCompareBool
,而Rust代码使用泛型,并将专门用于i32
的哈希函数和比较代码。在Rust中散列i32
的代码看起来相对便宜,并且删除了大部分散列算法(处理比4个字节更长的输入)。
它似乎是将Python和Rust分开的集合的构造。事实上,不仅仅是构造,还有一些重要的代码可以破坏Rust HashSet
s。 (这可以改进,提出错误:#31711)
性能问题归结为HashMap
和HashSet
的默认哈希实现。 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秒。
抛开一边,当你用一个微小的和一个巨大的集合错误地交叉时,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_disjoint
和union
有点(复制大集合并添加一些元素比它更便宜,比复制小集合并添加很多,但不是很大)。 A pull request被合并,所以这种差异将随着时间的推移而消失。
以上是关于Python设置交集比Rust HashSet交集更快的主要内容,如果未能解决你的问题,请参考以下文章