最小内存分配的随机字符串生成器

3
我希望根据每行大小和行数的参数生成一个大的伪随机ASCII字符文件,但我想不出一种方法,在不为每行分配新的字符串的情况下实现。以下是我的代码: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=42f5b803910e3a15ff20561117bf9176
use rand::{Rng, SeedableRng};
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let mut data: Vec<u8> = Vec::new();
    write_random_lines(&mut data, 10, 10)?;
    println!("{}", std::str::from_utf8(&data)?);
    Ok(())
}

fn write_random_lines<W>(
    file: &mut W,
    line_size: usize,
    line_count: usize,
) -> Result<(), Box<dyn Error>>
where
    W: std::io::Write,
{
    for _ in 0..line_count {
        let mut s: String = rand::rngs::SmallRng::from_entropy()
            .sample_iter(rand::distributions::Alphanumeric)
            .take(line_size)
            .collect();
        s.push('\n');
        file.write(s.as_bytes())?;
    }
    Ok(())
}

我在每一行都创建了一个新的String,所以我认为这不是内存高效的。有fn fill_bytes(&mut self, dest: &mut [u8]),但这是用于字节的。
我希望不必为每一行创建一个新的SmallRng,但它在循环中使用,并且SmallRng不能被复制。
如何以更加内存和时间高效的方式生成随机文件?
3个回答

3

你的代码修改后不会分配任何String,也不会每次构建一个新的SmallRng,但我没有对其进行基准测试:

fn write_random_lines<W>(
    file: &mut W,
    line_size: usize,
    line_count: usize,
) -> Result<(), Box<dyn Error>>
where
    W: std::io::Write,
{
    // One random data iterator.
    let mut rng_iter = rand::rngs::SmallRng::from_entropy()
        .sample_iter(rand::distributions::Alphanumeric);

    // Temporary storage for encoding of chars. If the characters used
    // are not all ASCII then its size should be increased to 4.
    let mut char_buffer = [0; 1];

    for _ in 0..line_count {
        for _ in 0..line_size {
            file.write(
                rng_iter.next()
                    .unwrap()  // iterator is infinite so this never fails
                    .encode_utf8(&mut char_buffer)
                    .as_bytes())?;
        }
        file.write("\n".as_bytes())?;
    }
    Ok(())
}

我对Rust不太熟悉,可能会有一些方式来使它更整洁。此外,请注意,这里每次只写入一个字符;如果您的 W 每个操作比内存缓冲区更昂贵,那么您可能需要将其包装在std::io::BufWriter中,它将批量写入到目标(使用一个需要分配但仅分配一次的缓冲区)。


谢谢你的想法。我测试了一下,似乎并没有更好。请看我的编辑结果。 - MakotoE
我的错,我犯了一个愚蠢的错误。你的方法更快,但分配的数量相同。这可能是因为在你的方法中迭代器被重复使用的原因。 - MakotoE
我不明白为什么这两个的内存分配是一样的。我本以为我的会糟糕得多。 - MakotoE
@Makoto,你的程序几乎肯定会一遍又一遍地释放和重新分配同一个小缓冲区,因此虽然分配器正在大量运转,但它从未一次需要超过100个字节(不计算两种情况下都存在的100MB缓冲区)。不确定Kevin的情况中额外的分配来自哪里,但我总是对测量问题持怀疑态度。 - trent
@trentcl 我把所有生成的字节都放进了一个 Vec 中,而不是写入文件,这就解决了问题。 - MakotoE

3
您可以通过在循环外创建一个 String ,并在使用完内容后进行清空,来轻松地在循环中重用该字符串。
    // Use Kevin's suggestion not to make a new `SmallRng` each time:
    let mut rng_iter =
        rand::rngs::SmallRng::from_entropy().sample_iter(rand::distributions::Alphanumeric);
    let mut s = String::with_capacity(line_size + 1);  // allocate the buffer
    for _ in 0..line_count {
        s.extend(rng_iter.by_ref().take(line_size));   // fill the buffer
        s.push('\n');
        file.write(s.as_bytes())?;                     // use the contents
        s.clear();                                     // clear the buffer
    }
String::clear方法可以清空String对象中的内容(如果有的话),但不会释放其后备缓冲区,因此可以在无需重新分配内存的情况下重复使用。

另请参阅


谢谢,我决定使用BufWriter,因为这是最简单和直接的方法。就速度而言,这比Kevin的快15%。在循环中,by_ref()将非常有用。 - MakotoE

0
我(MakotoE)对Kevin Reid的答案进行了基准测试,似乎他们的方法更快,尽管内存分配似乎是相同的。
基准测试时间为:
#[cfg(test)]
mod tests {
    extern crate test;
    use test::Bencher;
    use super::*;

    #[bench]
    fn bench_write_random_lines0(b: &mut Bencher) {
        let mut data: Vec<u8> = Vec::new();
        data.reserve(100 * 1000000);
        b.iter(|| {
            write_random_lines0(&mut data, 100, 1000000).unwrap();
            data.clear();
        });
    }

    #[bench]
    fn bench_write_random_lines1(b: &mut Bencher) {
        let mut data: Vec<u8> = Vec::new();
        data.reserve(100 * 1000000);
        b.iter(|| {
            // This is Kevin's implementation
            write_random_lines1(&mut data, 100, 1000000).unwrap();
            data.clear();
        });
    }
}

test tests::bench_write_random_lines0 ... bench: 764,953,658 ns/iter (+/- 7,597,989)
test tests::bench_write_random_lines1 ... bench: 360,662,595 ns/iter (+/- 886,456)

使用valgrind的Massif进行内存使用基准测试显示两者大致相同。我的总共使用了3.072 Gi,峰值为101.0 MB。Kevin的总共使用了4.166 Gi,峰值为128.0 MB。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接