Skip to content

gh-150818: Speed up logging.getLogger() for existing loggers#150825

Open
gaborbernat wants to merge 1 commit into
python:mainfrom
gaborbernat:opt/logging-getlogger-fastpath
Open

gh-150818: Speed up logging.getLogger() for existing loggers#150825
gaborbernat wants to merge 1 commit into
python:mainfrom
gaborbernat:opt/logging-getlogger-fastpath

Conversation

@gaborbernat
Copy link
Copy Markdown
Contributor

@gaborbernat gaborbernat commented Jun 2, 2026

logging.getLogger(name) returns the singleton logger for a name, creating it on first use. Every call takes the logging lock, even the overwhelmingly common case where the logger already exists and is returned unchanged. Libraries fetch their logger by name throughout their code, at module import and again inside functions, so the same handful of names are looked up over and over for the life of a process. Those repeat lookups are pure overhead: the logger is already there.

This returns an existing, fully-initialised logger through a lock-free fast path before falling back to the locked section. The fast path reads loggerDict with a single dict.get(), which is atomic under both the GIL and free threading, and a Logger is inserted into that dict only after it is fully constructed under the lock, so the fast path never observes a half-built object or a placeholder. First-time creation, placeholder resolution and the parent/child wiring still run under the lock exactly as before.

Resolving logger names collected from the top-1000 corpus improves from 6.68 µs to 5.02 µs, 33% faster.

Benchmark base patched
getLogger, existing loggers 6.68 µs 5.02 µs: 33% faster
Benchmark (pyperf)

Run base vs patched by swapping Lib/logging/__init__.py on the same interpreter. The names are real getLogger() string arguments mined from the top-1000 corpus.

import logging, pyperf

names = ["elastic_transport.transport", "peewee.pool", "LiteLLM", "httpx",
    "uvicorn.error", "uvicorn.access", "fsspec", "boto3", "posthog", "amqp",
    "nox", "tldextract.cache", "dulwich.lfs", "urllib3.connectionpool",
    "asyncio", "sqlalchemy.engine", "werkzeug", "django.request", "celery.worker",
    "kafka.conn", "redis.client", "paramiko.transport", "matplotlib.font_manager"]
for n in names:
    logging.getLogger(n)   # pre-create, as modules do at import

runner = pyperf.Runner()
runner.bench_func("getLogger %d existing loggers" % len(names),
                  lambda: [logging.getLogger(n) for n in names])

Resolves #150818.

getLogger() took the logging lock on every call, including the common case of
an already-registered logger. Return that logger through a lock-free fast path
backed by an atomic dict lookup. First-time creation, placeholder resolution
and parent/child wiring still run under the lock, and the fast path is safe
under both the GIL and free threading.
@gaborbernat gaborbernat force-pushed the opt/logging-getlogger-fastpath branch from defd276 to 9c0df96 Compare June 2, 2026 23:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Speed up logging.getLogger() for already-registered loggers

1 participant