通过字符串的窗口迭代而不收集

时间:2016-08-14 22:37:21

标签: string rust

我需要迭代并比较一个字符串长度未知的窗口。我当前的实现工作,但我已经完成了性能测试,效率非常低。需要保证该方法对Unicode是安全的。

fn foo(line: &str, patt: &str) {
    for window in line.chars().collect::<Vec<char>>().windows(patt.len()) {
        let mut bar = String::new();
        for ch in window {
            bar.push(*ch);
        }
        // perform various comparison checks
    }
}

2 个答案:

答案 0 :(得分:4)

对Shepmaster的最终解决方案进行了改进,显着降低了开销(约为1.5倍),

fn foo(line: &str, pattern: &str) -> bool {
    let pattern_len = pattern.chars().count();

    let starts   = line.char_indices().map(|(i, _)| i);
    let mut ends = line.char_indices().map(|(i, _)| i);

    // Itertools::dropping
    if pattern_len != 0 { ends.nth(pattern_len - 1); }

    for (start, end) in starts.zip(ends.chain(Some(line.len()))) {
        let bar = &line[start..end];
        if bar == pattern { return true }
    }

    false
}

那就是说,来自Github页面的代码有点奇怪。例如,您尝试使用wordier版本

处理不同长度的打开和关闭标记
let length = cmp::max(comment.len(), comment_end.len());

但是你的支票

if window.contains(comment)

然后可以多次触发!

更好的方法是迭代缩小的切片。在迷你示例中,这将是

fn foo(line: &str, pattern: &str) -> bool {
    let mut chars = line.chars();
    loop {
        let bar = chars.as_str();
        if bar.starts_with(pattern) { return true }
        if chars.next().is_none() { break }
    }

    false
}

(请注意,这再一次最终会再次提高性能〜1.5倍。)

在一个更大的例子中,这就像

let mut is_in_comments = 0u64;

let start = match line.find(comment) {
    Some(start) => start,
    None => return false,
};

let end = match line.rfind(comment_end) {
    Some(end) => end,
    None => return true,
};

let mut chars = line[start..end + comment_end.len()].chars();
loop {
    let window = chars.as_str();

    if window.starts_with(comment) {
        if nested {
            is_in_comments += 1;
        } else {
            is_in_comments = 1;
        }
    } else if window.starts_with(comment_end) {
        is_in_comments = is_in_comments.saturating_sub(1);
    }

    if chars.next().is_none() { break }
}

请注意,这仍然会计算重叠,因此/*/可能会被视为开头/*,紧接着是结束*/

答案 1 :(得分:3)

  

需要保证该方法对Unicode安全。

pattern.len()返回字符串所需的字节的数量,因此您的代码可能已经做错了。我可能会建议您查看QuickCheck之类的工具来生成包含Unicode的任意字符串。

这是我的测试工具:

use std::iter;

fn main() {
    let mut haystack: String = iter::repeat('a').take(1024*1024*100).collect();
    haystack.push('b');

    println!("{}", haystack.len());
}

我正在通过cargo build --release && time ./target/release/x进行编译和计时。单独创建字符串需要 0.274s

我使用此版本的原始代码只是为了进行某种比较:

fn foo(line: &str, pattern: &str) -> bool {
    for window in line.chars().collect::<Vec<char>>().windows(pattern.len()) {
        let mut bar = String::new();
        for ch in window {
            bar.push(*ch);
        }

        if bar == pattern { return true }
    }

    false
}

foo需要4.565s或 4.291s

我看到的第一件事是内循环上发生了很多分配。代码为每次迭代创建,分配和销毁String。让我们重用String分配:

fn foo_mem(line: &str, pattern: &str) -> bool {
    let mut bar = String::new();

    for window in line.chars().collect::<Vec<char>>().windows(pattern.len()) {
        bar.clear();
        bar.extend(window.iter().cloned());

        if bar == pattern { return true }
    }

    false
}

foo_mem需要2.155s或 1.881s

继续,另一个无关的分配是String 所有的分配。我们已经有了看似正确的字节,所以让我们重用它们:

fn foo_no_string(line: &str, pattern: &str) -> bool {
    let indices: Vec<_> = line.char_indices().map(|(i, _c)| i).collect();
    let l = pattern.chars().count();

    for window in indices.windows(l + 1) {
        let first_idx = *window.first().unwrap();
        let last_idx = *window.last().unwrap();

        let bar = &line[first_idx..last_idx];

        if bar == pattern { return true }
    }

    // Do the last pair
    {
        let last_idx = indices[indices.len() - l];

        let bar = &line[last_idx..];
        if bar == pattern { return true }
    }

    false
}

这段代码很丑陋而且非常简单。我很确定一些想法(我现在太懒了)会让它看起来好多了。

foo_mem需要1.409s或 1.135s

由于这是〜25%的原始时间,Amdahl's Law表明这是一个合理的停止点。