【问题标题】:Why is finding the intersection of integer sets faster with a Vec compared to BTreeSet?为什么用 Vec 比 BTreeSet 更快地找到整数集的交集?
【发布时间】:2019-05-22 16:39:25
【问题描述】:

我需要快速找出两个给定集合中有多少整数。这些集合只被写入一次,但这个操作将使用不同的集合对执行多次。这些集合包含 5-30 个整数,其中最大的整数是 840000。

我最初尝试迭代一个 Vec 并为每个元素检查它是否存在于另一个 Vec 中。然后我决定改用BTreeSet,因为它在检查集合中是否存在整数时应该会更快,但似乎并非如此。 Vec 在稳定的 Rust 1.34 下以发布模式调用数千个集合时需要约 72 毫秒和 BTreeSet 约 96 毫秒,在夜间使用时具有相同的性能。

这是Vec 实现:

use std::cmp;

fn main() {
    let mut sets = Vec::with_capacity(1000);
    for i in 1..1000 {
        let mut set = Vec::new();
        for j in 1..i % 30 {
            set.push(i * j % 50000);
        }
        sets.push(set);
    }
    for left_set in sets.iter() {
        for right_set in sets.iter() {
            calculate_waste(left_set, right_set);
        }
    }
}

fn calculate_waste(left_nums: &Vec<usize>, right_nums: &Vec<usize>) -> usize {
    let common_nums = left_nums.iter().fold(0, |intersection_count, num| {
        intersection_count + right_nums.contains(num) as usize
    });
    let left_side = left_nums.len() - common_nums;
    let right_side = right_nums.len() - common_nums;
    let score = cmp::min(common_nums, cmp::min(left_side, right_side));
    left_side - score + right_side - score + common_nums - score
}

这是BTreeSet 的实现:

use std::cmp;
use std::collections::BTreeSet;

fn main() {
    let mut sets = Vec::with_capacity(1000);
    for i in 1..1000 {
        let mut set = BTreeSet::new();
        for j in 1..i % 30 {
            set.insert(i * j % 50000);
        }
        sets.push(set);
    }
    for left_set in sets.iter() {
        for right_set in sets.iter() {
            calculate_waste(left_set, right_set);
        }
    }
}

fn calculate_waste(left_nums: &BTreeSet<usize>, right_nums: &BTreeSet<usize>) -> usize {
    let common_nums = left_nums.intersection(&right_nums).count();
    let left_side = left_nums.len() - common_nums;
    let right_side = right_nums.len() - common_nums;
    let score = cmp::min(common_nums, cmp::min(left_side, right_side));
    left_side - score + right_side - score + common_nums - score
}

它是使用命令运行的(-w 50 使其忽略前 50 次运行):

hyperfine "cargo run --release" -w 50 -m 100

程序的完整代码可用here

BTreeSet 实现是否较慢,因为集合中的整数太少而无法使其 O(log n) 访问时间发光?如果是这样,我还能做些什么来加快这个功能?

【问题讨论】:

标签: vector rust b-tree set-intersection


【解决方案1】:

由于您的集合不会随时间而改变,我认为您最好的选择是使用排序向量。在初始化时只需要对向量进行一次排序。两个已排序向量的交集可以通过同时迭代它们在线性时间内计算,始终推进当前指向较小数字的迭代器。这是一个实现的尝试:

fn intersection_count_sorted_vec(a: &[u32], b: &[u32]) -> usize {
    let mut count = 0;
    let mut b_iter = b.iter();
    if let Some(mut current_b) = b_iter.next() {
        for current_a in a {
            while current_b < current_a {
                current_b = match b_iter.next() {
                    Some(current_b) => current_b,
                    None => return count,
                };
            }
            if current_a == current_b {
                count += 1;
            }
        }
    }
    count
}

这可能不是特别优化;不管怎样,benchmarking with Criterion-based code 表示此版本的速度是您使用向量的解决方案的三倍以上。

【讨论】:

  • 我注意到这里所做的另一个更改是使用u32 而不是usize => 内存很重要!如果它适合 u32,那么使用 usize 浪费一半空间毫无意义!
  • @MatthieuM。是的,OP提到最大值是840000,所以我悄悄地做了这个改变。由于内存带宽,我预计它也会对性能产生一些影响,但在我的机器上,差异只有 1%,所以我没有在答案中明确提及。
  • 我已经尝试过 BitSet 和这种方法,虽然 BitSet 产生了更大的性能优势,但在某些情况下内存开销太大。此方法在使用可忽略不计的内存量的情况下实现了类似的性能。
  • "这可能不是特别优化;"是的,也许强调这不是可能的线性时间复杂度。虽然我可能弄错了,但在我看来 for current_a in a 循环并没有同时推进两个迭代器。
  • @Unapiedra 我非常有信心该算法具有线性运行时间。该循环确实并行推进了两个迭代器,因为这是计算两个排序向量的交集的整个想法。 “没有很好优化”的评论是关于微优化的——我认为不可能提高算法的复杂性。
猜你喜欢
  • 2016-05-28
  • 2019-04-26
  • 2021-09-08
  • 1970-01-01
  • 2012-02-14
  • 2019-03-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多