Skip to content

Avoid kwargs unpacking in _process_event hot path#821

Open
Charisn wants to merge 1 commit into
hynek:mainfrom
Charisn:perf/process-event-update
Open

Avoid kwargs unpacking in _process_event hot path#821
Charisn wants to merge 1 commit into
hynek:mainfrom
Charisn:perf/process-event-update

Conversation

@Charisn

@Charisn Charisn commented Jun 20, 2026

Copy link
Copy Markdown

Summary

BoundLoggerBase._process_event runs on every non-filtered log call and is the hottest function in structlog. It assembles the event dict like this:

event_dict: Any = self._context.copy()
event_dict.update(**event_kw)

event_kw is always a plain dict — it originates from **event_kw in _proxy_to_logger — so update(**event_kw) unpacks it back into keyword arguments only for dict.update to repack them into a dict again. That ** call path materializes a fresh keyword dict and revalidates the keys on every single log call, even when event_kw is empty.

Passing the mapping positionally — event_dict.update(event_kw) — is semantically identical for a dict of string keys (both the resulting value and its type are unchanged, so custom context_classes are unaffected) and skips that machinery entirely.

Why it's safe

  • event_kw is guaranteed to be a dict, so there's no behavioral difference between the positional and keyword forms.
  • It's verified by the unchanged test suite (_process_event is exercised throughout tests/test_base.py and others): 276 passed, 1 skipped locally.

Numbers

Measured on CPython 3.14, averaged over several A/B rounds isolating the copy() + update() core of _process_event:

kwargs before after speedup
0 117 ns 89 ns 1.31×
1 134 ns 91 ns 1.48×
3 199 ns 150 ns 1.33×
8 258 ns 180 ns 1.44×

End-to-end through _proxy_to_logger (3-key bound context, 3 kwargs): ~919 ns → ~844 ns per call, roughly 8% faster. The win is present even with zero kwargs, because update(**{}) still pays for the empty-kwargs path.

Pull Request Check List

  • I acknowledge this project's AI policy.
  • This pull requests is not from my main branch.
  • There's tests for all new and changed code.
    • This is a behavior-preserving change with no new code paths; the existing tests for _process_event already cover the modified line, and they pass unchanged.
  • New APIs are added to our typing tests in api.py.
    • N/A — no API changes.
  • Updated documentation for changed code.
    • N/A — internal change only, no public signature or behavior change, so no versionchanged directive applies.
  • Documentation in .rst and .md files is written using semantic newlines.
  • Changes (and possible deprecations) are documented in the changelog.

`BoundLoggerBase._process_event` runs on every non-filtered log call and
is the hottest function in structlog. It assembled the event dict with:

    event_dict = self._context.copy()
    event_dict.update(**event_kw)

`event_kw` is always a plain `dict` -- it originates from `**event_kw` in
`_proxy_to_logger` -- so unpacking it back into keyword arguments only for
`dict.update` to repack them is pure overhead. The `update(**event_kw)`
call path materializes a fresh keyword dict and revalidates the keys on
every single log call, even when `event_kw` is empty.

Passing the mapping positionally -- `event_dict.update(event_kw)` -- is
semantically identical for a dict of string keys (both the resulting
value and its type are unchanged) but skips that machinery entirely.

Measured on CPython 3.14, averaged over several A/B rounds on the
`copy()` + `update()` core of `_process_event`:

    kwargs   before    after   speedup
    0        117 ns    89 ns   1.31x
    1        134 ns    91 ns   1.48x
    3        199 ns   150 ns   1.33x
    8        258 ns   180 ns   1.44x

End-to-end through `_proxy_to_logger` (3-key bound context, 3 kwargs):
~919 ns -> ~844 ns per call, roughly 8% faster. The improvement is
present even with zero kwargs, because `update(**{})` still pays for the
empty-kwargs path.

This is behavior-preserving; the existing test suite passes unchanged.
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