Skip to content

Commit 4680154

Browse files
committed
perf(span): hash Span as a single u64
1 parent fc19033 commit 4680154

1 file changed

Lines changed: 51 additions & 15 deletions

File tree

  • crates/oxc_span/src/span

crates/oxc_span/src/span/mod.rs

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,25 @@ impl Span {
341341
pub fn primary_label<S: Into<String>>(self, label: S) -> LabeledSpan {
342342
LabeledSpan::new_primary_with_span(Some(label.into()), self)
343343
}
344+
345+
/// Convert [`Span`] to a single `u64`.
346+
///
347+
/// On 64-bit platforms, `Span` is aligned on 8, so equivalent to a `u64`.
348+
/// Compiler boils this conversion down to a no-op on 64-bit platforms.
349+
/// <https://godbolt.org/z/9qv5qqd3e>
350+
///
351+
/// Do not use this on 32-bit platforms as it's likely to be less efficient.
352+
///
353+
/// Note: `#[ast]` macro adds `#[repr(C)]` to the struct, so field order is guaranteed.
354+
#[expect(clippy::inline_always)] // Because this is a no-op on 64-bit platforms.
355+
#[inline(always)]
356+
fn as_u64(self) -> u64 {
357+
if cfg!(target_endian = "little") {
358+
(u64::from(self.end) << 32) | u64::from(self.start)
359+
} else {
360+
(u64::from(self.start) << 32) | u64::from(self.end)
361+
}
362+
}
344363
}
345364

346365
impl Index<Span> for str {
@@ -378,12 +397,18 @@ impl From<Span> for LabeledSpan {
378397
}
379398
}
380399

381-
// Skip hashing `_align` field
400+
// Skip hashing `_align` field.
401+
// On 64-bit platforms, hash `Span` as a single `u64`, which is faster with `FxHash`.
402+
// https://godbolt.org/z/4q36xrWG8
382403
impl Hash for Span {
383-
#[inline] // We exclusively use `FxHasher`, which produces small output hashing `u32`s
404+
#[inline] // We exclusively use `FxHasher`, which produces small output hashing `u64`s and `u32`s
384405
fn hash<H: Hasher>(&self, hasher: &mut H) {
385-
self.start.hash(hasher);
386-
self.end.hash(hasher);
406+
if cfg!(target_pointer_width = "64") {
407+
self.as_u64().hash(hasher);
408+
} else {
409+
self.start.hash(hasher);
410+
self.end.hash(hasher);
411+
}
387412
}
388413
}
389414

@@ -446,7 +471,6 @@ mod test {
446471
}
447472

448473
#[test]
449-
#[expect(clippy::items_after_statements)]
450474
fn test_hash() {
451475
use std::hash::{DefaultHasher, Hash, Hasher};
452476
fn hash<T: Hash>(value: T) -> u64 {
@@ -455,20 +479,32 @@ mod test {
455479
hasher.finish()
456480
}
457481

458-
let first_hash = hash(Span::new(0, 5));
459-
let second_hash = hash(Span::new(0, 5));
482+
let first_hash = hash(Span::new(1, 5));
483+
let second_hash = hash(Span::new(1, 5));
460484
assert_eq!(first_hash, second_hash);
461485

462-
// Check `_align` field does not alter hash
463-
#[derive(Hash)]
464-
#[repr(C)]
465-
struct PlainSpan {
466-
start: u32,
467-
end: u32,
486+
// On 64-bit platforms, check hash is equivalent to `u64`
487+
#[cfg(target_pointer_width = "64")]
488+
{
489+
let u64_equivalent: u64 =
490+
if cfg!(target_endian = "little") { 1 + (5 << 32) } else { (1 << 32) + 5 };
491+
let u64_hash = hash(u64_equivalent);
492+
assert_eq!(first_hash, u64_hash);
468493
}
469494

470-
let plain_hash = hash(PlainSpan { start: 0, end: 5 });
471-
assert_eq!(plain_hash, first_hash);
495+
// On 32-bit platforms, check `_align` field does not alter hash
496+
#[cfg(not(target_pointer_width = "64"))]
497+
{
498+
#[derive(Hash)]
499+
#[repr(C)]
500+
struct PlainSpan {
501+
start: u32,
502+
end: u32,
503+
}
504+
505+
let plain_hash = hash(PlainSpan { start: 1, end: 5 });
506+
assert_eq!(first_hash, plain_hash);
507+
}
472508
}
473509

474510
#[test]

0 commit comments

Comments
 (0)