微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

基于迭代器的代码与过程代码:如何使基于迭代器的算法更快? 上下文基准代码典型的基准测试结果问题

如何解决基于迭代器的代码与过程代码:如何使基于迭代器的算法更快? 上下文基准代码典型的基准测试结果问题

我可以看到基于迭代器的算法(慢)和过程算法(快)在性能方面的巨大差异。我想提高基于迭代器的算法的速度,但我不知道该怎么做。

上下文

我需要将 12345678 之类的字符串转换12_345_678。像in this question这样的问题已经有人问过了。 (注意:我不想为此使用板条箱,因为我有一些关于它的定制需求)。

编辑:假设输入字符串是ASCII。

我写了两个版本的算法:

  • 一个基于迭代器的版本,但我不满意,因为有两个.collect(),这意味着很多分配(至少一个.collect()是额外的,无法删除它,请参阅下面的代码);
  • 它的程序版本。

然后我比较了两个版本的执行时间:与程序版本相比,我的迭代器版本大约慢了 ~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 中的每个块分配一个新的 inputprocedural 版本只是将 input 切片并将其附加到 { {1}} 根据需要。之后,这些 output 被连接到目标 String 的反向版本中,该版本再次必须反向转换为另一个 String,包括确定其字符偏移量并收集到另一个 {{1} }}。这是很多额外的工作,可以解释大规模放缓。

您可以通过StringString做一些非常相似的事情,甚至比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 假定 inputprocedural 之间没有给出的 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 举报,一经查实,本站将立刻删除。