Skip to content

Commit aa2ad18

Browse files
authored
perf: const KeyHasher with rapidhash (#669)
1 parent 9cf5066 commit aa2ad18

File tree

9 files changed

+236
-86
lines changed

9 files changed

+236
-86
lines changed

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ quickcheck_macros = { version = "1", default-features = false }
6666
radix_trie = { version = "0.2", default-features = false }
6767
rand = { version = "0.9", default-features = false, features = [ "thread_rng" ] }
6868
rand_xoshiro = { version = "0.7", default-features = false }
69-
rapidhash = { version = "4.1.1", default-features = false }
69+
rapidhash = { version = "4.4.1", default-features = false }
7070
ratatui = { version = "0.29", default-features = false }
7171
rustls = { version = "0.23", default-features = false }
7272
sketches-ddsketch = { version = "0.3", default-features = false }
@@ -76,3 +76,7 @@ tracing = { version = "0.1", default-features = false }
7676
tracing-core = { version = "0.1", default-features = false }
7777
tracing-subscriber = { version = "0.3", default-features = false }
7878
trybuild = { version = "1", default-features = false }
79+
80+
[profile.bench]
81+
codegen-units = 1
82+
lto = true

metrics-util/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ crossbeam-utils = { workspace = true, optional = true }
5252
hashbrown = { workspace = true, optional = true }
5353
indexmap = { workspace = true, optional = true }
5454
metrics = { version = "^0.24", path = "../metrics" }
55+
rapidhash = { workspace = true }
5556
ordered-float = { workspace = true, optional = true }
5657
quanta = { workspace = true, optional = true }
5758
rand = { workspace = true, optional = true }

metrics-util/benches/registry.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,14 @@ fn registry_benchmark(c: &mut Criterion) {
4646
group.bench_function("const key overhead (basic)", |b| {
4747
b.iter(|| {
4848
static KEY_NAME: &str = "simple_key";
49-
Key::from_static_name(KEY_NAME)
49+
const { Key::from_static_name(KEY_NAME) } // this gets evaluated at runtime unless wrapped in const
5050
})
5151
});
5252
group.bench_function("const key data overhead (labels)", |b| {
5353
b.iter(|| {
5454
static KEY_NAME: &str = "simple_key";
5555
static LABELS: [Label; 1] = [Label::from_static_parts("type", "http")];
56-
Key::from_static_parts(KEY_NAME, &LABELS)
56+
const { Key::from_static_parts(KEY_NAME, &LABELS) }
5757
})
5858
});
5959
group.bench_function("owned key overhead (basic)", |b| b.iter(|| Key::from_name("simple_key")));

metrics-util/src/common.rs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use std::fmt::Debug;
22
use std::hash::{Hash, Hasher};
33

4-
use metrics::{Key, KeyHasher};
4+
use metrics::Key;
5+
use rapidhash::fast::RapidHasher;
56

67
/// A type that can hash itself.
78
///
@@ -22,6 +23,7 @@ pub trait Hashable: Hash {
2223
type Hasher: Hasher + Default;
2324

2425
/// Generate the hash of this object.
26+
#[inline]
2527
fn hashable(&self) -> u64 {
2628
let mut hasher = Self::Hasher::default();
2729
self.hash(&mut hasher);
@@ -32,11 +34,46 @@ pub trait Hashable: Hash {
3234
impl Hashable for Key {
3335
type Hasher = KeyHasher;
3436

37+
#[inline]
3538
fn hashable(&self) -> u64 {
3639
self.get_hash()
3740
}
3841
}
3942

43+
/// A no-op hasher for pre-hashed [`Key`][metrics::Key] types.
44+
///
45+
/// This hasher is designed for use with [`Key`][metrics::Key], which pre-computes its hash at
46+
/// construction time. When `Key::hash()` is called, it writes the pre-computed hash via
47+
/// `write_u64()`, and `finish()` simply returns that value.
48+
///
49+
/// This ensures that `HashMap<Key, V, BuildHasherDefault<KeyHasher>>` lookups work correctly
50+
/// when using raw_entry APIs with pre-computed hashes.
51+
///
52+
/// # Panics
53+
///
54+
/// Panics if `finish()` is called without first calling `write_u64()`, or if any write method
55+
/// other than `write_u64()` is called. This hasher is specifically for pre-hashed keys only.
56+
#[derive(Debug, Default)]
57+
pub struct KeyHasher {
58+
hash: Option<u64>,
59+
}
60+
61+
impl Hasher for KeyHasher {
62+
#[inline(always)]
63+
fn finish(&self) -> u64 {
64+
self.hash.expect("KeyHasher::finish() called without write_u64(); KeyHasher is only for pre-hashed Key types")
65+
}
66+
67+
fn write(&mut self, _bytes: &[u8]) {
68+
panic!("KeyHasher::write() called; KeyHasher only supports write_u64() for pre-hashed Key types");
69+
}
70+
71+
#[inline(always)]
72+
fn write_u64(&mut self, i: u64) {
73+
self.hash = Some(i);
74+
}
75+
}
76+
4077
/// A wrapper type that provides `Hashable` for any type that is `Hash`.
4178
///
4279
/// As part of using [`Registry`][crate::registry::Registry], the chosen key type must implement
@@ -47,10 +84,10 @@ impl Hashable for Key {
4784
pub struct DefaultHashable<H: Hash>(pub H);
4885

4986
impl<H: Hash> Hashable for DefaultHashable<H> {
50-
type Hasher = KeyHasher;
87+
type Hasher = RapidHasher<'static>;
5188

5289
fn hashable(&self) -> u64 {
53-
let mut hasher = KeyHasher::default();
90+
let mut hasher = RapidHasher::default();
5491
self.hash(&mut hasher);
5592
hasher.finish()
5693
}

metrics-util/src/registry/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::{
88
};
99

1010
use hashbrown::{hash_map::RawEntryMut, HashMap};
11-
use metrics::{Key, KeyHasher};
11+
use metrics::Key;
1212
pub use storage::{AtomicStorage, Storage};
1313

1414
#[cfg(feature = "recency")]
@@ -20,6 +20,7 @@ pub use recency::{
2020
Generation, Generational, GenerationalAtomicStorage, GenerationalStorage, Recency,
2121
};
2222

23+
use crate::common::KeyHasher;
2324
use crate::Hashable;
2425

2526
type RegistryHasher = KeyHasher;

metrics/src/common.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,23 @@ pub type SharedString = Cow<'static, str>;
1919

2020
/// Key-specific hashing algorithm.
2121
///
22+
/// Deprecated in favor of a no-hash based implementation in `metrics-util::common::KeyHasher`.
23+
///
2224
/// Currently uses rapidhash - <https://github.com/hoxxep/rapidhash>
2325
///
24-
/// For any use-case within a `metrics`-owned or adjacent crate, where hashing of a key is required,
25-
/// this is the hasher that will be used.
26+
/// For any use-case within a `metrics`-owned or adjacent crate, where hashing of a
27+
/// [`Key`][crate::Key] is required, this is the hasher that will be used.
28+
#[deprecated(since = "0.24.4", note = "Use `metrics-util::common::KeyHasher` instead.")]
2629
pub struct KeyHasher(RapidHasher<'static>);
2730

31+
#[allow(deprecated)]
2832
impl std::fmt::Debug for KeyHasher {
2933
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3034
f.debug_struct("KeyHasher").finish_non_exhaustive()
3135
}
3236
}
3337

38+
#[allow(deprecated)]
3439
impl Default for KeyHasher {
3540
fn default() -> Self {
3641
// The seed should be randomized on application start if DoS resistance is required, but
@@ -39,6 +44,7 @@ impl Default for KeyHasher {
3944
}
4045
}
4146

47+
#[allow(deprecated)]
4248
impl Hasher for KeyHasher {
4349
fn finish(&self) -> u64 {
4450
self.0.finish()

metrics/src/cow.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,3 +702,109 @@ fn clone_shared<T: Cowable + ?Sized>(
702702

703703
(ptr, *metadata)
704704
}
705+
706+
/// These implementations should be identical to Deref, but enable dereferencing in a `const`
707+
/// environment for `Key` hashing.
708+
pub(crate) mod const_cow {
709+
use super::*;
710+
use crate::Label;
711+
use std::ptr::slice_from_raw_parts;
712+
713+
impl Cow<'static, str> {
714+
pub(crate) const fn as_const_str(&self) -> &str {
715+
let borrowed_ptr =
716+
slice_from_raw_parts(self.ptr.as_ptr(), self.metadata.len()) as *const str;
717+
718+
// SAFETY: We only ever hold a pointer to a borrowed value of at least the lifetime of
719+
// `Self`, or an owned value which we have ownership of (albeit indirectly when using
720+
// `Arc<T>`), so our pointer is always valid and live for dereferencing.
721+
//
722+
// self.ptr is also `NonNull<T>`, and so borrowed_ptr is guaranteed to not be null.
723+
unsafe { &*borrowed_ptr }
724+
}
725+
}
726+
727+
impl Cow<'static, [Label]> {
728+
pub(crate) const fn as_const_slice(&self) -> &[Label] {
729+
let borrowed_ptr =
730+
slice_from_raw_parts(self.ptr.as_ptr(), self.metadata.len()) as *const [Label];
731+
732+
// SAFETY: We only ever hold a pointer to a borrowed value of at least the lifetime of
733+
// `Self`, or an owned value which we have ownership of (albeit indirectly when using
734+
// `Arc<T>`), so our pointer is always valid and live for dereferencing.
735+
//
736+
// self.ptr is also `NonNull<T>`, and so borrowed_ptr is guaranteed to not be null.
737+
unsafe { &*borrowed_ptr }
738+
}
739+
}
740+
741+
#[cfg(test)]
742+
mod tests {
743+
use super::*;
744+
use std::sync::Arc;
745+
746+
#[test]
747+
fn test_as_const_str() {
748+
// Borrowed
749+
let cow = Cow::const_str("hello");
750+
assert_eq!(cow.as_const_str(), "hello");
751+
752+
// Owned
753+
let cow: Cow<'static, str> = Cow::from_owned(String::from("hello"));
754+
assert_eq!(cow.as_const_str(), "hello");
755+
756+
// Shared
757+
let arc: Arc<str> = Arc::from("hello");
758+
let cow: Cow<'static, str> = Cow::from_shared(arc);
759+
assert_eq!(cow.as_const_str(), "hello");
760+
}
761+
762+
#[test]
763+
fn test_as_const_slice() {
764+
// Borrowed
765+
{
766+
static LABELS: [Label; 2] = [
767+
Label::from_static_parts("key1", "value1"),
768+
Label::from_static_parts("key2", "value2"),
769+
];
770+
let cow = Cow::const_slice(&LABELS);
771+
let slice = cow.as_const_slice();
772+
assert_eq!(slice.len(), 2);
773+
assert_eq!(slice[0].key(), "key1");
774+
assert_eq!(slice[0].value(), "value1");
775+
assert_eq!(slice[1].key(), "key2");
776+
assert_eq!(slice[1].value(), "value2");
777+
}
778+
779+
// Owned
780+
{
781+
let labels = vec![
782+
Label::from_static_parts("key1", "value1"),
783+
Label::from_static_parts("key2", "value2"),
784+
];
785+
let cow: Cow<'static, [Label]> = Cow::from_owned(labels);
786+
let slice = cow.as_const_slice();
787+
assert_eq!(slice.len(), 2);
788+
assert_eq!(slice[0].key(), "key1");
789+
assert_eq!(slice[0].value(), "value1");
790+
assert_eq!(slice[1].key(), "key2");
791+
assert_eq!(slice[1].value(), "value2");
792+
}
793+
794+
// Shared
795+
{
796+
let labels: Arc<[Label]> = Arc::from([
797+
Label::from_static_parts("key1", "value1"),
798+
Label::from_static_parts("key2", "value2"),
799+
]);
800+
let cow: Cow<'static, [Label]> = Cow::from_shared(labels);
801+
let slice = cow.as_const_slice();
802+
assert_eq!(slice.len(), 2);
803+
assert_eq!(slice[0].key(), "key1");
804+
assert_eq!(slice[0].value(), "value1");
805+
assert_eq!(slice[1].key(), "key2");
806+
assert_eq!(slice[1].value(), "value2");
807+
}
808+
}
809+
}
810+
}

0 commit comments

Comments
 (0)