Skip to content

Perf: compute global features once per file; single hash lookup in Feature::evaluate - #19

Merged
marirs merged 1 commit into
marirs:masterfrom
mnaza:perf/global-features-cache
Aug 1, 2026
Merged

marirs merged 1 commit into
marirs:masterfrom
mnaza:perf/global-features-cache

Conversation

@mnaza

@mnaza mnaza commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Closes #18.

1. Cache global features per extractor

extract_global_features paid a full goblin::Object::parse(self.buf()) for the OS feature on every call — and it is called per instruction (extract_insn_features), per basic block and per function. OS/arch are constant per file, so the result is now computed once and cached in a once_cell::sync::OnceCell on the smda extractor. The sync flavour is required because find_capabilities shares the extractor across rayon worker threads; get_or_try_init keeps error propagation intact.

Measured with a harness that runs extract_insn_features over every instruction (release build, identical extracted feature sets before/after):

input before after speedup
data/mimikatz.exe_ (808 KiB, ~136k insns) ~8.4 s ~0.2 s ~40×
data/Demo64.dll (29 KiB, ~1k insns) ~23 ms ~3.4 ms ~7×

The gain grows with file size (parse cost scales with the buffer), so large real-world binaries benefit the most.

2. Single hash lookup in Feature::evaluate

All 20 feature types (Api, Number, String, Os, …) used:

if features.contains_key(&Feature::X(self.clone())) {
    return Ok((true, features[&Feature::X(self.clone())].clone()));
}

— two hash-map lookups plus two clones of self (including its HashSet<Scope> heap allocation) on every hit, in the hottest operation of the rule engine ((rules × features) evaluations per function). All 20 sites now do a single features.get(...), the pattern RangeStatement in src/rules/statement.rs already used.

Tests: 13/13 pass, clippy clean, fmt applied, CHANGELOG entry under [Unreleased]. Independent of #17 (based on master); only the CHANGELOG [Unreleased] heading will trivially conflict with it.

…ate (closes marirs#18)

- smda extractor: extract_global_features paid a full goblin parse of
  the buffer for the OS feature on every call — and it was called per
  instruction, per basic block and per function. OS/arch are constant
  per file, so the result is now cached in a once_cell::sync::OnceCell
  (thread-safe for the rayon per-function loop). Instruction-level
  extraction on mimikatz.exe_ (808 KiB, ~136k insns) drops ~8.4s ->
  ~0.2s (~40x); Demo64.dll ~23ms -> ~3.4ms. Extracted feature sets are
  unchanged.
- rules/features: all 20 Feature::evaluate sites used contains_key +
  indexing, doing two hash lookups and cloning self (with its
  HashSet<Scope>) twice per hit in the hottest path of the engine.
  Now a single HashMap::get, matching RangeStatement's existing
  pattern.
@mnaza
mnaza force-pushed the perf/global-features-cache branch from c356be9 to 5b26ce4 Compare July 31, 2026 08:57
@marirs
marirs merged commit 6920a58 into marirs:master Aug 1, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Perf: full goblin re-parse per instruction/basic-block; double hash lookup + double clone in every Feature::evaluate

2 participants