Skip to content

Remove the last format_args! macro uses from hot paths #98

@SpriteOvO

Description

@SpriteOvO

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.

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 of writeln! 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

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(())
})?;

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())?;
}

/// 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

mmstick/numtoa#5

itoa

dtolnay/itoa#46

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions