Why is my own error code slower than error-chain?

Since error-chain is no longer maintained, I've now switched to my own error module but I notice that it is slower than error-chain and I don't know why.
It is in the source for retest's xerror.rs file which I've also copied below since its only 100 lines.

Is there any way to make it faster (without making it too complicated!)?

use std::error::Error;
use std::fmt;
use std::io;
use std::num;

pub type XResult<T> = Result<T, XError>;

pub fn xerror<T>(message: &str) -> XResult<T> {
    Err(XError::new(&message))
}

#[macro_export]
macro_rules! xerr {
    ($x:expr) => {{return xerror($x);}};
    ($x:expr, $($y:expr),+) => {{return xerror(&format!($x, $($y),+));}};
}

#[derive(Debug)]
pub enum XError {
    Image(image::ImageError),
    Io(::std::io::Error),
    Json(json::Error),
    Log(log::SetLoggerError),
    ParseFloat(::std::num::ParseFloatError),
    ParseInt(::std::num::ParseIntError),
    Rayon(rayon::ThreadPoolBuildError),
    Retest(String),
}

impl Error for XError {}

impl XError {
    pub fn new(message: &str) -> XError {
        XError::Retest(message.to_string())
    }
}

impl fmt::Display for XError {
    fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            XError::Image(ref err) => write!(out, "Image error: {}", err),
            XError::Io(ref err) => write!(out, "File error: {}", err),
            XError::Json(ref err) => write!(out, "JSON error: {}", err),
            XError::Log(ref err) => {
                write!(out, "Failed to set logger: {}", err)
            }
            XError::ParseFloat(ref err) => {
                write!(out, "Failed to read decimal number: {}", err)
            }
            XError::ParseInt(ref err) => {
                write!(out, "Failed to read whole number: {}", err)
            }
            XError::Rayon(ref err) => {
                write!(out, "Failed to create thread pool: {}", err)
            }
            XError::Retest(ref err) => write!(out, "{}", err),
        }
    }
}

impl From<image::ImageError> for XError {
    fn from(err: image::ImageError) -> XError {
        XError::Image(err)
    }
}

impl From<io::Error> for XError {
    fn from(err: io::Error) -> XError {
        XError::Io(err)
    }
}

impl From<json::Error> for XError {
    fn from(err: json::Error) -> XError {
        XError::Json(err)
    }
}

impl From<log::SetLoggerError> for XError {
    fn from(err: log::SetLoggerError) -> XError {
        XError::Log(err)
    }
}

impl From<num::ParseFloatError> for XError {
    fn from(err: num::ParseFloatError) -> XError {
        XError::ParseFloat(err)
    }
}

impl From<num::ParseIntError> for XError {
    fn from(err: num::ParseIntError) -> XError {
        XError::ParseInt(err)
    }
}

impl From<rayon::ThreadPoolBuildError> for XError {
    fn from(err: rayon::ThreadPoolBuildError) -> XError {
        XError::Rayon(err)
    }
}

I have another version of this that's slower still, but used in a private project so can't be posted. In that the errors in the enum are these:

#[derive(Debug)]
pub enum XError {
    Clap(::clap::Error),
    PrivateProject(String),
    Image(image::ImageError),
    Io(::std::io::Error),
    ParseFloat(::std::num::ParseFloatError),
    ParseInt(::std::num::ParseIntError),
    Rayon(rayon::ThreadPoolBuildError),
    SerdeJson(serde_json::error::Error),
    SharedLib(sharedlib::error::Error),
    Utf8(::std::string::FromUtf8Error),
}

For this the fmt() method and the From's have all been implemented in exactly the same way as the retest version.

How are you measuring performance? Is it slower to compile? Slower in some use case with a lot of errors?

2 Likes
  1. You could try to #[inline] all your constructors (xerror, new and from functions) and see if it improves.

  2. But more importantly, I would definitely change your xerr! handling of formatted strings (format! heap-allocates a brand new String only for it to be passed by reference in a function wanting at the end an owned String, hence forced to clone it. Using impl Into<String> is therefore better than &'_ str, since it prevents this kind of misshaps):

    #[inline]
    pub
    fn xerror<T> (message: impl Into<String>) -> XResult<T>
    {
        Err(XError::new(message))
    }
    
    #[macro_export]
    macro_rules! xerr {
        ($msg:expr $(,)?) => (return xerror($msg));
        ($fmt:expr $(, $y:expr)+ $(,)?) => (return xerror(format!($fmt, $($y),*)));
    }
    
    impl XError {
        #[inline]
        pub
        fn new (message: impl Into<String>) -> XError
        {
            XError::Retest(message.into())
        }
    }
    

Sorry I didn't specify: slower at runtime even when there are no errors.

You could try activating #![warn(variant_size_differences)] and also checking the overall size of your error type. If it is big enough then it will be bloating the size of your results and requiring extra copying, in those cases you may want to box either the error enum as a whole, or some variants if most are small.

1 Like

I've now incorporated all your suggestions in retest. I've also applied them all to the private project I'm working on. It is still a little slower than error-chain but much better than it was. Thank you.

Thanks. I tried the #![warn... you suggested but it did not produce any compile-time output at all. I didn't want to box since that introduces its own overhead.

Boxing errors should be generally ok. The error path is already the cold path so paying a little more overhead when on it isn't a big deal.

The problem with large error types is that they can really increase the copying when returning from functions that have small Ok types. Here's a playground showing the sizes of your error (less the json error as that's not available on the playground) vs a version of it that boxes the larger chained errors and a version that boxes the whole error. For relatively common Ok types like Result<Vec<u8>, _> you can reduce the amount of extra copying (on both the hot and cold paths) by a third by just boxing the larger of your chain errors. And if you commonly return small Ok types wrapping the entire error in a box can give you another halving.

This is why error-chain actually created something closer to

enum ErrorKind {
    Image,
    Io,
    ...,
}

struct Error {
    kind: ErrorKind,
    source: Box<dyn std::error::Error>,
}

The downside of putting the error next to the kind like this is you lose out on the per-variant type information of the error, so if for some reason a user wanted to inspect what specific image::ImageError occurred they can't. That's why either boxing inside individual variants or boxing the whole error can give you a lot of the upside of allowing you to expose this type information while reducing the copying overhead.

1 Like

Thanks for those measurements.
I've now changed my fundamental error type to this:

pub type XResult<T> = Result<T, Box<XError>>;

This meant updating the xerror function, the XError::new method, and changing all the From implementations.
For one short test (9-13 secs) the variability was too great to say which was faster, but for a longer series of tests (approx 2 mins) the boxed version is slightly faster. I'll do a longer test series (approx 15 mins) to see, but it looks like your idea has helped.
Thanks.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.