Skip to content

String formatting is slow. Commands like Print should write strings directly and not use write!. #628

@markus-bauer

Description

@markus-bauer

Background

I was using Print to display the characters of a string one by one, instead of printing the entire string at once. This made me realize that using a lot of Print commands in a cycle is much slower than expected.

Problem

The flamegraph shows that most of the time is spent on core::fmt::write, which is called here (and in other commands):

write!(f, "{}", self.0)

It's known that the write macro (and all string formatting in rust) is considerably slower than writing directly:
rust-lang/rust#10761
rust-lang/rust#76490

The issue is that style::Print doesn't actually need the macro because it doesn't format the string. I suspect that a lot of the commands could avoid the macro and just write the string directly.

Note, that there was a proposed change to the format macro that addresses this case (but it seems to be dead):
rust-lang/rust#76531

Apparently even creating a string with consecutive push_str() is faster than formatting:
rust-lang/rust-clippy#6926

I want to make it clear that it's just an assumption that avoiding the macro will improve performance. Unfortunately the crossterm codebase is designed so much around fmt that I found it difficult to test.
The only quick test I could come up with is to just replicate the core issue. The code below writes a character n times to stdout, using either Print, write!, or write_all.
It looks like it's up to 2x as fast without the macro, but the results are inconsistent between runs.

#![feature(test)]

use crossterm::{style::Print, terminal, QueueableCommand};
use std::io::{stdout, BufWriter, Write};

extern crate test;
use test::Bencher;

enum WriteCharacters {
    Format,
    Direct,
    Crossterm,
}
impl WriteCharacters {
    /// Writes a character n times.
    fn run(&self) {
        // NOTE: make the buffer big enough to avoid flushing.
        let mut bufwriter = BufWriter::with_capacity(100 * 1024, stdout());
        bufwriter.queue(terminal::EnterAlternateScreen);
        let text = "x";
        let n = 1600 * 900;
        for _ in 0..n {
            match self {
                Self::Crossterm => {
                    // using Print:
                    bufwriter.queue(Print(text));
                }
                Self::Format => {
                    // this is like Print and should give the same results:
                    write!(bufwriter, "{}", text);
                }
                Self::Direct => {
                    // this writes the string directly:
                    bufwriter.write_all(text.as_bytes());
                }
            }
        }
        bufwriter.queue(terminal::LeaveAlternateScreen);
        bufwriter.flush();
    }
}

#[bench]
fn with_crossterm(bh: &mut Bencher) {
    bh.iter(|| {
        WriteCharacters::Crossterm.run();
    });
}

#[bench]
fn with_format(bh: &mut Bencher) {
    bh.iter(|| {
        WriteCharacters::Format.run();
    });
}

#[bench]
fn without_format(bh: &mut Bencher) {
    bh.iter(|| {
        WriteCharacters::Direct.run();
    });
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions