切片数组或使用 Iterator::skip 是否更有效?

Ива*_*ван 4 arrays monads vector slice rust

我想为 slice 中的每个元素调用一个函数[0+k .. n],其中k是偏移量,n是向量中的元素数。重要的是,我想要原始切片中元素的索引。

我找到了两种方法来做到这一点:

  1. 使用enumerateskip开始项目

    vec.iter().enumerate().skip(k).map(|(i, v)| (f(v), i)).min()
    
    Run Code Online (Sandbox Code Playgroud)
  2. 取一个子切片并将偏移量添加到`enumerate 的索引中

    vec[k..].iter().enumerate().map(|(i, v)| (f(v), i + k)).min()
    
    Run Code Online (Sandbox Code Playgroud)

在这两种情况下,vec都是字符串向量,并f返回字符串中的特定字符 ( v.chars().nth(offset))。这些解决方案中哪个最有效?

She*_*ter 5

让我们以这段代码为例。它与您的示例类似,但更简单:

fn main() {
    let items = ["a", "bb", "ccc", "dddd", "eeeee"];
    let k = 3;

    let one = items.iter().enumerate().skip(k).map(|(i, v)| (v.len(), i));
    let two = items[k..].iter().enumerate().map(|(i, v)| (v.len(), i + k));

    // Sanity check that the results are the same
    let items1: Vec<_> = one.collect();
    let items2: Vec<_> = two.collect();

    println!("{}", items1 == items2);
}
Run Code Online (Sandbox Code Playgroud)

哪个性能更好是一个棘手的话题。Rust 和 LLVM 有很好的优化,可以使代码非常快。

纯粹根据我的直觉,如果我知道我将跳过“少数”项目,我可能会使用第一个,如果我不知道有多少或很多,我可能会使用第二个.

在第一种情况下,从概念上讲,您必须遍历要跳过的所有项目。优化器可能会减少这种情况,但是与enumerateand的交互很复杂map,所以我不会在不检查程序集的情况下指望它。

第二个 ( items[k..]) 使用细分,这将是一个 O(1) 操作,因为它只是要索引到一块内存中。然后你做加法,这也很简单。

但是,唯一真正的测试是进行一些分析。我们将创建一个大型输入数组并开始:

fn main() {
    let items = ["a", "bb", "ccc", "dddd", "eeeee"];
    let items: Vec<_> = items.iter().cycle().take(10_000_000).collect();

    let k = 371_223;

    // let one = items.iter().enumerate().skip(k).map(|(i, v)| (v.len(), i));
    let two = items[k..].iter().enumerate().map(|(i, v)| (v.len(), i + k));

    // let items1: Vec<_> = one.collect();
    let items2: Vec<_> = two.collect();

    // println!("{}", items1.len());
    println!("{}", items2.len());
}
Run Code Online (Sandbox Code Playgroud)

运行经过优化编译的代码,运行 10 次后的平均时间如下:

  1. 153.6ms
  2. 160.1ms

因此,与我的直觉所说的相反,第一个版本更快。我的基准测试完全有可能不正确,但这就是您应该对真实代码进行基准测试的原因。

另外,请注意,无论哪种方式,这都非常快。每个项目大约需要 15 或 16纳秒。朋友之间的一纳秒是多少?