Skip to content

Commit

Permalink
Use Custom Formatting for Integer Parts of Times
Browse files Browse the repository at this point in the history
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 LiveSplit#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` |
  • Loading branch information
CryZe committed Oct 6, 2022
1 parent 66fab23 commit d93e4f3
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 77 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ members = ["capi", "capi/bind_gen", "crates/*"]
base64 = { version = "0.13.0", default-features = false, features = ["alloc"] }
bytemuck = { version = "1.9.1", default-features = false, features = ["derive"] }
cfg-if = "1.0.0"
itoa = { version = "1.0.3", default-features = false }
time = { version = "0.3.3", default-features = false }
hashbrown = "0.12.0"
libm = "0.2.1"
Expand Down
104 changes: 95 additions & 9 deletions src/timing/formatter/accuracy.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
use super::{format_padded, NANOS_PER_HUNDREDTH, NANOS_PER_MILLI, NANOS_PER_TENTH};
use core::{
fmt::{Display, Formatter, Result},
str,
};
use serde::{Deserialize, Serialize};

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

impl Accuracy {
/// Formats the nanoseconds provided with the chosen accuracy.
pub const fn format_nanoseconds(self, nanoseconds: u32) -> FormattedSeconds {
FormattedSeconds {
pub const fn format_nanoseconds(self, nanoseconds: u32) -> FractionalPart {
FractionalPart {
accuracy: self,
nanoseconds,
}
}
}

use core::fmt::{Display, Formatter, Result};

#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub struct FormattedSeconds {
pub struct FractionalPart {
accuracy: Accuracy,
nanoseconds: u32,
}

impl Display for FormattedSeconds {
impl Display for FractionalPart {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self.accuracy {
Accuracy::Seconds => Ok(()),
Accuracy::Tenths => write!(f, ".{}", self.nanoseconds / 100_000_000),
Accuracy::Hundredths => write!(f, ".{:02}", self.nanoseconds / 10_000_000),
Accuracy::Milliseconds => write!(f, ".{:03}", self.nanoseconds / 1_000_000),
Accuracy::Tenths => {
f.write_str(".")?;
let v = (self.nanoseconds / NANOS_PER_TENTH) as u8;
assert!(v < 10);
// SAFETY: We ensured the value is between 0 and 10, so adding
// that on top of ASCII '0' ensures us that we get an ASCII
// digit.
unsafe { f.write_str(str::from_utf8_unchecked(&[v + b'0'])) }
}
Accuracy::Hundredths => {
f.write_str(".")?;
f.write_str(format_padded(
(self.nanoseconds / NANOS_PER_HUNDREDTH) as u8,
))
}
Accuracy::Milliseconds => {
f.write_str(".")?;
let first = (self.nanoseconds / NANOS_PER_TENTH) as u8;
let second_and_third =
((self.nanoseconds % NANOS_PER_TENTH) / NANOS_PER_MILLI) as u8;
assert!(first < 10);
// SAFETY: We ensured the value is between 0 and 10, so adding
// that on top of ASCII '0' ensures us that we get an ASCII
// digit.
unsafe {
f.write_str(str::from_utf8_unchecked(&[first + b'0']))?;
}
f.write_str(format_padded(second_and_third))
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn format_seconds() {
let acc = Accuracy::Seconds;
assert_eq!(acc.format_nanoseconds(0).to_string(), "");
assert_eq!(acc.format_nanoseconds(1).to_string(), "");
assert_eq!(acc.format_nanoseconds(789_654_321).to_string(), "");
assert_eq!(acc.format_nanoseconds(7_654_321).to_string(), "");
assert_eq!(acc.format_nanoseconds(70_654_321).to_string(), "");
assert_eq!(acc.format_nanoseconds(700_654_321).to_string(), "");
assert_eq!(acc.format_nanoseconds(109_654_321).to_string(), "");
assert_eq!(acc.format_nanoseconds(999_999_999).to_string(), "");
}

#[test]
fn format_tenths() {
let acc = Accuracy::Tenths;
assert_eq!(acc.format_nanoseconds(0).to_string(), ".0");
assert_eq!(acc.format_nanoseconds(1).to_string(), ".0");
assert_eq!(acc.format_nanoseconds(789_654_321).to_string(), ".7");
assert_eq!(acc.format_nanoseconds(7_654_321).to_string(), ".0");
assert_eq!(acc.format_nanoseconds(70_654_321).to_string(), ".0");
assert_eq!(acc.format_nanoseconds(700_654_321).to_string(), ".7");
assert_eq!(acc.format_nanoseconds(109_654_321).to_string(), ".1");
assert_eq!(acc.format_nanoseconds(999_999_999).to_string(), ".9");
}

#[test]
fn format_hundredths() {
let acc = Accuracy::Hundredths;
assert_eq!(acc.format_nanoseconds(0).to_string(), ".00");
assert_eq!(acc.format_nanoseconds(1).to_string(), ".00");
assert_eq!(acc.format_nanoseconds(789_654_321).to_string(), ".78");
assert_eq!(acc.format_nanoseconds(7_654_321).to_string(), ".00");
assert_eq!(acc.format_nanoseconds(70_654_321).to_string(), ".07");
assert_eq!(acc.format_nanoseconds(700_654_321).to_string(), ".70");
assert_eq!(acc.format_nanoseconds(109_654_321).to_string(), ".10");
assert_eq!(acc.format_nanoseconds(999_999_999).to_string(), ".99");
}

#[test]
fn format_milliseconds() {
let acc = Accuracy::Milliseconds;
assert_eq!(acc.format_nanoseconds(0).to_string(), ".000");
assert_eq!(acc.format_nanoseconds(1).to_string(), ".000");
assert_eq!(acc.format_nanoseconds(789_654_321).to_string(), ".789");
assert_eq!(acc.format_nanoseconds(7_654_321).to_string(), ".007");
assert_eq!(acc.format_nanoseconds(70_654_321).to_string(), ".070");
assert_eq!(acc.format_nanoseconds(700_654_321).to_string(), ".700");
assert_eq!(acc.format_nanoseconds(109_654_321).to_string(), ".109");
assert_eq!(acc.format_nanoseconds(999_999_999).to_string(), ".999");
}
}
28 changes: 22 additions & 6 deletions src/timing/formatter/complete.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use super::{TimeFormatter, ASCII_MINUS, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE};
use super::{
format_padded, TimeFormatter, ASCII_MINUS, SECONDS_PER_DAY, SECONDS_PER_HOUR,
SECONDS_PER_MINUTE,
};
use crate::TimeSpan;
use core::fmt::{Display, Formatter, Result};

Expand Down Expand Up @@ -57,14 +60,27 @@ impl Display for Inner {
// calculate all of them in parallel. On top of that they are
// integer divisions of known constants, which get turned into
// multiplies and shifts, which is very fast.
let seconds = total_seconds % SECONDS_PER_MINUTE;
let minutes = (total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
let hours = (total_seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR;
let seconds = (total_seconds % SECONDS_PER_MINUTE) as u8;
let minutes = ((total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8;
let hours = ((total_seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR) as u8;
let days = total_seconds / SECONDS_PER_DAY;

let mut buffer = itoa::Buffer::new();

if days > 0 {
write!(f, "{days}.")?;
f.write_str(buffer.format(days))?;
f.write_str(".")?;
}
write!(f, "{hours:02}:{minutes:02}:{seconds:02}.{nanoseconds:09}")

f.write_str(format_padded(hours))?;
f.write_str(":")?;
f.write_str(format_padded(minutes))?;
f.write_str(":")?;
f.write_str(format_padded(seconds))?;
f.write_str(".")?;
let nanoseconds = buffer.format(nanoseconds);
f.write_str(&"000000000"[nanoseconds.len()..])?;
f.write_str(nanoseconds)
} else {
f.write_str("00:00:00.000000000")
}
Expand Down
23 changes: 16 additions & 7 deletions src/timing/formatter/days.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use super::{TimeFormatter, MINUS, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE};
use super::{
format_padded, TimeFormatter, MINUS, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE,
};
use crate::TimeSpan;
use core::fmt::{Display, Formatter, Result};

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

let mut buffer = itoa::Buffer::new();

if days > 0 {
write!(f, "{days}d ")?;
f.write_str(buffer.format(days))?;
f.write_str("d ")?;
}

if days > 0 || hours > 0 {
write!(f, "{hours}:{minutes:02}:{seconds:02}")
f.write_str(buffer.format(hours))?;
f.write_str(":")?;
f.write_str(format_padded(minutes))?;
} else {
write!(f, "{minutes}:{seconds:02}")
f.write_str(buffer.format(minutes))?;
}
f.write_str(":")?;
f.write_str(format_padded(seconds))
} else {
f.write_str("0:00")
}
Expand Down
30 changes: 19 additions & 11 deletions src/timing/formatter/delta.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use super::{Accuracy, TimeFormatter, DASH, MINUS, PLUS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE};
use super::{
format_padded, Accuracy, TimeFormatter, DASH, MINUS, PLUS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE,
};
use crate::TimeSpan;
use core::fmt::{Display, Formatter, Result};

Expand Down Expand Up @@ -83,22 +85,28 @@ impl Display for Inner {
// calculate all of them in parallel. On top of that they are
// integer divisions of known constants, which get turned into
// multiplies and shifts, which is very fast.
let seconds = total_seconds % SECONDS_PER_MINUTE;
let minutes = (total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
let seconds = (total_seconds % SECONDS_PER_MINUTE) as u8;
let minutes = ((total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8;
let hours = total_seconds / SECONDS_PER_HOUR;

let mut buffer = itoa::Buffer::new();

if hours > 0 {
write!(f, "{hours}:{minutes:02}:{seconds:02}")?;
f.write_str(buffer.format(hours))?;
f.write_str(":")?;
f.write_str(format_padded(minutes))?;
f.write_str(":")?;
f.write_str(format_padded(seconds))?;
} else if minutes > 0 {
write!(f, "{minutes}:{seconds:02}")?;
f.write_str(buffer.format(minutes))?;
f.write_str(":")?;
f.write_str(format_padded(seconds))?;
} else {
return write!(
f,
"{seconds}{}",
self.accuracy.format_nanoseconds(nanoseconds)
);
f.write_str(buffer.format(seconds))?;
return self.accuracy.format_nanoseconds(nanoseconds).fmt(f);
}
if !self.drop_decimals {
write!(f, "{}", self.accuracy.format_nanoseconds(nanoseconds))
self.accuracy.format_nanoseconds(nanoseconds).fmt(f)
} else {
Ok(())
}
Expand Down
32 changes: 31 additions & 1 deletion src/timing/formatter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub use self::{
};

use crate::TimeSpan;
use core::fmt::Display;
use core::{fmt::Display, str};

/// Time Formatters can be used to format optional Time Spans in various ways.
pub trait TimeFormatter<'a> {
Expand All @@ -63,3 +63,33 @@ pub const PLUS: &str = "+";
const SECONDS_PER_MINUTE: u64 = 60;
const SECONDS_PER_HOUR: u64 = 60 * SECONDS_PER_MINUTE;
const SECONDS_PER_DAY: u64 = 24 * SECONDS_PER_HOUR;

const NANOS_PER_MILLI: u32 = 1_000_000;
const NANOS_PER_HUNDREDTH: u32 = 10_000_000;
const NANOS_PER_TENTH: u32 = 100_000_000;

#[rustfmt::skip]
static LOOKUP: [[u8; 2]; 100] = [
*b"00", *b"01", *b"02", *b"03", *b"04", *b"05", *b"06", *b"07", *b"08", *b"09",
*b"10", *b"11", *b"12", *b"13", *b"14", *b"15", *b"16", *b"17", *b"18", *b"19",
*b"20", *b"21", *b"22", *b"23", *b"24", *b"25", *b"26", *b"27", *b"28", *b"29",
*b"30", *b"31", *b"32", *b"33", *b"34", *b"35", *b"36", *b"37", *b"38", *b"39",
*b"40", *b"41", *b"42", *b"43", *b"44", *b"45", *b"46", *b"47", *b"48", *b"49",
*b"50", *b"51", *b"52", *b"53", *b"54", *b"55", *b"56", *b"57", *b"58", *b"59",
*b"60", *b"61", *b"62", *b"63", *b"64", *b"65", *b"66", *b"67", *b"68", *b"69",
*b"70", *b"71", *b"72", *b"73", *b"74", *b"75", *b"76", *b"77", *b"78", *b"79",
*b"80", *b"81", *b"82", *b"83", *b"84", *b"85", *b"86", *b"87", *b"88", *b"89",
*b"90", *b"91", *b"92", *b"93", *b"94", *b"95", *b"96", *b"97", *b"98", *b"99",
];

#[inline(always)]
fn format_padded(x: u8) -> &'static str {
// SAFETY: The lookup table is always initialized with valid UTF-8.
unsafe { str::from_utf8_unchecked(&LOOKUP[x as usize]) }
}

#[inline(always)]
fn format_unpadded(x: u8) -> &'static str {
// SAFETY: The lookup table is always initialized with valid UTF-8.
unsafe { str::from_utf8_unchecked(&LOOKUP[x as usize][(x < 10) as usize..]) }
}
4 changes: 2 additions & 2 deletions src/timing/formatter/none_wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ impl<'a, F: 'a + TimeFormatter<'a>, S: 'a + AsRef<str>> TimeFormatter<'a> for No
impl<'a, F: TimeFormatter<'a>, S: 'a + AsRef<str>> Display for Inner<'a, F, S> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
if self.time.is_none() {
write!(f, "{}", self.wrapper.1.as_ref())
self.wrapper.1.as_ref().fmt(f)
} else {
write!(f, "{}", self.wrapper.0.format(self.time))
self.wrapper.0.format(self.time).fmt(f)
}
}
}
28 changes: 15 additions & 13 deletions src/timing/formatter/regular.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use super::{Accuracy, TimeFormatter, DASH, MINUS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE};
use super::{
format_padded, Accuracy, TimeFormatter, DASH, MINUS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE,
};
use crate::TimeSpan;
use core::fmt::{Display, Formatter, Result};

Expand Down Expand Up @@ -76,22 +78,22 @@ impl Display for Inner {
// calculate all of them in parallel. On top of that they are
// integer divisions of known constants, which get turned into
// multiplies and shifts, which is very fast.
let seconds = total_seconds % SECONDS_PER_MINUTE;
let minutes = (total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
let seconds = (total_seconds % SECONDS_PER_MINUTE) as u8;
let minutes = ((total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8;
let hours = total_seconds / SECONDS_PER_HOUR;

let mut buffer = itoa::Buffer::new();

if hours > 0 {
write!(
f,
"{hours}:{minutes:02}:{seconds:02}{}",
self.accuracy.format_nanoseconds(nanoseconds)
)
f.write_str(buffer.format(hours))?;
f.write_str(":")?;
f.write_str(format_padded(minutes))?;
} else {
write!(
f,
"{minutes}:{seconds:02}{}",
self.accuracy.format_nanoseconds(nanoseconds)
)
f.write_str(buffer.format(minutes))?;
}
f.write_str(":")?;
f.write_str(format_padded(seconds))?;
self.accuracy.format_nanoseconds(nanoseconds).fmt(f)
} else {
f.write_str(DASH)
}
Expand Down
Loading

0 comments on commit d93e4f3

Please sign in to comment.