-
Notifications
You must be signed in to change notification settings - Fork 309
Description
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):
Line 401 in 9a50fd2
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();
});
}