-
Notifications
You must be signed in to change notification settings - Fork 15
Description
Background
In the early stages of spdlog-rs development a few years ago, we had already noticed that the standard library's write!
macros are very slow, and this could be proven by simple benchmarking.
- Related issue in Rust: format_args! is slow rust-lang/rust#76490
- There's also a blog "Behind the Scenes of Rust String Formatting: format_args!()" by m-ou-se that explains more details.
In spdlog-rs, we've avoided using write!
macros in formatters and sinks as much as possible, and instead use .write_str()
to put things together manually, for example
- Commit 9ff8b01: Use
write_all
instead ofwriteln!
to improve performance slightly - Commit 913234e: Improve performance of built-in patterns
Because of these optimizations, performance in formatting has been significantly improved. However, there are still some remaining uses of format_args!
that have not been completely removed, for example
spdlog-rs/spdlog/src/formatter/full_formatter.rs
Lines 65 to 72 in 654b440
fmt_with_time(ctx, record, |mut time: TimeDate| { | |
dest.write_str("[")?; | |
dest.write_str(time.full_second_str())?; | |
dest.write_str(".")?; | |
write!(dest, "{:03}", time.millisecond())?; | |
dest.write_str("] [")?; | |
Ok(()) | |
})?; |
spdlog-rs/spdlog/src/formatter/full_formatter.rs
Lines 85 to 92 in 654b440
if let Some(srcloc) = record.source_location() { | |
dest.write_str("] [")?; | |
dest.write_str(srcloc.module_path())?; | |
dest.write_str(", ")?; | |
dest.write_str(srcloc.file())?; | |
dest.write_str(":")?; | |
write!(dest, "{}", srcloc.line())?; | |
} |
spdlog-rs/spdlog/src/formatter/pattern_formatter/pattern/datetime.rs
Lines 326 to 340 in 654b440
/// A pattern that writes the nanosecond part within a second of the timestamp | |
/// of a log record into the output. Example: `482930154`. | |
#[derive(Clone, Default)] | |
pub struct Nanosecond; | |
impl Pattern for Nanosecond { | |
fn format( | |
&self, | |
_record: &Record, | |
dest: &mut StringBuf, | |
ctx: &mut PatternContext, | |
) -> crate::Result<()> { | |
write!(dest, "{:09}", ctx.time_date().nanosecond()).map_err(Error::FormatRecord) | |
} | |
} |
What they have in common is that they write an integer to buffer and may with padding. However, writing integers without allocation with just stdlib is not possible in stable Rust today. If you're in nightly Rust, you can do this manually and probably improve performance.
use std::fmt::Display as _;
let mut opt = std::fmt::FormattingOptions::new(); // unstable
opt.width(Some(9)).fill('0'); // unstable
nanosecond.fmt(std::fmt::Formatter::new(&mut dest, opt) /* unstable */);
Solution
I found two crates that can convert integers to &str
or &[u8]
without allocation, which will most likely allow us to get rid of the last uses of format_args!
and improve performance a bit further.
numtoa
itoa
They each have some issues and I'm not sure which one is better, so I'll test and benchmark them separately for spdlog-rs and then decide which one to use.