Skip to content

Commit d37bc85

Browse files
authored
Fix: speed up discriminator validation (#260)
speed up discriminator validation update benchmark
1 parent 7e96f27 commit d37bc85

File tree

3 files changed

+114
-109
lines changed

3 files changed

+114
-109
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Improved performance of ProgramAccount discriminant validation (#260)
13+
1014
## [0.25.0] - 2025-09-11
1115

1216
### Fixed

example_programs/bench/COMPUTE_UNITS.md

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,54 +12,54 @@ Solana version: 2.1.0
1212
| accountInfo2 | 185 | 🟢 **-710 (79.33%)** |
1313
| accountInfo4 | 215 | 🟢 **-1,338 (86.16%)** |
1414
| accountInfo8 | 278 | 🟢 **-2,645 (90.49%)** |
15-
| accountEmptyInit1 | 1,989 | 🟢 **-3,094 (60.87%)** |
16-
| accountEmpty1 | 193 | 🟢 **-452 (70.08%)** |
17-
| accountEmptyInit2 | 3,592 | 🟢 **-5,709 (61.38%)** |
18-
| accountEmpty2 | 245 | 🟢 **-762 (75.67%)** |
19-
| accountEmptyInit4 | 6,805 | 🟢 **-10,959 (61.69%)** |
20-
| accountEmpty4 | 336 | 🟢 **-1,388 (80.51%)** |
21-
| accountEmptyInit8 | 13,218 | 🟢 **-21,505 (61.93%)** |
22-
| accountEmpty8 | 520 | 🟢 **-2,643 (83.56%)** |
23-
| accountSizedInit1 | 1,997 | 🟢 **-3,195 (61.54%)** |
24-
| accountSized1 | 195 | 🟢 **-498 (71.86%)** |
25-
| accountSizedInit2 | 3,606 | 🟢 **-5,883 (62.00%)** |
26-
| accountSized2 | 246 | 🟢 **-829 (77.12%)** |
27-
| accountSizedInit4 | 6,832 | 🟢 **-11,338 (62.40%)** |
28-
| accountSized4 | 336 | 🟢 **-1,512 (81.82%)** |
29-
| accountSizedInit8 | 13,274 | 🟢 **-22,159 (62.54%)** |
30-
| accountSized8 | 519 | 🟢 **-2,868 (84.68%)** |
31-
| accountUnsizedInit1 | 1,997 | 🟢 **-3,308 (62.36%)** |
32-
| accountUnsized1 | 193 | 🟢 **-553 (74.13%)** |
33-
| accountUnsizedInit2 | 3,605 | 🟢 **-6,154 (63.06%)** |
34-
| accountUnsized2 | 246 | 🟢 **-917 (78.85%)** |
35-
| accountUnsizedInit4 | 6,831 | 🟢 **-11,772 (63.28%)** |
36-
| accountUnsized4 | 336 | 🟢 **-1,666 (83.22%)** |
37-
| accountUnsizedInit8 | 13,273 | 🟢 **-22,720 (63.12%)** |
38-
| accountUnsized8 | 520 | 🟢 **-3,153 (85.84%)** |
39-
| boxedAccountEmptyInit1 | 2,009 | 🟢 **-3,166 (61.18%)** |
40-
| boxedAccountEmpty1 | 211 | 🟢 **-523 (71.25%)** |
41-
| boxedAccountEmptyInit2 | 3,631 | 🟢 **-5,783 (61.43%)** |
42-
| boxedAccountEmpty2 | 282 | 🟢 **-834 (74.73%)** |
43-
| boxedAccountEmptyInit4 | 6,881 | 🟢 **-11,037 (61.60%)** |
44-
| boxedAccountEmpty4 | 408 | 🟢 **-1,464 (78.21%)** |
45-
| boxedAccountEmptyInit8 | 13,370 | 🟢 **-21,583 (61.75%)** |
46-
| boxedAccountEmpty8 | 672 | 🟢 **-2,729 (80.24%)** |
47-
| boxedAccountSizedInit1 | 2,016 | 🟢 **-3,255 (61.75%)** |
48-
| boxedAccountSized1 | 210 | 🟢 **-573 (73.18%)** |
49-
| boxedAccountSizedInit2 | 3,646 | 🟢 **-5,937 (61.95%)** |
50-
| boxedAccountSized2 | 281 | 🟢 **-909 (76.39%)** |
51-
| boxedAccountSizedInit4 | 6,909 | 🟢 **-11,321 (62.10%)** |
52-
| boxedAccountSized4 | 409 | 🟢 **-1,587 (79.51%)** |
53-
| boxedAccountSizedInit8 | 13,425 | 🟢 **-22,128 (62.24%)** |
54-
| boxedAccountSized8 | 673 | 🟢 **-2,955 (81.45%)** |
55-
| boxedAccountUnsizedInit1 | 2,016 | 🟢 **-3,355 (62.47%)** |
56-
| boxedAccountUnsized1 | 210 | 🟢 **-626 (74.88%)** |
57-
| boxedAccountUnsizedInit2 | 3,645 | 🟢 **-6,114 (62.65%)** |
58-
| boxedAccountUnsized2 | 282 | 🟢 **-988 (77.80%)** |
59-
| boxedAccountUnsizedInit4 | 6,909 | 🟢 **-11,649 (62.77%)** |
60-
| boxedAccountUnsized4 | 408 | 🟢 **-1,724 (80.86%)** |
61-
| boxedAccountUnsizedInit8 | 13,425 | 🟢 **-22,760 (62.90%)** |
62-
| boxedAccountUnsized8 | 672 | 🟢 **-3,209 (82.68%)** |
15+
| accountEmptyInit1 | 1,984 | 🟢 **-3,099 (60.97%)** |
16+
| accountEmpty1 | 188 | 🟢 **-457 (70.85%)** |
17+
| accountEmptyInit2 | 3,582 | 🟢 **-5,719 (61.49%)** |
18+
| accountEmpty2 | 235 | 🟢 **-772 (76.66%)** |
19+
| accountEmptyInit4 | 6,785 | 🟢 **-10,979 (61.80%)** |
20+
| accountEmpty4 | 316 | 🟢 **-1,408 (81.67%)** |
21+
| accountEmptyInit8 | 13,178 | 🟢 **-21,545 (62.05%)** |
22+
| accountEmpty8 | 478 | 🟢 **-2,685 (84.89%)** |
23+
| accountSizedInit1 | 1,990 | 🟢 **-3,202 (61.67%)** |
24+
| accountSized1 | 190 | 🟢 **-503 (72.58%)** |
25+
| accountSizedInit2 | 3,592 | 🟢 **-5,897 (62.15%)** |
26+
| accountSized2 | 236 | 🟢 **-839 (78.05%)** |
27+
| accountSizedInit4 | 6,804 | 🟢 **-11,366 (62.55%)** |
28+
| accountSized4 | 316 | 🟢 **-1,532 (82.90%)** |
29+
| accountSizedInit8 | 13,218 | 🟢 **-22,215 (62.70%)** |
30+
| accountSized8 | 477 | 🟢 **-2,910 (85.92%)** |
31+
| accountUnsizedInit1 | 1,990 | 🟢 **-3,315 (62.49%)** |
32+
| accountUnsized1 | 188 | 🟢 **-558 (74.80%)** |
33+
| accountUnsizedInit2 | 3,591 | 🟢 **-6,168 (63.20%)** |
34+
| accountUnsized2 | 236 | 🟢 **-927 (79.71%)** |
35+
| accountUnsizedInit4 | 6,803 | 🟢 **-11,800 (63.43%)** |
36+
| accountUnsized4 | 316 | 🟢 **-1,686 (84.22%)** |
37+
| accountUnsizedInit8 | 13,217 | 🟢 **-22,776 (63.28%)** |
38+
| accountUnsized8 | 478 | 🟢 **-3,195 (86.99%)** |
39+
| boxedAccountEmptyInit1 | 2,004 | 🟢 **-3,171 (61.28%)** |
40+
| boxedAccountEmpty1 | 206 | 🟢 **-528 (71.93%)** |
41+
| boxedAccountEmptyInit2 | 3,621 | 🟢 **-5,793 (61.54%)** |
42+
| boxedAccountEmpty2 | 271 | 🟢 **-845 (75.72%)** |
43+
| boxedAccountEmptyInit4 | 6,861 | 🟢 **-11,057 (61.71%)** |
44+
| boxedAccountEmpty4 | 387 | 🟢 **-1,485 (79.33%)** |
45+
| boxedAccountEmptyInit8 | 13,330 | 🟢 **-21,623 (61.86%)** |
46+
| boxedAccountEmpty8 | 630 | 🟢 **-2,771 (81.48%)** |
47+
| boxedAccountSizedInit1 | 2,009 | 🟢 **-3,262 (61.89%)** |
48+
| boxedAccountSized1 | 205 | 🟢 **-578 (73.82%)** |
49+
| boxedAccountSizedInit2 | 3,632 | 🟢 **-5,951 (62.10%)** |
50+
| boxedAccountSized2 | 270 | 🟢 **-920 (77.31%)** |
51+
| boxedAccountSizedInit4 | 6,881 | 🟢 **-11,349 (62.25%)** |
52+
| boxedAccountSized4 | 388 | 🟢 **-1,608 (80.56%)** |
53+
| boxedAccountSizedInit8 | 13,369 | 🟢 **-22,184 (62.40%)** |
54+
| boxedAccountSized8 | 631 | 🟢 **-2,997 (82.61%)** |
55+
| boxedAccountUnsizedInit1 | 2,009 | 🟢 **-3,362 (62.60%)** |
56+
| boxedAccountUnsized1 | 205 | 🟢 **-631 (75.48%)** |
57+
| boxedAccountUnsizedInit2 | 3,631 | 🟢 **-6,128 (62.79%)** |
58+
| boxedAccountUnsized2 | 271 | 🟢 **-999 (78.66%)** |
59+
| boxedAccountUnsizedInit4 | 6,881 | 🟢 **-11,677 (62.92%)** |
60+
| boxedAccountUnsized4 | 387 | 🟢 **-1,745 (81.85%)** |
61+
| boxedAccountUnsizedInit8 | 13,369 | 🟢 **-22,816 (63.05%)** |
62+
| boxedAccountUnsized8 | 630 | 🟢 **-3,251 (83.77%)** |
6363
| boxedInterfaceAccountMint1 | 233 | 🟢 **-1,118 (82.75%)** |
6464
| boxedInterfaceAccountMint2 | 323 | 🟢 **-1,800 (84.79%)** |
6565
| boxedInterfaceAccountMint4 | 483 | 🟢 **-3,173 (86.79%)** |

star_frame/src/account_set/mod.rs

Lines changed: 62 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,7 @@ pub trait ProgramAccount: HasOwnerProgram {
3434
#[allow(clippy::inline_always)]
3535
#[inline(always)]
3636
fn validate_account_info(info: AccountInfo) -> Result<()> {
37-
let data = info.account_data()?;
38-
39-
if data.len() < size_of::<OwnerProgramDiscriminant<Self>>() {
40-
bail!(
41-
"Account {} data length {} is less than expected discriminant size {}",
42-
info.pubkey(),
43-
data.len(),
44-
size_of::<OwnerProgramDiscriminant<Self>>()
45-
);
46-
}
47-
48-
// Choose optimal comparison strategy based on discriminator length
49-
if !compare_discriminator(&data, bytes_of(&Self::DISCRIMINANT)) {
50-
bail!(
51-
"Account {} data does not match expected discriminant for program {}",
52-
info.pubkey(),
53-
Self::OwnerProgram::ID
54-
);
55-
}
37+
validate_discriminant::<Self>(info)?;
5638

5739
if !info.owner().fast_eq(&Self::OwnerProgram::ID) {
5840
bail!(
@@ -67,55 +49,74 @@ pub trait ProgramAccount: HasOwnerProgram {
6749
}
6850
}
6951

70-
/// Fast discriminator comparison, with fast path unaligned reads for small discriminators.
52+
/// Fast discriminant comparison, with fast path unaligned reads for small discriminants.
7153
///
7254
/// Adapted from [Typhoon](https://github.com/exotic-markets-labs/typhoon/blob/60c5197cc632f1bce07ba27876669e4ca8580421/crates/accounts/src/discriminator.rs#L8)
7355
#[allow(clippy::inline_always)]
7456
#[inline(always)]
75-
#[must_use]
76-
pub fn compare_discriminator(data: &[u8], discriminator: &[u8]) -> bool {
77-
let len = discriminator.len();
78-
// Choose optimal comparison strategy based on discriminator length
79-
match len {
80-
0 => true, // No discriminator to check
81-
1..=8 => {
82-
// Use unaligned integer reads for small discriminators (most common case)
83-
// SAFETY: We've already verified that data.len() >= discriminator.len()
84-
// in the caller before calling this function, so we know we have at least
85-
// `len` bytes available for reading. Unaligned reads are safe for primitive
86-
// types on all supported architectures. The pointer casts to smaller integer
87-
// types (u16, u32, u64) are valid because we're only reading the exact number
88-
// of bytes specified by `len`.
89-
unsafe {
90-
// We are reading unaligned, so the casts are fine
91-
#[allow(clippy::cast_ptr_alignment)]
92-
let data_ptr = data.as_ptr().cast::<u64>();
93-
#[allow(clippy::cast_ptr_alignment)]
94-
let disc_ptr = discriminator.as_ptr().cast::<u64>();
95-
96-
match len {
97-
1 => *data.get_unchecked(0) == *discriminator.get_unchecked(0),
98-
2 => {
99-
let data_val = data_ptr.cast::<u16>().read_unaligned();
100-
let disc_val = disc_ptr.cast::<u16>().read_unaligned();
101-
data_val == disc_val
102-
}
103-
4 => {
104-
let data_val = data_ptr.cast::<u32>().read_unaligned();
105-
let disc_val = disc_ptr.cast::<u32>().read_unaligned();
106-
data_val == disc_val
107-
}
108-
8 => {
109-
let data_val = data_ptr.read_unaligned();
110-
let disc_val = disc_ptr.read_unaligned();
111-
data_val == disc_val
112-
}
113-
_ => data[..len] == discriminator[..],
114-
}
57+
fn validate_discriminant<T: ProgramAccount + ?Sized>(info: AccountInfo) -> Result<()> {
58+
// This check should be optimized out
59+
if size_of::<OwnerProgramDiscriminant<T>>() == 0 {
60+
return Ok(());
61+
}
62+
63+
// Ensure account data is at least the size of the discriminant
64+
if info.data_len() < size_of::<OwnerProgramDiscriminant<T>>() {
65+
bail!(
66+
"Account {} data length {} is less than expected discriminant size {}",
67+
info.pubkey(),
68+
info.data_len(),
69+
size_of::<OwnerProgramDiscriminant<T>>()
70+
);
71+
}
72+
73+
info.can_borrow_data()?;
74+
let data_ptr = info.data_ptr();
75+
76+
// SAFETY:
77+
// We've already verified that data.len() >= discriminant.len(),
78+
// so we know we have at least `len` bytes available for reading (so can cast to slice).
79+
// Unaligned reads are safe for primitive types on all supported architectures.
80+
// The pointer casts to smaller integer types (u16, u32, u64) are valid because we're
81+
// only reading the exact number of bytes specified by `len`.
82+
let matches = unsafe {
83+
// We are reading unaligned, so the casts are fine
84+
// Choose optimal comparison strategy based on discriminant length
85+
#[allow(clippy::cast_ptr_alignment)]
86+
#[allow(clippy::cast_ptr_alignment)]
87+
match size_of::<OwnerProgramDiscriminant<T>>() {
88+
1 => *data_ptr == bytemuck::cast::<_, u8>(T::DISCRIMINANT),
89+
2 => {
90+
let data_val = data_ptr.cast::<u16>().read_unaligned();
91+
let disc_val = bytemuck::cast::<_, u16>(T::DISCRIMINANT);
92+
data_val == disc_val
93+
}
94+
4 => {
95+
let data_val = data_ptr.cast::<u32>().read_unaligned();
96+
let disc_val = bytemuck::cast::<_, u32>(T::DISCRIMINANT);
97+
data_val == disc_val
98+
}
99+
8 => {
100+
let data_val = data_ptr.cast::<u64>().read_unaligned();
101+
let disc_val = bytemuck::cast::<_, u64>(T::DISCRIMINANT);
102+
data_val == disc_val
103+
}
104+
_ => {
105+
let data =
106+
slice::from_raw_parts(data_ptr, size_of::<OwnerProgramDiscriminant<T>>());
107+
data == bytemuck::bytes_of(&T::DISCRIMINANT)
115108
}
116109
}
117-
_ => data[..len] == discriminator[..],
110+
};
111+
if !matches {
112+
bail!(
113+
"Account {} data does not match expected discriminant for program {}",
114+
info.pubkey(),
115+
T::OwnerProgram::ID
116+
);
118117
}
118+
119+
Ok(())
119120
}
120121

121122
/// Convenience methods for decoding and validating a list of [`AccountInfo`]s to an [`AccountSet`].

0 commit comments

Comments
 (0)