Skip to content

Commit 710f53c

Browse files
committed
Speed up the comparison time lookups
We use a Vec here now because a HashMap would require hashing the comparison and then comparing the comparison with the string at the index calculated from the hash. This means at least two full iterations over the string are necessary, with one of them being somewhat expensive due to the hashing. Most of the time it is faster to just iterate the few comparisons we have and compare them directly. Most will be rejected right away as the first byte doesn't even match, so in the end you'll end up with less than two full iterations over the string. In fact most of the time Personal Best will be the first in the list and that's the one we most often want to look up anyway. One additional reason for doing this is that the ahash that was calculated for the HashMap uses 128-bit multiplications which regressed a lot in Rust 1.44 for targets where the `compiler-builtins` helpers were used. rust-lang/rust#73135 We could potentially look into interning our comparisons in the future which could yield even better performance.
1 parent 73cc3d1 commit 710f53c

File tree

6 files changed

+95
-18
lines changed

6 files changed

+95
-18
lines changed

src/rendering/glyph_cache.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use super::mesh::{fill_builder, Mesh};
22
use super::Backend;
3+
use hashbrown::HashMap;
34
use lyon::{
45
path::{self, math::point, Path},
56
tessellation::{FillOptions, FillTessellator},
67
};
78
use rusttype::{Font, GlyphId, OutlineBuilder, Scale};
8-
use std::collections::HashMap;
99

1010
struct PathBuilder(path::Builder);
1111

src/run/comparisons.rs

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use crate::platform::prelude::*;
2+
use crate::Time;
3+
4+
// We use a Vec here because a HashMap would require hashing the comparison and
5+
// then comparing the comparison with the string at the index calculated from
6+
// the hash. This means at least two full iterations over the string are
7+
// necessary, with one of them being somewhat expensive due to the hashing. Most
8+
// of the time it is faster to just iterate the few comparisons we have and
9+
// compare them directly. Most will be rejected right away as the first byte
10+
// doesn't even match, so in the end you'll end up with less than two full
11+
// iterations over the string. In fact most of the time Personal Best will be
12+
// the first in the list and that's the one we most often want to look up
13+
// anyway.
14+
//
15+
// One additional reason for doing this is that the ahash that was calculated
16+
// for the HashMap uses 128-bit multiplications which regressed a lot in Rust
17+
// 1.44 for targets where the `compiler-builtins` helpers were used.
18+
// https://github.com/rust-lang/rust/issues/73135
19+
//
20+
// We could potentially look into interning our comparisons in the future which
21+
// could yield even better performance.
22+
23+
/// A collection of a segment's comparison times.
24+
#[derive(Clone, Default, Debug, PartialEq)]
25+
pub struct Comparisons(Vec<(Box<str>, Time)>);
26+
27+
impl Comparisons {
28+
fn index_of(&self, comparison: &str) -> Option<usize> {
29+
Some(
30+
self.0
31+
.iter()
32+
.enumerate()
33+
.find(|(_, (c, _))| &**c == comparison)?
34+
.0,
35+
)
36+
}
37+
38+
/// Accesses the time for the comparison specified.
39+
pub fn get(&self, comparison: &str) -> Option<Time> {
40+
Some(self.0[self.index_of(comparison)?].1)
41+
}
42+
43+
/// Accesses the time for the comparison specified, or inserts a new empty
44+
/// one if there is none.
45+
pub fn get_or_insert_default(&mut self, comparison: &str) -> &mut Time {
46+
if let Some(index) = self.index_of(comparison) {
47+
&mut self.0[index].1
48+
} else {
49+
self.0.push((comparison.into(), Time::default()));
50+
&mut self.0.last_mut().unwrap().1
51+
}
52+
}
53+
54+
/// Sets the time for the comparison specified.
55+
pub fn set(&mut self, comparison: &str, time: Time) {
56+
*self.get_or_insert_default(comparison) = time;
57+
}
58+
59+
/// Removes the time for the comparison specified and returns it if there
60+
/// was one.
61+
pub fn remove(&mut self, comparison: &str) -> Option<Time> {
62+
let index = self.index_of(comparison)?;
63+
let (_, time) = self.0.remove(index);
64+
Some(time)
65+
}
66+
67+
/// Clears all the comparisons and their times.
68+
pub fn clear(&mut self) {
69+
self.0.clear();
70+
}
71+
72+
/// Iterates over all the comparisons and their times.
73+
pub fn iter(&self) -> impl Iterator<Item = &(Box<str>, Time)> + '_ {
74+
self.0.iter()
75+
}
76+
77+
/// Mutably iterates over all the comparisons and their times. Be careful
78+
/// when modifying the comparison name. Having duplicates will likely cause
79+
/// problems.
80+
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut (Box<str>, Time)> + '_ {
81+
self.0.iter_mut()
82+
}
83+
}

src/run/editor/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ impl Editor {
650650
}
651651
}
652652

653-
for (comparison, first_time) in first.comparisons_mut() {
653+
for (comparison, first_time) in first.comparisons_mut().iter_mut() {
654654
// Fix the comparison times based on the new positions of the two
655655
// segments
656656
let previous_time = previous

src/run/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
//! ```
1717
1818
mod attempt;
19+
mod comparisons;
1920
pub mod editor;
2021
#[cfg(feature = "std")]
2122
pub mod parser;
@@ -29,6 +30,7 @@ mod segment_history;
2930
mod tests;
3031

3132
pub use attempt::Attempt;
33+
pub use comparisons::Comparisons;
3234
pub use editor::{Editor, RenameError};
3335
pub use run_metadata::{CustomVariable, RunMetadata};
3436
pub use segment::Segment;

src/run/segment.rs

+7-15
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
use super::Comparisons;
12
use crate::comparison::personal_best;
23
use crate::platform::prelude::*;
34
use crate::{settings::Image, SegmentHistory, Time, TimeSpan, TimingMethod};
4-
use hashbrown::HashMap;
55

66
/// A Segment describes a point in a speedrun that is suitable for storing a
77
/// split time. This stores the name of that segment, an icon, the split times
@@ -24,7 +24,7 @@ pub struct Segment {
2424
best_segment_time: Time,
2525
split_time: Time,
2626
segment_history: SegmentHistory,
27-
comparisons: HashMap<String, Time>,
27+
comparisons: Comparisons,
2828
}
2929

3030
impl Segment {
@@ -70,28 +70,23 @@ impl Segment {
7070
/// Grants mutable access to the comparison times stored in the Segment.
7171
/// This includes both the custom comparisons and the generated ones.
7272
#[inline]
73-
pub fn comparisons_mut(&mut self) -> &mut HashMap<String, Time> {
73+
pub fn comparisons_mut(&mut self) -> &mut Comparisons {
7474
&mut self.comparisons
7575
}
7676

7777
/// Grants mutable access to the specified comparison's time. If there's
7878
/// none for this comparison, a new one is inserted with an empty time.
7979
#[inline]
8080
pub fn comparison_mut(&mut self, comparison: &str) -> &mut Time {
81-
self.comparisons
82-
.entry(comparison.into())
83-
.or_insert_with(Time::default)
81+
self.comparisons.get_or_insert_default(comparison)
8482
}
8583

8684
/// Accesses the specified comparison's time. If there's none for this
8785
/// comparison, an empty time is being returned (but not stored in the
8886
/// segment).
8987
#[inline]
9088
pub fn comparison(&self, comparison: &str) -> Time {
91-
self.comparisons
92-
.get(comparison)
93-
.cloned()
94-
.unwrap_or_default()
89+
self.comparisons.get(comparison).unwrap_or_default()
9590
}
9691

9792
/// Accesses the given timing method of the specified comparison. If either
@@ -112,23 +107,20 @@ impl Segment {
112107
pub fn personal_best_split_time(&self) -> Time {
113108
self.comparisons
114109
.get(personal_best::NAME)
115-
.cloned()
116110
.unwrap_or_else(Time::default)
117111
}
118112

119113
/// Grants mutable access to the split time of the Personal Best for this
120114
/// segment. If it doesn't exist an empty time is inserted.
121115
#[inline]
122116
pub fn personal_best_split_time_mut(&mut self) -> &mut Time {
123-
self.comparisons
124-
.entry(personal_best::NAME.to_string())
125-
.or_insert_with(Time::default)
117+
self.comparisons.get_or_insert_default(personal_best::NAME)
126118
}
127119

128120
/// Sets the split time of the Personal Best to the time provided.
129121
#[inline]
130122
pub fn set_personal_best_split_time(&mut self, time: Time) {
131-
self.comparisons.insert(personal_best::NAME.into(), time);
123+
self.comparisons.set(personal_best::NAME, time);
132124
}
133125

134126
/// Accesses the Best Segment Time.

src/settings/gradient.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
33

44
/// Describes a Gradient for coloring a region with more than just a single
55
/// color.
6-
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
6+
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
77
pub enum Gradient {
88
/// Don't use any color, keep it transparent.
99
Transparent,

0 commit comments

Comments
 (0)