Skip to content

gh-153060: Add an empty frozendict singleton#153061

Open
vstinner wants to merge 6 commits into
python:mainfrom
vstinner:frozendict_empty
Open

gh-153060: Add an empty frozendict singleton#153061
vstinner wants to merge 6 commits into
python:mainfrom
vstinner:frozendict_empty

Conversation

@vstinner

@vstinner vstinner commented Jul 5, 2026

Copy link
Copy Markdown
Member

The singleton is created immortal to avoid refcount contention in Free Threading.

  • Add PyInterpreterState.dict_state.empty_frozendict.
  • Add _PyDict_Init() and _PyDict_Fini() to create and destroy the empty frozendict singleton.

The singleton is created immortal to avoid refcount contention in
Free Threading.

* Add PyInterpreterState.dict_state.empty_frozendict.
* Add _PyDict_Init() and _PyDict_Fini() to create and destroy the
  empty frozendict singleton.
@vstinner

vstinner commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

Examples:

>>> empty = frozendict()
>>> frozendict() is empty
True
>>> frozendict([]) is empty
True
>>> (empty | empty) is empty
True
>>> (empty | {}) is empty
True
>>> frozendict.fromkeys('') is empty
True

@vstinner

vstinner commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

cc @corona10

@corona10 corona10 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good to me, but give me enough time to take a look at see the detail.

@vstinner

vstinner commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

I ran a microbenchmark on Linux with CPU isolation on this change:

Benchmark ref2 singleton2
frozendict() 39.8 ns 19.6 ns: 2.04x faster
frozendict([]) 79.8 ns 92.3 ns: 1.16x slower
frozendict((x for x in ())) 230 ns 259 ns: 1.13x slower
frozendict() | {} 73.0 ns 50.0 ns: 1.46x faster
frozendict.fromkeys("") 133 ns 148 ns: 1.11x slower
frozendict({"a": 1, "b": 2, "c": 3, "d": 4}) 78.2 ns 85.7 ns: 1.10x slower
frozendict(x=1, y=2) | frozendict(z=3) 251 ns 278 ns: 1.11x slower
frozendict.fromkeys("abdef") 260 ns 266 ns: 1.02x slower
Geometric mean (ref) 1.06x faster

I'm surprised by the slowdown on these operations:

  • frozendict((x for x in ())): 230 ns => 259 ns (+29 ns)
  • frozendict.fromkeys(""): 133 ns => 148 ns (+15 ns)
  • frozendict(x=1, y=2) | frozendict(z=3): 251 ns => 278 ns (+27 ns)

bench.py script:

Details
import pyperf
runner = pyperf.Runner()

# Get the singleton
runner.timeit('frozendict()',
    stmt='frozendict()')
runner.timeit('frozendict([])',
    setup='iterable = []',
    stmt='frozendict(iterable)')
runner.timeit('frozendict((x for x in ()))',
    stmt='frozendict((x for x in ()))')
runner.timeit('frozendict() | {}',
    stmt='frozendict() | {}')
runner.timeit('frozendict.fromkeys("")',
    stmt='frozendict.fromkeys("")')

# Operations on some small frozen dictionaries
runner.timeit('frozendict({"a": 1, "b": 2, "c": 3, "d": 4})',
    setup='d = {"a": 1, "b": 2, "c": 3, "d": 4}',
    stmt='frozendict(d)')
runner.timeit('frozendict(x=1, y=2) | frozendict(z=3)',
    stmt='frozendict(x=1, y=2) | frozendict(z=3)')
runner.timeit('frozendict.fromkeys("abdef")',
    stmt='frozendict.fromkeys("abdef")')

@ZeroIntensity ZeroIntensity left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense to also add this to Py_GetConstantBorrowed.

PyMutex watcher_mutex; // Protects the watchers array (free-threaded builds)
_PyOnceFlag watcher_setup_once; // One-time optimizer watcher setup
PyDict_WatchCallback watchers[DICT_MAX_WATCHERS];
PyObject *empty_frozendict;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Singletons are usually supposed to be stored on _PyRuntime.static_objects.singletons, not on the interpreter state. Is there a reason we're deviating from that convention here?

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.

3 participants