As discussed on Bluesky, here's one of the LLM-generated performance improvement suggestions from profiling https://github.com/jonhoo/avdl on (synthetic) bigger .avdl files with the ANTLR-generated parser. I'd be happy to test out changes if you don't have a benchmark handy!
Symptom
Hash map operations in ATNConfigSet show significant self-time: ATNConfigSet::local_hash_key at 2.1% (81/3785 samples), ATNConfigSet::add_cached at 1.3% (49 samples), hashbrown::...::HashMap::insert at 2.3% (86 samples), and hashbrown::...::RawTable::reserve_rehash at 1.0% (37 samples). Together these account for ~6.7% of total self-time, a portion of which is caused by repeated reallocation of zero-initialized collections as configs are added one at a time.
Root cause
ATNConfigSet::new_base_atnconfig_set (atn_config_set.rs:95) creates configs: vec![] and config_lookup: HashMap::with_hasher(...) — both with zero capacity. During LL(*) prediction, configs are added one at a time via add_cached, triggering:
- Vec reallocation:
Vec::new() starts at capacity 0. The growth pattern (0 → 4 → 8 → 16 → ...) means the first ~4 configs cause 2 reallocations.
- HashMap rehashing:
HashMap::with_hasher(...) starts at capacity 0. Each growth step rehashes all existing entries. With the default load factor of 87.5%, a set of 10 configs triggers ~2 rehashes.
New ATNConfigSets are created frequently in the hot path: compute_reach_set (line 549, 580), compute_start_state (line 687), remove_all_configs_not_in_rule_stop_state (line 655), split_according_to_semantic_validity (lines 870–871), and compute_reach_set in lexer_atn_simulator.rs (line 286).
A typical parser decision point processes 5–20 configs per set.
Affected files
runtime/Rust/src/atn_config_set.rs:95–108 — constructor new_base_atnconfig_set, and new_ordered (line 111)
runtime/Rust/src/parser_atn_simulator.rs — call sites at lines 549, 580, 655, 687, 722, 870, 871
runtime/Rust/src/lexer_atn_simulator.rs — call sites at lines 286, 421
Suggested fix
-
Add a with_capacity(n: usize) constructor to ATNConfigSet:
pub fn with_capacity(full_ctx: bool, capacity: usize) -> ATNConfigSet {
ATNConfigSet {
cached_hash: 0,
config_lookup: HashMap::with_capacity_and_hasher(
capacity,
MurmurHasherBuilder {},
),
configs: Vec::with_capacity(capacity),
conflicting_alts: Default::default(),
dips_into_outer_context: false,
full_ctx,
has_semantic_context: false,
read_only: false,
unique_alt: 0,
hasher: Self::local_hash_key,
}
}
-
Replace ATNConfigSet::new_base_atnconfig_set(full_ctx) with ATNConfigSet::with_capacity(full_ctx, 16) at the hot-path call sites in parser_atn_simulator.rs. A capacity of 16 covers the vast majority of decision points (5–20 configs) without waste.
-
Similarly update new_ordered() (used in lexer_atn_simulator.rs) to pre-allocate.
This change is purely additive — the existing zero-capacity constructor remains available for cold paths or cases where the expected size is unknown.
Symptom
Hash map operations in
ATNConfigSetshow significant self-time:ATNConfigSet::local_hash_keyat 2.1% (81/3785 samples),ATNConfigSet::add_cachedat 1.3% (49 samples),hashbrown::...::HashMap::insertat 2.3% (86 samples), andhashbrown::...::RawTable::reserve_rehashat 1.0% (37 samples). Together these account for ~6.7% of total self-time, a portion of which is caused by repeated reallocation of zero-initialized collections as configs are added one at a time.Root cause
ATNConfigSet::new_base_atnconfig_set(atn_config_set.rs:95) createsconfigs: vec![]andconfig_lookup: HashMap::with_hasher(...)— both with zero capacity. During LL(*) prediction, configs are added one at a time viaadd_cached, triggering:Vec::new()starts at capacity 0. The growth pattern (0 → 4 → 8 → 16 → ...) means the first ~4 configs cause 2 reallocations.HashMap::with_hasher(...)starts at capacity 0. Each growth step rehashes all existing entries. With the default load factor of 87.5%, a set of 10 configs triggers ~2 rehashes.New
ATNConfigSets are created frequently in the hot path:compute_reach_set(line 549, 580),compute_start_state(line 687),remove_all_configs_not_in_rule_stop_state(line 655),split_according_to_semantic_validity(lines 870–871), andcompute_reach_setinlexer_atn_simulator.rs(line 286).A typical parser decision point processes 5–20 configs per set.
Affected files
runtime/Rust/src/atn_config_set.rs:95–108— constructornew_base_atnconfig_set, andnew_ordered(line 111)runtime/Rust/src/parser_atn_simulator.rs— call sites at lines 549, 580, 655, 687, 722, 870, 871runtime/Rust/src/lexer_atn_simulator.rs— call sites at lines 286, 421Suggested fix
Add a
with_capacity(n: usize)constructor toATNConfigSet:Replace
ATNConfigSet::new_base_atnconfig_set(full_ctx)withATNConfigSet::with_capacity(full_ctx, 16)at the hot-path call sites inparser_atn_simulator.rs. A capacity of 16 covers the vast majority of decision points (5–20 configs) without waste.Similarly update
new_ordered()(used inlexer_atn_simulator.rs) to pre-allocate.This change is purely additive — the existing zero-capacity constructor remains available for cold paths or cases where the expected size is unknown.