如何解决基于迭代器的代码与过程代码:如何使基于迭代器的算法更快? 上下文基准代码典型的基准测试结果问题
我可以看到基于迭代器的算法(慢)和过程算法(快)在性能方面的巨大差异。我想提高基于迭代器的算法的速度,但我不知道该怎么做。
上下文
我需要将 12345678
之类的字符串转换为 12_345_678
。像in this question这样的问题已经有人问过了。 (注意:我不想为此使用板条箱,因为我有一些关于它的定制需求)。
编辑:假设输入字符串是ASCII。
我写了两个版本的算法:
然后我比较了两个版本的执行时间:与程序版本相比,我的迭代器版本大约慢了 ~8x。
基准代码
use itertools::Itertools; // Using itertools v0.10.0
/// Size of the string blocks to separate:
const BLOCK_SIZE: usize = 3;
/// Procedural version.
fn procedural(input: &str) -> String {
let len = input.len();
let nb_complete_blocks = (len as isize - 1).max(0) as usize / BLOCK_SIZE;
let first_block_len = len - nb_complete_blocks * BLOCK_SIZE;
let capacity = first_block_len + nb_complete_blocks * (BLOCK_SIZE + 1);
let mut output = String::with_capacity(capacity);
output.push_str(&input[..first_block_len]);
for i in 0..nb_complete_blocks {
output.push('_');
let start = first_block_len + i * BLOCK_SIZE;
output.push_str(&input[start..start + BLOCK_SIZE]);
}
output
}
/// Iterator version.
fn with_iterators(input: &str) -> String {
input.chars()
.rev()
.chunks(BLOCK_SIZE)
.into_iter()
.map(|c| c.collect::<String>())
.join("_")
.chars()
.rev()
.collect::<String>()
}
fn main() {
let input = "12345678";
macro_rules! bench {
( $version:ident ) => {{
let Now = std::time::Instant::Now();
for _ in 0..1_000_000 {
$version(input);
}
println!("{:.2?}",Now.elapsed());
}};
}
print!("Procedural benchmark: ");
bench!(procedural);
print!("Iterator benchmark: ");
bench!(with_iterators);
}
典型的基准测试结果
Procedural benchmark: 17.07ms
Iterator benchmark: 240.19ms
问题
如何改进基于迭代器的版本,以达到程序版本的性能?
解决方法
with_iterators
版本为 String
中的每个块分配一个新的 input
而 procedural
版本只是将 input
切片并将其附加到 { {1}} 根据需要。之后,这些 output
被连接到目标 String
的反向版本中,该版本再次必须反向转换为另一个 String
,包括确定其字符偏移量并收集到另一个 {{1} }}。这是很多额外的工作,可以解释大规模放缓。
您可以通过String
和String
做一些非常相似的事情,甚至比procedural
版本更强大:
chars
如果您不能排除多字节编码的字符是 for_each
的一部分,那么直接切入 /// Iterator version.
fn with_iterators(input: &str) -> String {
let n_chars = input.chars().count();
let capacity = n_chars + n_chars / BLOCK_SIZE;
let mut acc = String::with_capacity(capacity);
input
.chars()
.enumerate()
.for_each(|(idx,c)| {
if idx != 0 && (n_chars - idx) % BLOCK_SIZE == 0 {
acc.push('_');
}
acc.push(c);
});
acc
}
很容易引起恐慌。即,&str
假定 input
和 procedural
之间没有给出的 1 对 1 映射。
u8
返回每个字符,使用 char
,您可以根据其他字符跟踪它在 chars
中的偏移量,并确定何时推送 enumerate()
。
建议的版本和 str
版本都在我的机器上运行 10-20 毫秒。
这也很很慢。但这里的价值略有不同。
fn iterators_todd(input: &str) -> String {
let len = input.len();
let n_first = len % BLOCK_SIZE;
let it = input.chars();
let mut out = it.clone().take(n_first).collect::<String>();
if len > BLOCK_SIZE && n_first != 0 {
out.push('_');
}
out.push_str(&it.skip(n_first)
.chunks(BLOCK_SIZE)
.into_iter()
.map(|c| c.collect::<String>())
.join("_"));
out
}
我不会将缓慢完全归咎于 .collect()
。分块迭代器也可能会减慢它的速度。通常,将多个迭代器链接在一起,然后通过链迭代地拉取数据不会像链接在一起的迭代器较少的方法那样有效。
上面的代码粗略是一个O(N)
算法(但很慢),至少从上面代码的视觉外观来看,没有深入研究迭代器的实现。
我使用 timeit 板条箱对每个解决方案的性能进行计时。 This Gist 具有产生以下结果的代码。
-
iterators_yolenoyer
- yolenoyer 的迭代器方法。 -
procedural_yolenoyer
- yolenoyer 的“程序化”函数。 -
procedural_yolenoyer_modified
- 前一个有一些变化。 -
iterators_sebpuetz
- sebpuetz 的迭代器示例。 -
procedural_sebpuetz
- 以上使用for
循环代替.for_each()
。 -
iterators_todd
- 此答案中的示例。
输出:
iterators_yolenoyer benchmark: 701.427605 ns
procedural_yolenoyer benchmark: 62.651766 ns
procedural_yolenoyer_modified benchmark: 59.283306 ns
iterators_sebpuetz benchmark: 94.315160 ns
procedural_sebpuetz benchmark: 121.447247 ns
iterators_todd benchmark: 606.468828 ns
有趣的是,使用 .for_each()
比简单的 for
循环更快(将使用 iterators_sebpuetz
的 .for_each()
与使用 {{1} } 循环,for
)。 速度较慢的 procedural_sebpuetz
循环可能并非适用于所有用例。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。