Skip to content

Performance degrades if stdlib function uses io::Error::new() with &'static str #82812

Closed
@Frago9876543210

Description

@Frago9876543210

I am facing a problem while reading different kinds of binary formats. If you are going to read primitives like i32 too often, then performance degrades. This is mainly due to the fact that Error::new(kind, &'static str) returns box. Also it does boxing TWICE!

//stdlib code
enum Repr {
    Os(i32),
    Simple(ErrorKind),
    Custom(Box<Custom>),
}

#[derive(Debug)]
struct Custom {
    kind: ErrorKind,
    error: Box<dyn error::Error + Send + Sync>,
}

doc link: https://doc.rust-lang.org/std/io/struct.Error.html#method.new

I wrote simple bench to show pref problem.

cargo new --vcs=none io-perf
cd io-perf
mkdir benches
cat /dev/null > benches/read.rs
# now you have to modify this file e.g. `subl benches/read.rs`

read.rs:

#![feature(test)]

extern crate test;

use std::hint::black_box;
use std::io::{ErrorKind, Read, Result};
use test::Bencher;

pub trait CustomRead {
    fn read_exact_fast(&mut self, buf: &mut [u8]) -> Result<()>;
}

impl CustomRead for &[u8] {
    /// Clone of `&[u8]::read_exact` without boxing `&'static str`
    #[inline]
    fn read_exact_fast(&mut self, buf: &mut [u8]) -> Result<()> {
        if buf.len() > self.len() {
            // [!] no message here:
            return Err(ErrorKind::UnexpectedEof.into());
        }
        let (a, b) = self.split_at(buf.len());

        if buf.len() == 1 {
            buf[0] = a[0];
        } else {
            buf.copy_from_slice(a);
        }

        *self = b;
        Ok(())
    }
}

impl<R: CustomRead + ?Sized> CustomRead for &mut R {
    #[inline]
    fn read_exact_fast(&mut self, buf: &mut [u8]) -> Result<()> {
        (**self).read_exact_fast(buf)
    }
}

const READ_SIZE: usize = 4; //e.g. reading int x1000 times
const LOOPS: usize = 1000;

#[bench]
fn bench_read_exact_std_err(b: &mut Bencher) {
    b.iter(|| {
        let mut buf = [0u8; READ_SIZE];
        let mut r: &[u8] = &[]; //always would be error

        for _ in 0..LOOPS {
            black_box(Read::read_exact(&mut &*r, &mut buf).is_err());
        }
    });
}

#[bench]
fn bench_read_exact_fast_err(b: &mut Bencher) {
    b.iter(|| {
        let mut buf = [0u8; READ_SIZE];
        let mut r: &[u8] = &[]; //always would be error

        for _ in 0..LOOPS {
            black_box(CustomRead::read_exact_fast(&mut &*r, &mut buf).is_err());
        }
    });
}

#[bench]
fn bench_read_exact_std_ok(b: &mut Bencher) {
    b.iter(|| {
        let mut buf = [0u8; READ_SIZE];
        let mut r: &[u8] = &[0u8; READ_SIZE]; //always would be ok

        for _ in 0..LOOPS {
            black_box(Read::read_exact(&mut &*r, &mut buf).is_ok());
        }
    });
}

#[bench]
fn bench_read_exact_fast_ok(b: &mut Bencher) {
    b.iter(|| {
        let mut buf = [0u8; READ_SIZE];
        let mut r: &[u8] = &[0u8; READ_SIZE]; //always would be ok

        for _ in 0..LOOPS {
            black_box(CustomRead::read_exact_fast(&mut &*r, &mut buf).is_ok());
        }
    });
}

Results

Use cargo bench to get it

running 4 tests
test bench_read_exact_fast_err ... bench:         255 ns/iter (+/- 239)
test bench_read_exact_fast_ok  ... bench:         256 ns/iter (+/- 9)
test bench_read_exact_std_err  ... bench:     164,789 ns/iter (+/- 11,572)
test bench_read_exact_std_ok   ... bench:         256 ns/iter (+/- 11)

We can clearly see that performance is degrading. The more calls it takes to read, the worse the results will be.

Affected crates and stdlib itself

The first very popular crate that comes to mind is byteorder.
https://github.com/BurntSushi/byteorder/blob/5d9d0386488d0763954f14a5cb02b1c0afccbdcb/src/io.rs#L84-L89

It mainly depends on the data format. But what if you need to receive data over the network? The attacker may abuse wrong data to make ton of pointless memory allocations.

stdlib is also use Error::new() often enough. Let's run a simple bash to make sure of this

cd ~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src
grep -r --include="*.rs" -E "ErrorKind::\w+," -A 2 -B 2

In many cases, you will see that it is used &'static str. stdlib sometimes create io::Error with format!() macro, however it doesn't happen very often.

rust/library/std/src/sys/unix/os.rs-            if path_len <= 1 {
rust/library/std/src/sys/unix/os.rs-                return Err(io::Error::new(
rust/library/std/src/sys/unix/os.rs:                    io::ErrorKind::Other,
rust/library/std/src/sys/unix/os.rs-                    "KERN_PROC_PATHNAME sysctl returned zero-length string",
rust/library/std/src/sys/unix/os.rs-                ));
--
rust/library/std/src/sys/unix/os.rs-        }
rust/library/std/src/sys/unix/os.rs-        Err(io::Error::new(
rust/library/std/src/sys/unix/os.rs:            io::ErrorKind::Other,
rust/library/std/src/sys/unix/os.rs-            "/proc/curproc/exe doesn't point to regular file.",
rust/library/std/src/sys/unix/os.rs-        ))

Possible solutions

  • Consider adding a new static method such as io::ErrorKind::with_message(_: &'static str) and new internal Repr member of that's enum.
  • How about using something like Cow, but with static lifetime?

Meta

rustc --version --verbose:

rustc 1.52.0-nightly (94736c434 2021-02-27)
binary: rustc
commit-hash: 94736c434ee154b30e2ec22ec112b79e3f6c5884
commit-date: 2021-02-27
host: x86_64-unknown-linux-gnu
release: 1.52.0-nightly
LLVM version: 11.0.1

Update 07.03.2021: Benchmarks have been improved as per the comment. This should show the problem better.

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-bugCategory: This is a bug.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions