diff --git a/Cargo.lock b/Cargo.lock index b66dad3..cf2aebb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,12 +29,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.21" @@ -348,6 +363,12 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.55" @@ -378,6 +399,33 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.4.4" @@ -565,6 +613,41 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -614,6 +697,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -2232,6 +2321,10 @@ dependencies = [ name = "gordian-types" version = "0.0.0" dependencies = [ + "criterion", + "data-encoding", + "data-encoding-macro", + "rand 0.9.2", "serde", "serde_json", "smallstr", @@ -2270,6 +2363,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hash32" version = "0.3.1" @@ -2755,6 +2859,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -3226,6 +3339,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -3286,6 +3405,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parking" version = "2.2.1" @@ -3369,6 +3498,34 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -4699,6 +4856,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.10.0" diff --git a/crates/gordian-types/Cargo.toml b/crates/gordian-types/Cargo.toml index 7cb22a1..11c6ee9 100644 --- a/crates/gordian-types/Cargo.toml +++ b/crates/gordian-types/Cargo.toml @@ -7,6 +7,9 @@ license.workspace = true edition.workspace = true publish.workspace = true +[features] +nightly = [] + [dependencies] smallstr = { version = "0.3.1" } thiserror.workspace = true @@ -16,4 +19,12 @@ sqlx = { version = "0.8.6", optional = true } time = { workspace = true, optional = true } [dev-dependencies] +criterion = "0.8.2" +data-encoding.workspace = true +data-encoding-macro = "0.1.19" +rand.workspace = true serde_json.workspace = true + +[[bench]] +name = "parse_did" +harness = false diff --git a/crates/gordian-types/benches/parse_did.rs b/crates/gordian-types/benches/parse_did.rs new file mode 100644 index 0000000..13e701d --- /dev/null +++ b/crates/gordian-types/benches/parse_did.rs @@ -0,0 +1,56 @@ +use std::iter; + +use criterion::BatchSize; +use criterion::Criterion; +use criterion::Throughput; +use criterion::criterion_group; +use criterion::criterion_main; +use data_encoding::Encoding; +use gordian_types::Did; +use gordian_types::DidBuf; +use rand::Rng; +use rand::SeedableRng; +use rand::rngs::StdRng; + +const BASE32: Encoding = data_encoding_macro::new_encoding! { + symbols: "234567abcdefghijklmnopqrstuvwxyz", + padding: None, +}; + +fn parse_did(c: &mut Criterion) { + let mut group = c.benchmark_group("DID:PLC"); + + let mut rng = StdRng::seed_from_u64(8612); + let items: Vec<_> = iter::repeat_with(|| { + let bytes: [u8; 15] = rng.random(); + format!("did:plc:{}", BASE32.encode(&bytes)) + }) + .take(1000) + .collect(); + + group.throughput(Throughput::Elements(items.len() as u64)); + group.bench_with_input("Did::parse", items.as_slice(), |b, dids: &[String]| { + b.iter(|| { + for did in dids { + assert!(Did::parse(&did).is_ok()); + } + }); + }); + + group.throughput(Throughput::Elements(items.len() as u64)); + group.bench_with_input("String->DidBuf", items.as_slice(), |b, dids: &[String]| { + b.iter_batched( + || dids.to_vec(), + |data| { + let dids: Vec = data.into_iter().map(|s| s.try_into().unwrap()).collect(); + dids + }, + BatchSize::SmallInput, + ); + }); + + group.finish(); +} + +criterion_group!(benches, parse_did); +criterion_main!(benches); diff --git a/crates/gordian-types/src/did.rs b/crates/gordian-types/src/did.rs index b9bbbfa..81398f2 100644 --- a/crates/gordian-types/src/did.rs +++ b/crates/gordian-types/src/did.rs @@ -136,6 +136,11 @@ pub enum Error { } fn validate_did(did: &str) -> Result<(), Error> { + // Optimized validation path for PLC DIDs. + if validate_did_plc(did) { + return Ok(()); + } + let mut parts = did.splitn(3, ':'); match (parts.next(), parts.next(), parts.next()) { (Some("did"), Some(""), _) => Err(Error::EmptyMethod), @@ -173,6 +178,171 @@ fn validate_ident(ident: &str) -> Result<(), Error> { // @TODO Validate percent encoding in ident. } +fn validate_did_plc(did: &str) -> bool { + if did.len() == 32 { + // x86_64 specific impl for stable rust. + #[cfg(target_arch = "x86_64")] + #[cfg(not(feature = "nightly"))] + if is_x86_feature_detected!("avx2") { + // SAFETY: AVX2 support has been detected and the DID candidate is 32 bytes + // long. + return unsafe { validate_x86_64::validate_did_plc(did) }; + } + + #[cfg(feature = "nightly")] + return validate_simd::validate_did_plc(did); + } + + let Some(ident) = did.strip_prefix("did:plc:") else { + return false; + }; + + ident + .as_bytes() + .iter() + .all(|val: &u8| (b'2'..=b'7').contains(val) || val.is_ascii_lowercase()) +} + +#[cfg(any(feature = "nightly", target_arch = "x86_64"))] +#[repr(align(32))] +#[derive(Clone, Copy)] +struct Aligned32([u8; 32]); + +#[cfg(any(feature = "nightly", target_arch = "x86_64"))] +impl Aligned32 { + const fn new(bytes: &'static [u8; 32]) -> Self { + Self(*bytes) + } +} + +#[cfg(any(feature = "nightly", target_arch = "x86_64"))] +const PLC_ALPHA_MIN: Aligned32 = Aligned32::new(b"did:plc:aaaaaaaaaaaaaaaaaaaaaaaa"); +#[cfg(any(feature = "nightly", target_arch = "x86_64"))] +const PLC_ALPHA_MAX: Aligned32 = Aligned32::new(b"did:plc:zzzzzzzzzzzzzzzzzzzzzzzz"); +#[cfg(any(feature = "nightly", target_arch = "x86_64"))] +const PLC_NUM_MIN: Aligned32 = Aligned32::new(b"did:plc:222222222222222222222222"); +#[cfg(any(feature = "nightly", target_arch = "x86_64"))] +const PLC_NUM_MAX: Aligned32 = Aligned32::new(b"did:plc:777777777777777777777777"); + +#[cfg(feature = "nightly")] +mod validate_simd { + //! Portable simd impl. Requires a nightly compiler. + + use std::ops::BitAnd; + use std::ops::BitOr; + use std::simd::prelude::*; + + use super::PLC_ALPHA_MAX; + use super::PLC_ALPHA_MIN; + use super::PLC_NUM_MAX; + use super::PLC_NUM_MIN; + + impl super::Aligned32 { + #[inline] + const fn as_u8x32(self) -> u8x32 { + u8x32::from_array(self.0) + } + } + + /// SIMD acclerated DID PLC validation. + pub fn validate_did_plc(did: &str) -> bool { + debug_assert_eq!(did.len(), 32); + + // Use the faster avx2 impl when available. + #[cfg(target_arch = "x86_64")] + if is_x86_feature_detected!("avx2") { + // SAFETY: AVX2 support has been detected. + return unsafe { validate_did_plc_avx2(did) }; + } + + let data = u8x32::from_slice(did.as_bytes()); + data.simd_ge(const { PLC_ALPHA_MIN.as_u8x32() }) + .bitand(data.simd_le(const { PLC_ALPHA_MAX.as_u8x32() })) + .bitor( + data.simd_ge(const { PLC_NUM_MIN.as_u8x32() }) + .bitand(data.simd_le(const { PLC_NUM_MAX.as_u8x32() })), + ) + .all() + } + + // AVX2 extensions are not enabled by the default x86_64 profile. Setting the + // avx2 target feature forces the compilier to generate avx2 instructions + // for this function only. + #[cfg(target_arch = "x86_64")] + #[target_feature(enable = "avx2")] + fn validate_did_plc_avx2(did: &str) -> bool { + let data = u8x32::from_slice(did.as_bytes()); + data.simd_ge(const { PLC_ALPHA_MIN.as_u8x32() }) + .bitand(data.simd_le(const { PLC_ALPHA_MAX.as_u8x32() })) + .bitor( + data.simd_ge(const { PLC_NUM_MIN.as_u8x32() }) + .bitand(data.simd_le(const { PLC_NUM_MAX.as_u8x32() })), + ) + .all() + } +} + +#[cfg(not(feature = "nightly"))] +#[cfg(target_arch = "x86_64")] +mod validate_x86_64 { + //! x86_64-specific impl. This is usable with stable rust. Remove when + //! `std::simd` is stablized. + + use std::arch::x86_64::__m256i; + + use super::PLC_ALPHA_MAX; + use super::PLC_ALPHA_MIN; + use super::PLC_NUM_MAX; + use super::PLC_NUM_MIN; + + impl super::Aligned32 { + #[inline] + pub fn as_mm256i(self) -> __m256i { + unsafe { + // SAFETY self.0 is aligned to 32 bytes. + std::arch::x86_64::_mm256_load_si256(self.0.as_ptr().cast()) + } + } + } + + #[target_feature(enable = "avx2")] + pub fn validate_did_plc(did: &str) -> bool { + use std::arch::x86_64; + + debug_assert_eq!(did.len(), 32); + + let data = unsafe { + // SAFETY: did_plc is 32 bytes (256 bits) long. + x86_64::_mm256_loadu_si256(did.as_ptr().cast()) + }; + + let mask = x86_64::_mm256_movemask_epi8(x86_64::_mm256_or_si256( + x86_64::_mm256_and_si256( + x86_64::_mm256_cmpeq_epi8( + x86_64::_mm256_min_epu8(data, PLC_ALPHA_MIN.as_mm256i()), + PLC_ALPHA_MIN.as_mm256i(), + ), + x86_64::_mm256_cmpeq_epi8( + x86_64::_mm256_max_epu8(data, PLC_ALPHA_MAX.as_mm256i()), + PLC_ALPHA_MAX.as_mm256i(), + ), + ), + x86_64::_mm256_and_si256( + x86_64::_mm256_cmpeq_epi8( + x86_64::_mm256_min_epu8(data, PLC_NUM_MIN.as_mm256i()), + PLC_NUM_MIN.as_mm256i(), + ), + x86_64::_mm256_cmpeq_epi8( + x86_64::_mm256_max_epu8(data, PLC_NUM_MAX.as_mm256i()), + PLC_NUM_MAX.as_mm256i(), + ), + ), + )); + + mask == !0 + } +} + /// Length in bytes an [`DidBuf`] can be before being allocated on the heap. // Assume most DIDs are PLC method, and this DID is a typical length. const DID_PLC_LEN: usize = "did:plc:65gha4t3avpfpzmvpbwovss7".len(); diff --git a/crates/gordian-types/src/lib.rs b/crates/gordian-types/src/lib.rs index a74f5ad..9d03eec 100644 --- a/crates/gordian-types/src/lib.rs +++ b/crates/gordian-types/src/lib.rs @@ -1,5 +1,7 @@ -//! +#![cfg_attr(feature = "nightly", feature(portable_simd))] + //! Primitive types in the atmosphere. + #[macro_use] mod macros;