Skip to content

Commit 2e86fd0

Browse files
authored
Impl Extend for LiteMap and avoid quadratic behavior in from_iter and deserialize (#6132)
Adds `lm_extend` to the `StoreMut` trait. The Vec implementation is optimized to have O(N) performance on sorted inputs and an asymptotic worst case of O(NLog(N)), effectively avoiding quadratic worst cases. This PR takes advantage of the new `extend` optimization and changes the `FromIterator` and `Deserialize` implementations to also avoid quadratic worst-case. Before ``` litemap/from_iter_rand/small time: [916.60 ns 941.77 ns 965.82 ns] litemap/from_iter_rand/large time: [2.3779 s 2.3863 s 2.3946 s] litemap/from_iter_sorted/small time: [84.010 ns 84.084 ns 84.155 ns] litemap/from_iter_sorted/large time: [725.31 µs 739.88 µs 755.20 µs] ``` After ``` litemap/from_iter_rand/small time: [354.62 ns 365.31 ns 376.02 ns] litemap/from_iter_rand/large time: [25.129 ms 25.415 ms 25.704 ms] litemap/from_iter_sorted/small time: [76.593 ns 76.767 ns 76.938 ns] litemap/from_iter_sorted/large time: [662.70 µs 673.66 µs 686.83 µs] litemap/extend_rand/large time: [66.221 ms 67.467 ms 68.814 ms] litemap/extend_rand_dups/large time: [190.24 ms 196.05 ms 202.13 ms] litemap/extend_from_litemap_rand/large time: [2.0401 s 2.0488 s 2.0575 s] ``` This is a followup of #6068
1 parent 4146498 commit 2e86fd0

File tree

12 files changed

+555
-124
lines changed

12 files changed

+555
-124
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/locale_core/src/shortvec/litemap.rs

+56
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ impl<K, V> StoreConstEmpty<K, V> for ShortBoxSlice<(K, V)> {
1212
const EMPTY: ShortBoxSlice<(K, V)> = ShortBoxSlice::new();
1313
}
1414

15+
impl<K, V> StoreSlice<K, V> for ShortBoxSlice<(K, V)> {
16+
type Slice = [(K, V)];
17+
18+
#[inline]
19+
fn lm_get_range(&self, range: core::ops::Range<usize>) -> Option<&Self::Slice> {
20+
self.get(range)
21+
}
22+
}
23+
1524
impl<K, V> Store<K, V> for ShortBoxSlice<(K, V)> {
1625
#[inline]
1726
fn lm_len(&self) -> usize {
@@ -83,13 +92,60 @@ impl<K, V> StoreMut<K, V> for ShortBoxSlice<(K, V)> {
8392
fn lm_clear(&mut self) {
8493
self.clear();
8594
}
95+
}
8696

97+
#[cfg(feature = "alloc")]
98+
impl<K: Ord, V> StoreBulkMut<K, V> for ShortBoxSlice<(K, V)> {
8799
fn lm_retain<F>(&mut self, mut predicate: F)
88100
where
89101
F: FnMut(&K, &V) -> bool,
90102
{
91103
self.retain(|(k, v)| predicate(k, v))
92104
}
105+
106+
fn lm_extend<I>(&mut self, other: I)
107+
where
108+
I: IntoIterator<Item = (K, V)>,
109+
{
110+
let mut other = other.into_iter();
111+
// Use an Option to hold the first item of the map and move it to
112+
// items if there are more items. Meaning that if items is not
113+
// empty, first is None.
114+
let mut first = None;
115+
let mut items = alloc::vec::Vec::new();
116+
match core::mem::take(&mut self.0) {
117+
ShortBoxSliceInner::ZeroOne(zo) => {
118+
first = zo;
119+
// Attempt to avoid the items allocation by advancing the iterator
120+
// up to two times. If we eventually find a second item, we can
121+
// lm_extend the Vec and with the first, next (second) and the rest
122+
// of the iterator.
123+
while let Some(next) = other.next() {
124+
if let Some(first) = first.take() {
125+
// lm_extend will take care of sorting and deduplicating
126+
// first, next and the rest of the other iterator.
127+
items.lm_extend([first, next].into_iter().chain(other));
128+
break;
129+
}
130+
first = Some(next);
131+
}
132+
}
133+
ShortBoxSliceInner::Multi(existing_items) => {
134+
items.reserve_exact(existing_items.len() + other.size_hint().0);
135+
// We use a plain extend with existing items, which are already valid and
136+
// lm_extend will fold over rest of the iterator sorting and deduplicating as needed.
137+
items.extend(existing_items);
138+
items.lm_extend(other);
139+
}
140+
}
141+
if items.is_empty() {
142+
debug_assert!(items.is_empty());
143+
self.0 = ShortBoxSliceInner::ZeroOne(first);
144+
} else {
145+
debug_assert!(first.is_none());
146+
self.0 = ShortBoxSliceInner::Multi(items.into_boxed_slice());
147+
}
148+
}
93149
}
94150

95151
impl<'a, K: 'a, V: 'a> StoreIterable<'a, K, V> for ShortBoxSlice<(K, V)> {

utils/litemap/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ serde = { workspace = true, optional = true, features = ["alloc"]}
2828
yoke = { workspace = true, features = ["derive"], optional = true }
2929

3030
[dev-dependencies]
31+
rand = { workspace = true }
3132
bincode = { workspace = true }
3233
icu_benchmark_macros = { path = "../../tools/benchmark/macros" }
3334
icu_locale_core = { path = "../../components/locale_core" }
@@ -43,7 +44,7 @@ criterion = { workspace = true }
4344
default = ["alloc"]
4445
alloc = []
4546
databake = ["dep:databake"]
46-
serde = ["dep:serde"]
47+
serde = ["dep:serde", "alloc"]
4748
yoke = ["dep:yoke"]
4849

4950
# Enables the `testing` module with tools for testing custom stores.

utils/litemap/README.md

+15-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

utils/litemap/benches/litemap.rs

+101
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use criterion::{black_box, criterion_group, criterion_main, Criterion};
66

77
use litemap::LiteMap;
8+
use rand::seq::SliceRandom;
89

910
const DATA: [(&str, &str); 16] = [
1011
("ar", "Arabic"),
@@ -57,6 +58,13 @@ fn overview_bench(c: &mut Criterion) {
5758
bench_deserialize_large(c);
5859
bench_lookup(c);
5960
bench_lookup_large(c);
61+
bench_from_iter(c);
62+
bench_from_iter_rand_large(c);
63+
bench_from_iter_sorted(c);
64+
bench_from_iter_large_sorted(c);
65+
bench_extend_rand(c);
66+
bench_extend_rand_dups(c);
67+
bench_extend_from_litemap_rand(c);
6068
}
6169

6270
fn build_litemap(large: bool) -> LiteMap<String, String> {
@@ -92,6 +100,99 @@ fn bench_deserialize_large(c: &mut Criterion) {
92100
});
93101
}
94102

103+
fn bench_from_iter(c: &mut Criterion) {
104+
c.bench_function("litemap/from_iter_rand/small", |b| {
105+
let mut ff = build_litemap(false).into_iter().collect::<Vec<_>>();
106+
ff[..].shuffle(&mut rand::rng());
107+
b.iter(|| {
108+
let map: LiteMap<&String, &String> = LiteMap::from_iter(ff.iter().map(|(k, v)| (k, v)));
109+
black_box(map)
110+
})
111+
});
112+
}
113+
114+
fn bench_from_iter_rand_large(c: &mut Criterion) {
115+
c.bench_function("litemap/from_iter_rand/large", |b| {
116+
let mut ff = build_litemap(true).into_iter().collect::<Vec<_>>();
117+
ff[..].shuffle(&mut rand::rng());
118+
b.iter(|| {
119+
let map: LiteMap<&String, &String> = LiteMap::from_iter(ff.iter().map(|(k, v)| (k, v)));
120+
black_box(map)
121+
})
122+
});
123+
}
124+
125+
fn bench_from_iter_sorted(c: &mut Criterion) {
126+
c.bench_function("litemap/from_iter_sorted/small", |b| {
127+
let ff = build_litemap(false).into_iter().collect::<Vec<_>>();
128+
b.iter(|| {
129+
let map: LiteMap<&String, &String> = LiteMap::from_iter(ff.iter().map(|(k, v)| (k, v)));
130+
black_box(map)
131+
})
132+
});
133+
}
134+
135+
fn bench_from_iter_large_sorted(c: &mut Criterion) {
136+
c.bench_function("litemap/from_iter_sorted/large", |b| {
137+
let ff = build_litemap(true).into_iter().collect::<Vec<_>>();
138+
b.iter(|| {
139+
let map: LiteMap<&String, &String> = LiteMap::from_iter(ff.iter().map(|(k, v)| (k, v)));
140+
black_box(map)
141+
})
142+
});
143+
}
144+
145+
fn bench_extend_rand(c: &mut Criterion) {
146+
c.bench_function("litemap/extend_rand/large", |b| {
147+
let mut ff = build_litemap(true).into_iter().collect::<Vec<_>>();
148+
ff[..].shuffle(&mut rand::rng());
149+
b.iter(|| {
150+
let mut map: LiteMap<&String, &String> = LiteMap::with_capacity(0);
151+
let mut iter = ff.iter().map(|(k, v)| (k, v));
152+
let step = ff.len().div_ceil(10);
153+
for _ in 0..10 {
154+
map.extend(iter.by_ref().take(step));
155+
}
156+
black_box(map)
157+
})
158+
});
159+
}
160+
161+
fn bench_extend_rand_dups(c: &mut Criterion) {
162+
c.bench_function("litemap/extend_rand_dups/large", |b| {
163+
let mut ff = build_litemap(true).into_iter().collect::<Vec<_>>();
164+
ff[..].shuffle(&mut rand::rng());
165+
b.iter(|| {
166+
let mut map: LiteMap<&String, &String> = LiteMap::with_capacity(0);
167+
for _ in 0..2 {
168+
let mut iter = ff.iter().map(|(k, v)| (k, v));
169+
let step = ff.len().div_ceil(10);
170+
for _ in 0..10 {
171+
map.extend(iter.by_ref().take(step));
172+
}
173+
}
174+
black_box(map)
175+
})
176+
});
177+
}
178+
179+
fn bench_extend_from_litemap_rand(c: &mut Criterion) {
180+
c.bench_function("litemap/extend_from_litemap_rand/large", |b| {
181+
let mut ff = build_litemap(true).into_iter().collect::<Vec<_>>();
182+
ff[..].shuffle(&mut rand::rng());
183+
b.iter(|| {
184+
let mut map: LiteMap<&String, &String> = LiteMap::with_capacity(0);
185+
let mut iter = ff.iter().map(|(k, v)| (k, v));
186+
let step = ff.len().div_ceil(10);
187+
for _ in 0..10 {
188+
let tmp: LiteMap<&String, &String> = LiteMap::from_iter(iter.by_ref().take(step));
189+
map.extend_from_litemap(tmp);
190+
}
191+
black_box(map)
192+
})
193+
});
194+
}
195+
95196
fn bench_lookup(c: &mut Criterion) {
96197
let map: LiteMap<String, String> = postcard::from_bytes(&POSTCARD).unwrap();
97198
c.bench_function("litemap/lookup/small", |b| {

utils/litemap/src/lib.rs

+15-1
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,28 @@
77
//! `litemap` is a crate providing [`LiteMap`], a highly simplistic "flat" key-value map
88
//! based off of a single sorted vector.
99
//!
10-
//! The goal of this crate is to provide a map that is good enough for small
10+
//! The main goal of this crate is to provide a map that is good enough for small
1111
//! sizes, and does not carry the binary size impact of [`HashMap`](std::collections::HashMap)
1212
//! or [`BTreeMap`](alloc::collections::BTreeMap).
1313
//!
1414
//! If binary size is not a concern, [`std::collections::BTreeMap`] may be a better choice
1515
//! for your use case. It behaves very similarly to [`LiteMap`] for less than 12 elements,
1616
//! and upgrades itself gracefully for larger inputs.
1717
//!
18+
//! ## Performance characteristics
19+
//!
20+
//! [`LiteMap`] is a data structure with similar characteristics as [`std::collections::BTreeMap`] but
21+
//! with slightly different trade-offs as it's implemented on top of a flat storage (e.g. Vec).
22+
//!
23+
//! * [`LiteMap`] iteration is generally faster than `BTreeMap` because of the flat storage.
24+
//! * [`LiteMap`] can be pre-allocated whereas `BTreeMap` can't.
25+
//! * [`LiteMap`] has a smaller memory footprint than `BTreeMap` for small collections (< 20 items).
26+
//! * Lookup is `O(log(n))` like `BTreeMap`.
27+
//! * Insertion is generally `O(n)`, but optimized to `O(1)` if the new item sorts greater than the current items. In `BTreeMap` it's `O(log(n))`.
28+
//! * Deletion is `O(n)` whereas `BTreeMap` is `O(log(n))`.
29+
//! * Bulk operations like `from_iter`, `extend` and deserialization have an optimized `O(n)` path
30+
//! for inputs that are ordered and `O(n*log(n))` complexity otherwise.
31+
//!
1832
//! ## Pluggable Backends
1933
//!
2034
//! By default, [`LiteMap`] is backed by a [`Vec`]; however, it can be backed by any appropriate

0 commit comments

Comments
 (0)