Skip to content

Reduce allocations on config getter and method_missing paths - #146

Open
tas50 wants to merge 1 commit into
chef:mainfrom
tas50:perf-hot-paths
Open

tas50 wants to merge 1 commit into
chef:mainfrom
tas50:perf-hot-paths

Conversation

@tas50

@tas50 tas50 commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Description

Profiling mixlib-config with chef-config as the workload showed a few places where the library allocates more than it needs to on hot paths. This removes that overhead without changing behavior.

  • Getter and setter methods: define_attr_accessor_methods collected *args and then re-splatted them into internal_get_or_set(symbol, *args), allocating a second array on every call. internal_get_or_set (private) now takes the array directly.
  • method_missing: setter names were detected with method_symbol.to_s =~ /(.+)=$/, which allocates a String, a MatchData, and a capture on every call. It now uses Symbol#name (a cached frozen string) with end_with?/chomp, with the same result for every name, including :==.
  • apply_nested_hash: called internal_get(k.to_sym) up to twice per Hash-valued key, which evaluated block defaults twice when loading a hash (from YAML, JSON, TOML, or from_hash) over them. Each key is now looked up once. For example, chef-config's cache_options default block previously ran twice on the first load and now runs once.

Benchmarks

Ruby 4.0.7, chef-config 19.3.15, benchmark-ips:

Call Before After Allocations
Config.chef_server_url (getter) 2.71M i/s 3.11M i/s (+15%) 2 → 1
Config.daemonize false (setter) 1.62M i/s 1.77M i/s (+9%) 2 → 1
unknown key read via method_missing 1.46M i/s 1.70M i/s (+16%) 4 → 2
unknown key set via method_missing 0.83M i/s 1.01M i/s (+21%) 7 → 4
Config.knife[:editor] (nested context) 1.18M i/s 1.25M i/s (+5%) 2 → 1
Config[:chef_server_url] (control, unchanged) 3.96M i/s 3.97M i/s 0

One allocation remains for getters because a *args parameter always allocates, even when nothing is passed. Removing it would mean dropping support for multi-argument setters like Config.foo 1, 2.

Related findings (not changed here)

  • mixlib-config accounts for about 0.4% of chef-config's load time; the rest is requires and subprocesses chef-config runs while loading.
  • save(true) on chef-config runs at about 577 calls/s. 97% of that is File.readable?/exist?/writable? calls inside chef-config's block defaults (for example user_home is evaluated 44 times per call). Block defaults are recomputed on every read by design, so any caching belongs in chef-config, not here.

Testing

  • bundle exec rspec: 160 examples, 0 failures

Types of changes

  • Performance improvement (non-breaking change)

- Pass the args array to internal_get_or_set instead of re-splatting
  it, which allocated a second array on every call to a defined
  getter or setter
- Detect setter names in method_missing with Symbol#name and
  end_with? instead of a regex, avoiding a String, MatchData, and
  capture per call
- Look up each key once in apply_nested_hash, so a block default is
  no longer evaluated twice when a hash value is loaded over it

With chef-config as the workload, method-style reads are ~15% faster
and unknown keys read or set through method_missing are 16-21% faster.

Signed-off-by: Tim Smith <tsmith84@proton.me>
@tas50
tas50 requested review from a team and jaymzh as code owners September 28, 2026 02:16
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.

1 participant