Skip to content

Use a hash set in the unique filter - #1052

Merged
Keats merged 2 commits into
Keats:masterfrom
patrick-immometrica:perf/unique-hashset
Oct 26, 2026
Merged

Keats merged 2 commits into
Keats:masterfrom
patrick-immometrica:perf/unique-hashset

Conversation

@patrick-immometrica

Copy link
Copy Markdown
Contributor

unique checked each value with res.contains(v), so it was quadratic in the array length. A list of a few thousand ids took tens of milliseconds just for that filter.

This puts strings, bools, ints and integral floats in a hash set instead. Integral floats are keyed as the integer they equal, so 1 and 1.0 still count as duplicates the way Value::eq says. Maps, arrays and non-integral floats can't go in the set (maps all compare equal in our Ord, which is why a BTreeSet wouldn't work either), but they can only ever equal each other, so they keep the linear scan among themselves only. Order of first occurrence is unchanged.

I added a line to the sort_and_unique snapshot that mixes ints, floats, -0.0, safe and normal strings, bools and none. The snapshot was generated with the old implementation and passes unchanged with the new one.

Before/after on {{ arr | unique | length }} with half the values duplicated (release build, µs per render, range over two runs):

input before after
100 ints 13–16 6.5–10
100 strings 15 4–11
1,000 ints 1,489–1,735 51–59
1,000 strings 1,224–1,269 21–71
5,000 ints 34,989–35,379 231–456
5,000 strings 28,878–29,546 102–496

unique checked every value against everything kept so far, so it was
quadratic in the array length. Strings, bools, ints and integral floats
now go through a hash set keyed so that 1 and 1.0 still collide. Maps,
arrays and non-integral floats keep the linear scan, but only among
themselves since they can't equal anything hashable.
@patrick-immometrica
patrick-immometrica marked this pull request as ready for review October 24, 2026 21:13
Comment thread tera/src/filters.rs Outdated

// Using a set cause issues with maps because we do impl some Ord which messes up things
let mut res = Vec::with_capacity(val.len());
let mut seen = HashSet::with_capacity(val.len());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it's ok to leave that with HashSet::new()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, changed.

Comment thread tera/src/filters.rs Outdated
Some(Key::U128(f as u128))
} else {
None
};

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Number::as_integer instead of having the same logic in multiple places

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. All numbers go through as_number()?.as_integer() now. Values outside i128 (big u128s, and floats like 2^127) fall back to the linear scan, so Value::eq still decides for them. I added test_unique_numbers_outside_i128 for that case.

@Keats
Keats merged commit 8a3e053 into Keats:master Oct 26, 2026
5 checks passed
@patrick-immometrica
patrick-immometrica deleted the perf/unique-hashset branch October 27, 2026 09:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants