Skip to content

Commit 026c66d

Browse files
authored
Use Custom Formatting for Integer Parts of Times (#577)
Going through the standard library's formatter is usually quite slow. And usually we want to show either minutes or seconds that are in the range from 0 to 59, or even tenths from 0 to 99. For those we can just define a lookup table where we look up the formatted strings and directly call `write_str` on the formatter instead of going through the `write!` macro, which would go through a lot more setup. Additionally we already have `itoa` as an indirect dependency which we now directly use for formatting integers that are not bound that way. `itoa` generally does the same algorithm as `std` but does so without going through any formatting machinery and is thus a lot faster, but also much less customizable. Overall this and #576 together result in a `~3.86x` performance improvement when formatting a time. | When | Time | |:------------|------------:| | Both PRs | `59.205 ns` | | Previous PR | `154.63 ns` | | Before | `228.47 ns` |
1 parent 66fab23 commit 026c66d

10 files changed

Lines changed: 254 additions & 77 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ members = ["capi", "capi/bind_gen", "crates/*"]
3737
base64 = { version = "0.13.0", default-features = false, features = ["alloc"] }
3838
bytemuck = { version = "1.9.1", default-features = false, features = ["derive"] }
3939
cfg-if = "1.0.0"
40+
itoa = { version = "1.0.3", default-features = false }
4041
time = { version = "0.3.3", default-features = false }
4142
hashbrown = "0.12.0"
4243
libm = "0.2.1"

src/timing/formatter/accuracy.rs

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
use super::{format_padded, NANOS_PER_HUNDREDTH, NANOS_PER_MILLI, NANOS_PER_TENTH};
2+
use core::{
3+
fmt::{Display, Formatter, Result},
4+
str,
5+
};
16
use serde::{Deserialize, Serialize};
27

38
/// The Accuracy describes how many digits to show for the fractional part of a
@@ -16,29 +21,110 @@ pub enum Accuracy {
1621

1722
impl Accuracy {
1823
/// Formats the nanoseconds provided with the chosen accuracy.
19-
pub const fn format_nanoseconds(self, nanoseconds: u32) -> FormattedSeconds {
20-
FormattedSeconds {
24+
pub const fn format_nanoseconds(self, nanoseconds: u32) -> FractionalPart {
25+
FractionalPart {
2126
accuracy: self,
2227
nanoseconds,
2328
}
2429
}
2530
}
2631

27-
use core::fmt::{Display, Formatter, Result};
28-
2932
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
30-
pub struct FormattedSeconds {
33+
pub struct FractionalPart {
3134
accuracy: Accuracy,
3235
nanoseconds: u32,
3336
}
3437

35-
impl Display for FormattedSeconds {
38+
impl Display for FractionalPart {
3639
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
3740
match self.accuracy {
3841
Accuracy::Seconds => Ok(()),
39-
Accuracy::Tenths => write!(f, ".{}", self.nanoseconds / 100_000_000),
40-
Accuracy::Hundredths => write!(f, ".{:02}", self.nanoseconds / 10_000_000),
41-
Accuracy::Milliseconds => write!(f, ".{:03}", self.nanoseconds / 1_000_000),
42+
Accuracy::Tenths => {
43+
f.write_str(".")?;
44+
let v = (self.nanoseconds / NANOS_PER_TENTH) as u8;
45+
assert!(v < 10);
46+
// SAFETY: We ensured the value is between 0 and 10, so adding
47+
// that on top of ASCII '0' ensures us that we get an ASCII
48+
// digit.
49+
unsafe { f.write_str(str::from_utf8_unchecked(&[v + b'0'])) }
50+
}
51+
Accuracy::Hundredths => {
52+
f.write_str(".")?;
53+
f.write_str(format_padded(
54+
(self.nanoseconds / NANOS_PER_HUNDREDTH) as u8,
55+
))
56+
}
57+
Accuracy::Milliseconds => {
58+
f.write_str(".")?;
59+
let first = (self.nanoseconds / NANOS_PER_TENTH) as u8;
60+
let second_and_third =
61+
((self.nanoseconds % NANOS_PER_TENTH) / NANOS_PER_MILLI) as u8;
62+
assert!(first < 10);
63+
// SAFETY: We ensured the value is between 0 and 10, so adding
64+
// that on top of ASCII '0' ensures us that we get an ASCII
65+
// digit.
66+
unsafe {
67+
f.write_str(str::from_utf8_unchecked(&[first + b'0']))?;
68+
}
69+
f.write_str(format_padded(second_and_third))
70+
}
4271
}
4372
}
4473
}
74+
75+
#[cfg(test)]
76+
mod tests {
77+
use super::*;
78+
79+
#[test]
80+
fn format_seconds() {
81+
let acc = Accuracy::Seconds;
82+
assert_eq!(acc.format_nanoseconds(0).to_string(), "");
83+
assert_eq!(acc.format_nanoseconds(1).to_string(), "");
84+
assert_eq!(acc.format_nanoseconds(789_654_321).to_string(), "");
85+
assert_eq!(acc.format_nanoseconds(7_654_321).to_string(), "");
86+
assert_eq!(acc.format_nanoseconds(70_654_321).to_string(), "");
87+
assert_eq!(acc.format_nanoseconds(700_654_321).to_string(), "");
88+
assert_eq!(acc.format_nanoseconds(109_654_321).to_string(), "");
89+
assert_eq!(acc.format_nanoseconds(999_999_999).to_string(), "");
90+
}
91+
92+
#[test]
93+
fn format_tenths() {
94+
let acc = Accuracy::Tenths;
95+
assert_eq!(acc.format_nanoseconds(0).to_string(), ".0");
96+
assert_eq!(acc.format_nanoseconds(1).to_string(), ".0");
97+
assert_eq!(acc.format_nanoseconds(789_654_321).to_string(), ".7");
98+
assert_eq!(acc.format_nanoseconds(7_654_321).to_string(), ".0");
99+
assert_eq!(acc.format_nanoseconds(70_654_321).to_string(), ".0");
100+
assert_eq!(acc.format_nanoseconds(700_654_321).to_string(), ".7");
101+
assert_eq!(acc.format_nanoseconds(109_654_321).to_string(), ".1");
102+
assert_eq!(acc.format_nanoseconds(999_999_999).to_string(), ".9");
103+
}
104+
105+
#[test]
106+
fn format_hundredths() {
107+
let acc = Accuracy::Hundredths;
108+
assert_eq!(acc.format_nanoseconds(0).to_string(), ".00");
109+
assert_eq!(acc.format_nanoseconds(1).to_string(), ".00");
110+
assert_eq!(acc.format_nanoseconds(789_654_321).to_string(), ".78");
111+
assert_eq!(acc.format_nanoseconds(7_654_321).to_string(), ".00");
112+
assert_eq!(acc.format_nanoseconds(70_654_321).to_string(), ".07");
113+
assert_eq!(acc.format_nanoseconds(700_654_321).to_string(), ".70");
114+
assert_eq!(acc.format_nanoseconds(109_654_321).to_string(), ".10");
115+
assert_eq!(acc.format_nanoseconds(999_999_999).to_string(), ".99");
116+
}
117+
118+
#[test]
119+
fn format_milliseconds() {
120+
let acc = Accuracy::Milliseconds;
121+
assert_eq!(acc.format_nanoseconds(0).to_string(), ".000");
122+
assert_eq!(acc.format_nanoseconds(1).to_string(), ".000");
123+
assert_eq!(acc.format_nanoseconds(789_654_321).to_string(), ".789");
124+
assert_eq!(acc.format_nanoseconds(7_654_321).to_string(), ".007");
125+
assert_eq!(acc.format_nanoseconds(70_654_321).to_string(), ".070");
126+
assert_eq!(acc.format_nanoseconds(700_654_321).to_string(), ".700");
127+
assert_eq!(acc.format_nanoseconds(109_654_321).to_string(), ".109");
128+
assert_eq!(acc.format_nanoseconds(999_999_999).to_string(), ".999");
129+
}
130+
}

src/timing/formatter/complete.rs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use super::{TimeFormatter, ASCII_MINUS, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE};
1+
use super::{
2+
format_padded, TimeFormatter, ASCII_MINUS, SECONDS_PER_DAY, SECONDS_PER_HOUR,
3+
SECONDS_PER_MINUTE,
4+
};
25
use crate::TimeSpan;
36
use core::fmt::{Display, Formatter, Result};
47

@@ -57,14 +60,27 @@ impl Display for Inner {
5760
// calculate all of them in parallel. On top of that they are
5861
// integer divisions of known constants, which get turned into
5962
// multiplies and shifts, which is very fast.
60-
let seconds = total_seconds % SECONDS_PER_MINUTE;
61-
let minutes = (total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
62-
let hours = (total_seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR;
63+
let seconds = (total_seconds % SECONDS_PER_MINUTE) as u8;
64+
let minutes = ((total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8;
65+
let hours = ((total_seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR) as u8;
6366
let days = total_seconds / SECONDS_PER_DAY;
67+
68+
let mut buffer = itoa::Buffer::new();
69+
6470
if days > 0 {
65-
write!(f, "{days}.")?;
71+
f.write_str(buffer.format(days))?;
72+
f.write_str(".")?;
6673
}
67-
write!(f, "{hours:02}:{minutes:02}:{seconds:02}.{nanoseconds:09}")
74+
75+
f.write_str(format_padded(hours))?;
76+
f.write_str(":")?;
77+
f.write_str(format_padded(minutes))?;
78+
f.write_str(":")?;
79+
f.write_str(format_padded(seconds))?;
80+
f.write_str(".")?;
81+
let nanoseconds = buffer.format(nanoseconds);
82+
f.write_str(&"000000000"[nanoseconds.len()..])?;
83+
f.write_str(nanoseconds)
6884
} else {
6985
f.write_str("00:00:00.000000000")
7086
}

src/timing/formatter/days.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use super::{TimeFormatter, MINUS, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE};
1+
use super::{
2+
format_padded, TimeFormatter, MINUS, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE,
3+
};
24
use crate::TimeSpan;
35
use core::fmt::{Display, Formatter, Result};
46

@@ -54,20 +56,27 @@ impl Display for Inner {
5456
// calculate all of them in parallel. On top of that they are
5557
// integer divisions of known constants, which get turned into
5658
// multiplies and shifts, which is very fast.
57-
let seconds = total_seconds % SECONDS_PER_MINUTE;
58-
let minutes = (total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
59-
let hours = (total_seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR;
59+
let seconds = (total_seconds % SECONDS_PER_MINUTE) as u8;
60+
let minutes = ((total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8;
61+
let hours = ((total_seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR) as u8;
6062
let days = total_seconds / SECONDS_PER_DAY;
6163

64+
let mut buffer = itoa::Buffer::new();
65+
6266
if days > 0 {
63-
write!(f, "{days}d ")?;
67+
f.write_str(buffer.format(days))?;
68+
f.write_str("d ")?;
6469
}
6570

6671
if days > 0 || hours > 0 {
67-
write!(f, "{hours}:{minutes:02}:{seconds:02}")
72+
f.write_str(buffer.format(hours))?;
73+
f.write_str(":")?;
74+
f.write_str(format_padded(minutes))?;
6875
} else {
69-
write!(f, "{minutes}:{seconds:02}")
76+
f.write_str(buffer.format(minutes))?;
7077
}
78+
f.write_str(":")?;
79+
f.write_str(format_padded(seconds))
7180
} else {
7281
f.write_str("0:00")
7382
}

src/timing/formatter/delta.rs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use super::{Accuracy, TimeFormatter, DASH, MINUS, PLUS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE};
1+
use super::{
2+
format_padded, Accuracy, TimeFormatter, DASH, MINUS, PLUS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE,
3+
};
24
use crate::TimeSpan;
35
use core::fmt::{Display, Formatter, Result};
46

@@ -83,22 +85,28 @@ impl Display for Inner {
8385
// calculate all of them in parallel. On top of that they are
8486
// integer divisions of known constants, which get turned into
8587
// multiplies and shifts, which is very fast.
86-
let seconds = total_seconds % SECONDS_PER_MINUTE;
87-
let minutes = (total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
88+
let seconds = (total_seconds % SECONDS_PER_MINUTE) as u8;
89+
let minutes = ((total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8;
8890
let hours = total_seconds / SECONDS_PER_HOUR;
91+
92+
let mut buffer = itoa::Buffer::new();
93+
8994
if hours > 0 {
90-
write!(f, "{hours}:{minutes:02}:{seconds:02}")?;
95+
f.write_str(buffer.format(hours))?;
96+
f.write_str(":")?;
97+
f.write_str(format_padded(minutes))?;
98+
f.write_str(":")?;
99+
f.write_str(format_padded(seconds))?;
91100
} else if minutes > 0 {
92-
write!(f, "{minutes}:{seconds:02}")?;
101+
f.write_str(buffer.format(minutes))?;
102+
f.write_str(":")?;
103+
f.write_str(format_padded(seconds))?;
93104
} else {
94-
return write!(
95-
f,
96-
"{seconds}{}",
97-
self.accuracy.format_nanoseconds(nanoseconds)
98-
);
105+
f.write_str(buffer.format(seconds))?;
106+
return self.accuracy.format_nanoseconds(nanoseconds).fmt(f);
99107
}
100108
if !self.drop_decimals {
101-
write!(f, "{}", self.accuracy.format_nanoseconds(nanoseconds))
109+
self.accuracy.format_nanoseconds(nanoseconds).fmt(f)
102110
} else {
103111
Ok(())
104112
}

src/timing/formatter/mod.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub use self::{
3636
};
3737

3838
use crate::TimeSpan;
39-
use core::fmt::Display;
39+
use core::{fmt::Display, str};
4040

4141
/// Time Formatters can be used to format optional Time Spans in various ways.
4242
pub trait TimeFormatter<'a> {
@@ -63,3 +63,33 @@ pub const PLUS: &str = "+";
6363
const SECONDS_PER_MINUTE: u64 = 60;
6464
const SECONDS_PER_HOUR: u64 = 60 * SECONDS_PER_MINUTE;
6565
const SECONDS_PER_DAY: u64 = 24 * SECONDS_PER_HOUR;
66+
67+
const NANOS_PER_MILLI: u32 = 1_000_000;
68+
const NANOS_PER_HUNDREDTH: u32 = 10_000_000;
69+
const NANOS_PER_TENTH: u32 = 100_000_000;
70+
71+
#[rustfmt::skip]
72+
static LOOKUP: [[u8; 2]; 100] = [
73+
*b"00", *b"01", *b"02", *b"03", *b"04", *b"05", *b"06", *b"07", *b"08", *b"09",
74+
*b"10", *b"11", *b"12", *b"13", *b"14", *b"15", *b"16", *b"17", *b"18", *b"19",
75+
*b"20", *b"21", *b"22", *b"23", *b"24", *b"25", *b"26", *b"27", *b"28", *b"29",
76+
*b"30", *b"31", *b"32", *b"33", *b"34", *b"35", *b"36", *b"37", *b"38", *b"39",
77+
*b"40", *b"41", *b"42", *b"43", *b"44", *b"45", *b"46", *b"47", *b"48", *b"49",
78+
*b"50", *b"51", *b"52", *b"53", *b"54", *b"55", *b"56", *b"57", *b"58", *b"59",
79+
*b"60", *b"61", *b"62", *b"63", *b"64", *b"65", *b"66", *b"67", *b"68", *b"69",
80+
*b"70", *b"71", *b"72", *b"73", *b"74", *b"75", *b"76", *b"77", *b"78", *b"79",
81+
*b"80", *b"81", *b"82", *b"83", *b"84", *b"85", *b"86", *b"87", *b"88", *b"89",
82+
*b"90", *b"91", *b"92", *b"93", *b"94", *b"95", *b"96", *b"97", *b"98", *b"99",
83+
];
84+
85+
#[inline(always)]
86+
fn format_padded(x: u8) -> &'static str {
87+
// SAFETY: The lookup table is always initialized with valid UTF-8.
88+
unsafe { str::from_utf8_unchecked(&LOOKUP[x as usize]) }
89+
}
90+
91+
#[inline(always)]
92+
fn format_unpadded(x: u8) -> &'static str {
93+
// SAFETY: The lookup table is always initialized with valid UTF-8.
94+
unsafe { str::from_utf8_unchecked(&LOOKUP[x as usize][(x < 10) as usize..]) }
95+
}

src/timing/formatter/none_wrapper.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ impl<'a, F: 'a + TimeFormatter<'a>, S: 'a + AsRef<str>> TimeFormatter<'a> for No
6565
impl<'a, F: TimeFormatter<'a>, S: 'a + AsRef<str>> Display for Inner<'a, F, S> {
6666
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
6767
if self.time.is_none() {
68-
write!(f, "{}", self.wrapper.1.as_ref())
68+
self.wrapper.1.as_ref().fmt(f)
6969
} else {
70-
write!(f, "{}", self.wrapper.0.format(self.time))
70+
self.wrapper.0.format(self.time).fmt(f)
7171
}
7272
}
7373
}

src/timing/formatter/regular.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use super::{Accuracy, TimeFormatter, DASH, MINUS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE};
1+
use super::{
2+
format_padded, Accuracy, TimeFormatter, DASH, MINUS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE,
3+
};
24
use crate::TimeSpan;
35
use core::fmt::{Display, Formatter, Result};
46

@@ -76,22 +78,22 @@ impl Display for Inner {
7678
// calculate all of them in parallel. On top of that they are
7779
// integer divisions of known constants, which get turned into
7880
// multiplies and shifts, which is very fast.
79-
let seconds = total_seconds % SECONDS_PER_MINUTE;
80-
let minutes = (total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
81+
let seconds = (total_seconds % SECONDS_PER_MINUTE) as u8;
82+
let minutes = ((total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8;
8183
let hours = total_seconds / SECONDS_PER_HOUR;
84+
85+
let mut buffer = itoa::Buffer::new();
86+
8287
if hours > 0 {
83-
write!(
84-
f,
85-
"{hours}:{minutes:02}:{seconds:02}{}",
86-
self.accuracy.format_nanoseconds(nanoseconds)
87-
)
88+
f.write_str(buffer.format(hours))?;
89+
f.write_str(":")?;
90+
f.write_str(format_padded(minutes))?;
8891
} else {
89-
write!(
90-
f,
91-
"{minutes}:{seconds:02}{}",
92-
self.accuracy.format_nanoseconds(nanoseconds)
93-
)
92+
f.write_str(buffer.format(minutes))?;
9493
}
94+
f.write_str(":")?;
95+
f.write_str(format_padded(seconds))?;
96+
self.accuracy.format_nanoseconds(nanoseconds).fmt(f)
9597
} else {
9698
f.write_str(DASH)
9799
}

0 commit comments

Comments
 (0)