GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Reference count contention with nested functions · Issue #124218 · python/cpython

Reference count contention with nested functions #124218

New issue
Copy link
New issue
Copy link
Closed
Closed
Reference count contention with nested functions#124218
Copy link
Labels
performancePerformance or resource usagePerformance or resource usagetopic-free-threading

Description

@colesbury
colesbury
opened on Sep 18, 2024
Issue body actions

Creating nested functions can be a source of reference count contention in the free-threaded build. Consider the following (contrived) example:

from concurrent.futures import ThreadPoolExecutor

def func(n):
    sum = 0
    for i in range(n):
        def square(x):
            return x ** 2
        sum += square(i)

with ThreadPoolExecutor(max_workers=8) as executor:
    future = executor.submit(func, 100000)
    print(future.result())

Creating many square functions concurrently causes reference count contention on the func_code, func_globals, and func_builtins fields:

cpython/Include/cpython/funcobject.h

Lines 11 to 16 in 21d2a9a

#define _Py_COMMON_FIELDS(PREFIX) \
PyObject *PREFIX ## globals; \
PyObject *PREFIX ## builtins; \
PyObject *PREFIX ## name; \
PyObject *PREFIX ## qualname; \
PyObject *PREFIX ## code; /* A code object, the __code__ attribute */ \

The code object and builtins and globals dictionaries are already configured to support deferred reference counting, but the references in PyFunctionObject are not _PyStackRefs -- they are normal "counted" references.

Note that this is an issue for nested functions (closures), but not module-level functions or methods because those are typically created infrequently.

I outline a few possibly ways to address this below. My preference is for 2a below.

Option 1: Use deferred reference counting in PyFunctionObject

Variant 1a: Use _PyStackRef in PyFunctionObject

Instead of PyObject *func_code we have _PyStackRef func_code. We use this strategy effectively in a number of other places, including the frame object and generators.

The downside of this approach is that the fields of PyFunctionObject are exposed in public headers (cpython/funcobject.h), even though they are not documented. Changing the type of func_code, func_globals, and func_builtins risks breaking backwards compatibility with some C API extensions.

Variant 1b: Use PyObject* and new bitfield

Instead of using _PyStackRef, we can keep the fields as PyObject * and store whether the field uses a deferred reference in a separate field. This was the approach I took by the nogil-3.9 fork.

This has fewer compatibility issues than using _PyStackRef, but there are still compatibility hazards. It would not be safe for extensions to change func_code/func_globals/func_builtins with something like Py_SETREF(func->func_globals, new_globals) because the reference counting semantics are different.

Option 2: Use per-thread reference counting

We already use per-thread reference counting for the references from instances to their types (i.e., ob_type), if the type is a heap type. Storing the reference counts per-thread avoids most of the reference count contention on the object. This also avoids the compatibility issues with option 1 because you can use a per-thread incref with a normal Py_DECREF -- the only risk is performance, not correctness.

The challenge with this approach is that we need some quick and reliable way to index the per-thread reference count array. For heap types, we added a new field unique_id in the free-threaded build. We can do something similar for code objects, but the globals and builtins are just "normal" dictionaries and I don't think we want to add a new field for every dictionary.

Variant 2a: Allocate space for identifier when creating globals and builtins.

When we create globals and builtins dictionaries we allocate space for an extra Py_ssize_t unique id at the end after the PyDictObject. The type would still just PyDict_Type, so Python and the rest of the C API would just see a normal dictionary. We can identify these special dictionaries using a bit in ob_gc_bits or by stealing another bit from ma_version_tag.

If the globals or builtins dictionaries are replaced by user defined dictionaries, than things would still work, they'd just might have scaling bottlenecks.

Variant 2b: Use a hash table for per-thread references

We can use a hash table to map PyObject* to their per-thread reference counts. This is less efficient than having a unique index into the per-thread reference count array, but avoids the need for an extra field.

Linked PRs

  • gh-124218: Refactor per-thread reference counting #124844
  • gh-124218: Use per-thread refcounts for code objects #125216
  • gh-124218: Use per-thread reference counting for globals and builtins #125713
  • gh-124218: Avoid refcount contention on builtins module #125847

Activity

  1. colesbury
    added
    performancePerformance or resource usagePerformance or resource usage
    topic-free-threading
    on Sep 18, 2024
  2. colesbury
    changed the title [-]Address reference count contention with nested functions[/-] [+]Reference count contention with nested functions[/+] on Sep 18, 2024
  3. markshannon commented on Sep 20, 2024

    @markshannon
    markshannon
    on Sep 20, 2024
    Member
    More actions

    __name__ and __qualname__ can be dealt with by faster-cpython/ideas#674
    That doesn't help with the other three, though.

    2a seems like the best solution. The dict version tag is being removed in 3.14, so most of that can be used for the ID.

    Just to be clear, the per-thread reference count would be in addition to the normal reference count and primarily to avoid contention when creating closures?

  4. colesbury commented on Sep 20, 2024

    @colesbury
    colesbury
    on Sep 20, 2024
    ContributorAuthor
    More actions

    Just to be clear, the per-thread reference count would be in addition to the normal reference count and primarily to avoid contention when creating closures?

    Yes, that's right.

  5. colesbury
    added 5 commits that reference this issue on Oct 1, 2024

    pythongh-124218: Refactor per-thread reference counting

    643dc25

    pythongh-124218: Refactor per-thread reference counting

    a2c5055

    pythongh-124218: Refactor per-thread reference counting

    0e1b00c

    Merge branch 'main' into pythongh-124218-unique-id

    5bc5d38

    gh-124218: Refactor per-thread reference counting (#124844)

    b482538
  6. colesbury
    added 11 commits that reference this issue on Oct 8, 2024

    pythongh-124218: Use per-thread refcounts for code objects

    41c9f7c

    pythongh-124218: Use per-thread refcounts for code objects

    85e05e4

    pythongh-124218: Use per-thread refcounts for code objects

    527aa2c

    pythongh-124218: Use per-thread refcounts for code objects

    018b2a1

    gh-124218: Use per-thread refcounts for code objects (#125216)

    3ea488a

    pythongh-124218: Use per-thread reference counting for globals and bu…

    f4bc71e

    pythongh-124218: Use per-thread reference counting for globals and bu…

    c318ca8

    pythongh-124218: Use per-thread reference counting for globals and bu…

    568d6b9

    gh-124218: Use per-thread reference counting for globals and builtins (…

    9b0bfba

    pythongh-124218: Avoid refcount contention on builtins module

    6283638

    gh-124218: Avoid refcount contention on builtins module (GH-125847)

    3c4a7fa
  7. colesbury
    closed this as completedon Oct 24, 2024
  8. ebonnal
    added 2 commits that reference this issue on Jan 12, 2025

    pythongh-124218: Use per-thread reference counting for globals and bu…

    8fa8d45

    pythongh-124218: Avoid refcount contention on builtins module (python…

    d1cbe18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    performancePerformance or resource usagePerformance or resource usagetopic-free-threading

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402520 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.