Functional programming '的性能影响是什么;功能性';生锈?

Functional programming '的性能影响是什么;功能性';生锈?,functional-programming,rust,imperative-programming,Functional Programming,Rust,Imperative Programming,我沿着铁锈的轨道走。我有相当多的C/C++经验。我喜欢Rust的“功能”元素,但我关心的是相对性能 我解决了以下问题: pub-fn-encode(源代码:&str)->String{ 让mut retval=String::new(); 让firstchar=source.chars().next(); 让mut currentchar=匹配firstchar{ 一些(x)=>x, None=>返回retval, }; 让mut currentcharcount:u32=0; 对于source

我沿着铁锈的轨道走。我有相当多的C/C++经验。我喜欢Rust的“功能”元素,但我关心的是相对性能

我解决了以下问题:

pub-fn-encode(源代码:&str)->String{
让mut retval=String::new();
让firstchar=source.chars().next();
让mut currentchar=匹配firstchar{
一些(x)=>x,
None=>返回retval,
};
让mut currentcharcount:u32=0;
对于source.chars()中的c{
如果c==currentchar{
currentcharcount+=1;
}否则{
如果currentcharcount>1{
retval.push_str(¤tcharcount.to_string());
}
retval.push(currentchar);
currentchar=c;
currentcharcount=1;
}
}
如果currentcharcount>1{
retval.push_str(¤tcharcount.to_string());
}
retval.push(currentchar);
复述
}
我注意到其中一个排名靠前的答案看起来更像这样:

外部板条箱工具;
使用itertools::itertools;
pub fn encode(数据:&str)->字符串{
data.chars()
.分组人(|和c | c)
.into_iter()
.map(|(c,组)|匹配组.count(){
1=>c.to_string(),
n=>格式!(“{}{}”,n,c),
})
.collect()
}
我喜欢一流的解决方案;它简单、实用、优雅。这就是他们向我保证生锈的原因。另一方面,我的想法是粗俗的,充满了可变变量。你可以看出我已经习惯C++了。 我的问题是功能性风格对性能有很大的影响。我用相同的4MB随机数据对这两个版本进行了1000次编码测试。我的紧急解决方案用了不到10秒;功能溶液为~2min 30秒

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

内存分配 这里提出的函数风格的一个大问题是传递给
map
方法的闭包,该方法分配了大量的资源。每个字符在被收集之前首先映射到一个
字符串

它还使用
格式
机制,已知该机制相对较慢

有时,人们太过努力地试图得到一个“纯”功能解决方案,而不是:

let mut result = String::new();
for (c, group) in &source.chars().group_by(|&c| c) {
    let count = group.count();
    if count > 1 {
        result.push_str(&count.to_string());
    }

    result.push(c);
}
与您的解决方案一样详细,但只在
count>1
时分配,而且也不使用
格式
机制

与全功能解决方案相比,我希望在性能上取得重大胜利,同时,与全功能解决方案相比,我仍然利用
groupby
提高可读性。有时候,你应该混合搭配

TL;博士

在某些情况下,功能实现可能比原始过程实现更快

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

同样,需要注意的重要一点是算法很重要。算法的表达方式(过程式、命令式、面向对象、函数式、声明式)通常并不重要

我看到功能代码有两个主要问题:

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
    })
}
  • 反复分配大量字符串是低效的。在最初的功能实现中,这是通过
    to_string
    格式完成的

  • 使用
    groupby
    会带来开销,它的存在是为了提供一个嵌套迭代器,您不需要只获取计数

使用更多的itertools(,)可以使这两种实现更加接近:

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()
}
4MB随机字母数字数据的基准测试,使用
RUSTFLAGS='-C target cpu=native'
编译:

编码(程序)时间:[21.082毫秒21.620毫秒22.211毫秒]
编码(快速)时间:[26.457毫秒27.104毫秒27.882毫秒]
在100个测量值中发现7个异常值(7.00%)
4(4.00%)高轻度
3(3.00%)高度严重
如果您对创建自己的迭代器感兴趣,可以将过程代码与更多功能代码混合匹配:

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
    })
}
我相信这更清楚地显示了这两种实现之间的主要基本区别:基于迭代器的解决方案是可恢复的。每次调用
next
,我们都需要查看是否有之前读取的字符(
self.saved
)。这会向过程代码中没有的代码添加分支

另一方面,基于迭代器的解决方案更加灵活-我们现在可以对数据进行各种转换,或者直接写入文件而不是
字符串
,等等。自定义迭代器可以扩展为对泛型类型而不是
字符
进行操作,这使得它非常灵活

另见:

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

我会的,直到基准测试显示这是瓶颈。然后评估为什么它是瓶颈

支持代码 总是要展示你的作品,对吗

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);
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);
    }
}
lib.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);
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);
    }
}
使用itertools::itertools;//0.8.0
使用rand;//0.6.5
发布fn随机数据(len:usize)->字符串{
使用rand::distributions::{字母数字,Distribution};
让mut rng=rand::thread_rng();
字母数字.sample_iter(&mut rng).take(len).collect()
}
pub fn encode_proc(源代码:&str)->字符串{
让mut retval=String::new();
让firstchar=source.chars().next();
让我们特写特写