我正在沿着铁 rust 的轨道走Exercism.io英里.我有相当多的C/C++经验.我喜欢Rust的"功能性"元素,但我担心它的相对性能.

我解决了'run length encoding' problem个问题:

pub fn encode(source: &str) -> String {
    let mut retval = String::new();
    let firstchar = source.chars().next();
    let mut currentchar = match firstchar {
        Some(x) => x,
        None => return retval,
    };
    let mut currentcharcount: u32 = 0;
    for c in source.chars() {
        if c == currentchar {
            currentcharcount += 1;
        } else {
            if currentcharcount > 1 {
                retval.push_str(&currentcharcount.to_string());
            }
            retval.push(currentchar);
            currentchar = c;
            currentcharcount = 1;
        }
    }
    if currentcharcount > 1 {
        retval.push_str(&currentcharcount.to_string());
    }
    retval.push(currentchar);
    retval
}

我注意到其中一个排名靠前的答案看起来更像这样:

extern crate itertools;

use itertools::Itertools;

pub fn encode(data: &str) -> String {
    data.chars()
        .group_by(|&c| c)
        .into_iter()
        .map(|(c, group)| match group.count() {
            1 => c.to_string(),
            n => format!("{}{}", n, c),
        })
        .collect()
}

我喜欢一流的解决方案;它简单、实用、优雅.这就是他们向我promise 的一切.另一方面,我的 idea 是粗俗的,充满了可变变量.你可以告诉我我已经习惯C++了.

我的问题是功能性风格对性能有很大影响.我用相同的4MB随机数据对这两个版本进行了1000次编码测试.我的紧急解决方案只用了不到10秒;功能性溶液约为2min 30秒.

  • 为什么功能性风格比命令式风格慢得多?
  • 功能实现是否存在导致如此巨大减速的问题?
  • 如果我想写高性能代码,我应该使用这种函数式风格吗?

推荐答案

TL;博士

在某些情况下,功能实现可能比最初的程序实现更快.

为什么功能性风格比命令式风格慢得多?功能实现是否存在导致如此巨大减速的问题?

作为Matthieu M. already pointed out,需要注意的重要一点是algorithm很重要.算法的表达方式(过程性、命令性、面向对象、功能性、声明性)通常并不重要.

我发现功能代码有两个主要问题:

  • 反复分配大量字符串是低效的.在最初的功能实现中,这是通过to_stringformat!实现的.

  • 使用group_by会带来开销,它的存在是为了得到嵌套的iterator,而你不需要仅仅获得计数.

使用more个itertools(batchingtake_while_refformat_with)使这两个实现更加接近:

pub fn encode_slim(data: &str) -> String {
    data.chars()
        .batching(|it| {
            it.next()
                .map(|v| (v, it.take_while_ref(|&v2| v2 == v).count() + 1))
        })
        .format_with("", |(c, count), f| match count {
            1 => f(&c),
            n => f(&format_args!("{}{}", n, c)),
        })
        .to_string()
}

4MiB随机字母数字数据的基准,用RUSTFLAGS='-C target-cpu=native':

encode (procedural)     time:   [21.082 ms 21.620 ms 22.211 ms]

encode (fast)           time:   [26.457 ms 27.104 ms 27.882 ms]
Found 7 outliers among 100 measurements (7.00%)
  4 (4.00%) high mild
  3 (3.00%) high severe

如果您对创建自己的迭代器感兴趣,可以将过程代码与更多功能代码混合匹配:

struct RunLength<I> {
    iter: I,
    saved: Option<char>,
}

impl<I> RunLength<I>
where
    I: Iterator<Item = char>,
{
    fn new(mut iter: I) -> Self {
        let saved = iter.next(); // See footnote 1
        Self { iter, saved }
    }
}

impl<I> Iterator for RunLength<I>
where
    I: Iterator<Item = char>,
{
    type Item = (char, usize);

    fn next(&mut self) -> Option<Self::Item> {
        let c = self.saved.take().or_else(|| self.iter.next())?;

        let mut count = 1;
        while let Some(n) = self.iter.next() {
            if n == c {
                count += 1
            } else {
                self.saved = Some(n);
                break;
            }
        }

        Some((c, count))
    }
}

pub fn encode_tiny(data: &str) -> String {
    use std::fmt::Write;

    RunLength::new(data.chars()).fold(String::new(), |mut s, (c, count)| {
        match count {
            1 => s.push(c),
            n => write!(&mut s, "{}{}", n, c).unwrap(),
        }
        s
    })
}

1-感谢Stargateur for pointing out,Eager 地获得第一个值有助于分支预测.

4MiB随机字母数字数据的基准,用RUSTFLAGS='-C target-cpu=native':

encode (procedural)     time:   [19.888 ms 20.301 ms 20.794 ms]
Found 4 outliers among 100 measurements (4.00%)
  3 (3.00%) high mild
  1 (1.00%) high severe

encode (tiny)           time:   [19.150 ms 19.262 ms 19.399 ms]
Found 11 outliers among 100 measurements (11.00%)
  5 (5.00%) high mild
  6 (6.00%) high severe

我认为这更清楚地显示了这两种实现之间的主要区别:基于迭代器的解决方案是resumable.每次我们拨打next,我们都需要看看是否有之前读过的字符(self.saved).这会向程序代码中没有的代码添加一个分支.

另一方面,基于迭代器的解决方案更灵活——我们现在可以对数据进行各种转换,或者直接写入文件而不是String,等等.自定义迭代器可以扩展为对泛型类型而不是char进行操作,从而使其更灵活.

另见:

如果我想写高性能代码,我应该使用这种函数式风格吗?

我会的,直到基准测试显示这是瓶颈.然后判断why,这是瓶颈.

支持代码

总是要展示你的作品,对吗?

benchmark.rs

use criterion::{criterion_group, criterion_main, Criterion}; // 0.2.11
use rle::*;

fn criterion_benchmark(c: &mut Criterion) {
    let data = rand_data(4 * 1024 * 1024);

    c.bench_function("encode (procedural)", {
        let data = data.clone();
        move |b| b.iter(|| encode_proc(&data))
    });

    c.bench_function("encode (functional)", {
        let data = data.clone();
        move |b| b.iter(|| encode_iter(&data))
    });

    c.bench_function("encode (fast)", {
        let data = data.clone();
        move |b| b.iter(|| encode_slim(&data))
    });

    c.bench_function("encode (tiny)", {
        let data = data.clone();
        move |b| b.iter(|| encode_tiny(&data))
    });
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

lib.rs

use itertools::Itertools; // 0.8.0
use rand; // 0.6.5

pub fn rand_data(len: usize) -> String {
    use rand::distributions::{Alphanumeric, Distribution};
    let mut rng = rand::thread_rng();
    Alphanumeric.sample_iter(&mut rng).take(len).collect()
}

pub fn encode_proc(source: &str) -> String {
    let mut retval = String::new();
    let firstchar = source.chars().next();
    let mut currentchar = match firstchar {
        Some(x) => x,
        None => return retval,
    };
    let mut currentcharcount: u32 = 0;
    for c in source.chars() {
        if c == currentchar {
            currentcharcount += 1;
        } else {
            if currentcharcount > 1 {
                retval.push_str(&currentcharcount.to_string());
            }
            retval.push(currentchar);
            currentchar = c;
            currentcharcount = 1;
        }
    }
    if currentcharcount > 1 {
        retval.push_str(&currentcharcount.to_string());
    }
    retval.push(currentchar);
    retval
}

pub fn encode_iter(data: &str) -> String {
    data.chars()
        .group_by(|&c| c)
        .into_iter()
        .map(|(c, group)| match group.count() {
            1 => c.to_string(),
            n => format!("{}{}", n, c),
        })
        .collect()
}

pub fn encode_slim(data: &str) -> String {
    data.chars()
        .batching(|it| {
            it.next()
                .map(|v| (v, it.take_while_ref(|&v2| v2 == v).count() + 1))
        })
        .format_with("", |(c, count), f| match count {
            1 => f(&c),
            n => f(&format_args!("{}{}", n, c)),
        })
        .to_string()
}

struct RunLength<I> {
    iter: I,
    saved: Option<char>,
}

impl<I> RunLength<I>
where
    I: Iterator<Item = char>,
{
    fn new(mut iter: I) -> Self {
        let saved = iter.next();
        Self { iter, saved }
    }
}

impl<I> Iterator for RunLength<I>
where
    I: Iterator<Item = char>,
{
    type Item = (char, usize);

    fn next(&mut self) -> Option<Self::Item> {
        let c = self.saved.take().or_else(|| self.iter.next())?;

        let mut count = 1;
        while let Some(n) = self.iter.next() {
            if n == c {
                count += 1
            } else {
                self.saved = Some(n);
                break;
            }
        }

        Some((c, count))
    }
}

pub fn encode_tiny(data: &str) -> String {
    use std::fmt::Write;

    RunLength::new(data.chars()).fold(String::new(), |mut s, (c, count)| {
        match count {
            1 => s.push(c),
            n => write!(&mut s, "{}{}", n, c).unwrap(),
        }
        s
    })
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn all_the_same() {
        let data = rand_data(1024);

        let a = encode_proc(&data);
        let b = encode_iter(&data);
        let c = encode_slim(&data);
        let d = encode_tiny(&data);

        assert_eq!(a, b);
        assert_eq!(a, c);
        assert_eq!(a, d);
    }
}

Rust相关问答推荐

为什么是!为Rust中的RwLockReadGuard和RwLockWriteGuard实现的发送特征?

Rust kill std::processs::child

如何使用字符串迭代器执行查找?

编译项目期间使用Cargo生成时出现rustc错误

如何导入crate-type=[";cdylib;]库?

铁 rust ,我的模块介绍突然遇到了一个问题

如何在函数中返回自定义字符串引用?

我如何使用AWS SDK for Rust获取我承担的角色的凭据?

为什么 tokio 在以奇怪的方式调用时只运行 n 个任务中的 n-1 个?

为什么 `Deref` 没有在 `Cell` 上实现?

将特征与具有生命周期的关联类型一起使用时的生命周期方差问题

如何在 `connect_activate()` 之外创建一个 `glib::MainContext::channel()` 并将其传入?

具有多个键的 HashMap

Rust 中指向自身的引用如何工作?

Rust 异步循环函数阻塞了其他future 任务的执行

为什么不可变特征的实现可以是可变的?

使用部分键从 Hashmap 中检索值

我如何将 google_gmail1::Gmail> 传递给线程生成?

为什么这个值在上次使用后没有下降?

在 macro_rules 中转义 $ 美元符号