Rust中用于编程竞赛的最快的惯用I / O例程?

时间:2019-06-04 17:51:55

标签: string parsing optimization rust

我的问题已得到部分回答,因此我对它进行了修改,以回应从评论和其他实验中学到的东西。

总而言之,我想要一个用于编程比赛的快速I / O例程,在该例程中,一个文件即可解决问题,而无需外部包装。它应该从BufRead(标准输入或文件)中读取一系列由空格分隔的标记。标记可以是整数,浮点数或ASCII词,用空格和换行符分隔,因此似乎我应该普遍支持FromStr类型。一小部分问题是交互式的,这意味着一开始并不是所有输入都可用,但是总会出现完整的一行。

对于上下文,这里是the discussion that led me to post here。有人编写了非常快速的自定义代码来直接从&[u8]的{​​{1}}输出中解析整数,但是在BufRead::fill_buf()中不是通用的。

这是我的best solution so far(强调FromStr结构):

Scanner

通过避免不必要的分配,此use std::io::{self, prelude::*}; fn solve<B: BufRead, W: Write>(mut scan: Scanner<B>, mut w: W) { let n = scan.token(); let mut a = Vec::with_capacity(n); let mut b = Vec::with_capacity(n); for _ in 0..n { a.push(scan.token::<i64>()); b.push(scan.token::<i64>()); } let mut order: Vec<_> = (0..n).collect(); order.sort_by_key(|&i| b[i] - a[i]); let ans: i64 = order .into_iter() .enumerate() .map(|(i, x)| a[x] * i as i64 + b[x] * (n - 1 - i) as i64) .sum(); writeln!(w, "{}", ans); } fn main() { let stdin = io::stdin(); let stdout = io::stdout(); let reader = Scanner::new(stdin.lock()); let writer = io::BufWriter::new(stdout.lock()); solve(reader, writer); } pub struct Scanner<B> { reader: B, buf_str: String, buf_iter: std::str::SplitWhitespace<'static>, } impl<B: BufRead> Scanner<B> { pub fn new(reader: B) -> Self { Self { reader, buf_str: String::new(), buf_iter: "".split_whitespace(), } } pub fn token<T: std::str::FromStr>(&mut self) -> T { loop { if let Some(token) = self.buf_iter.next() { return token.parse().ok().expect("Failed parse"); } self.buf_str.clear(); self.reader .read_line(&mut self.buf_str) .expect("Failed read"); self.buf_iter = unsafe { std::mem::transmute(self.buf_str.split_whitespace()) }; } } } 相当快。如果我们不在乎安全性,可以通过以下方法使速度更快,而不是将Scanner放入read_line(),将String放入read_until(b'\n'),然后再{ {1}}。

但是,我还想知道什么是最快的安全解决方案。是否有一种聪明的方法告诉Rust我的Vec<u8>实现实际上是安全的,因此消除了str::from_utf8_unchecked()?凭直觉,似乎我们应该将Scanner对象认为是拥有缓冲区,直到该缓冲区在返回mem::transmute后被有效删除为止。

在所有其他条件都相同的情况下,我想要一个“不错”的惯用标准库解决方案,因为我正试图向参加编程竞赛的其他人展示Rust。

1 个答案:

答案 0 :(得分:1)

当我在LibCodeJam锈实现中解决了这个确切的问题时,您问到我很高兴。具体来说,TokensReader type以及一些与之相关的小助手会从BufRead读取原始令牌。

这是相关的摘录。此处的基本思想是扫描BufRead::fill_buf缓冲区中的空白,并将非空白字符复制到本地缓冲区中,该本地缓冲区在令牌调用之间重用。找到空格字符或流结束后,本地缓冲区将解释为UTF-8并返回为&str

#[derive(Debug)]
pub enum LoadError {
    Io(io::Error),
    Utf8Error(Utf8Error),
    OutOfTokens,
}

/// TokenBuffer is a resuable buffer into which tokens are
/// read into, one-by-one. It is cleared but not deallocated
/// between each token.
#[derive(Debug)]
struct TokenBuffer(Vec<u8>);

impl TokenBuffer {
    /// Clear the buffer and start reading a new token
    fn lock(&mut self) -> TokenBufferLock {
        self.0.clear();
        TokenBufferLock(&mut self.0)
    }
}

/// TokenBufferLock is a helper type that helps manage the lifecycle
/// of reading a new token, then interpreting it as UTF-8.
#[derive(Debug, Default)]
struct TokenBufferLock<'a>(&'a mut Vec<u8>);

impl<'a> TokenBufferLock<'a> {
    /// Add some bytes to a token
    fn extend(&mut self, chunk: &[u8]) {
        self.0.extend(chunk)
    }

    /// Complete the token and attempt to interpret it as UTF-8
    fn complete(self) -> Result<&'a str, LoadError> {
        from_utf8(self.0).map_err(LoadError::Utf8Error)
    }
}

pub struct TokensReader<R: io::BufRead> {
    reader: R,
    token: TokenBuffer,
}

impl<R: io::BufRead> Tokens for TokensReader<R> {
    fn next_raw(&mut self) -> Result<&str, LoadError> {
        use std::io::ErrorKind::Interrupted;

        // Clear leading whitespace
        loop {
            match self.reader.fill_buf() {
                Err(ref err) if err.kind() == Interrupted => continue,
                Err(err) => return Err(LoadError::Io(err)),
                Ok([]) => return Err(LoadError::OutOfTokens),
                // Got some content; scan for the next non-whitespace character
                Ok(buf) => match buf.iter().position(|byte| !byte.is_ascii_whitespace()) {
                    Some(i) => {
                        self.reader.consume(i);
                        break;
                    }
                    None => self.reader.consume(buf.len()),
                },
            };
        }

        // If we reach this point, there is definitely a non-empty token ready to be read.
        let mut token_buf = self.token.lock();

        loop {
            match self.reader.fill_buf() {
                Err(ref err) if err.kind() == Interrupted => continue,
                Err(err) => return Err(LoadError::Io(err)),
                Ok([]) => return token_buf.complete(),
                // Got some content; scan for the next whitespace character
                Ok(buf) => match buf.iter().position(u8::is_ascii_whitespace) {
                    Some(i) => {
                        token_buf.extend(&buf[..i]);
                        self.reader.consume(i + 1);
                        return token_buf.complete();
                    }
                    None => {
                        token_buf.extend(buf);
                        self.reader.consume(buf.len());
                    }
                },
            }
        }
    }
}

此实现不会处理将字符串解析为FromStr类型的方法(这是单独处理的),但它确实能够正确处理累积的字节,将它们分成空格分隔的标记,并解释这些标记作为UTF-8。它确实假定仅将ASCII空格用于分隔令牌。

值得注意的是,FromStr不能直接在fill_buf缓冲区中使用,因为不能保证令牌不会跨越两个fill_buf调用之间的边界,并且没有强制BufRead读取更多字节的方式,直到现有缓冲区被完全消耗为止。我假设很明显,一旦有了Ok(&str),您就可以在闲暇时对其进行FromStr

此实现不是0副本,而是(摊销)0分配,它可以最大限度地减少不必要的复制或缓冲。它使用单个持久性缓冲区,只有在单个令牌太小时才调整大小,并且在令牌之间重用此缓冲区。字节直接从输入BufRead缓冲区复制到此缓冲区中,而无需进行额外的中间复制。