我需要迭代并比较一个字符串长度未知的窗口。我当前的实现工作,但我已经完成了性能测试,效率非常低。需要保证该方法对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
}
}
答案 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表明这是一个合理的停止点。