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 — copy.copy and copy.deepcopy scale poorly with free-threading · Issue #132657 · python/cpython

copy.copy and copy.deepcopy scale poorly with free-threading #132657

New issue
Copy link
New issue
Copy link
Closed
Closed
copy.copy and copy.deepcopy scale poorly with free-threading#132657
Copy link
Labels
performancePerformance or resource usagePerformance or resource usagestdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-free-threadingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@eendebakpt
eendebakpt
opened on Apr 17, 2025
Issue body actions

The copy.copy and copy.deepcopy operations running on different objects in different threads should be able to run independently. The scaling is not good: output of the ftscalingbench.py with added benchmarks:

Running benchmarks with 8 threads
shallow_copy               4.6x slower
deepcopy                   2.3x slower
object_cfunction           5.9x faster
cmodule_function           5.9x faster
object_lookup_special      5.5x faster
mult_constant              5.6x faster
generator                  4.9x faster
pymethod                   5.2x faster
pyfunction                 4.5x faster
module_function            5.0x faster
load_string_const          5.6x faster
load_tuple_const           5.9x faster
create_pyobject            2.3x faster
create_closure             7.1x faster
create_dict                3.3x faster
thread_local_read          3.3x faster

There are at least two reasons:

  • The copy module uses module level variables in the copy.copy and copy.deepcopy methods.
  • The _copy_atomic_types (and some similar data structures) is a set which requires locking for membership testing.

Linked PRs

  • gh-132657: Add free-threading scaling benchmark for copy.copy and copy.deepcopy #132658
  • gh-132657: Avoid locking in frozenset.__contains__ #132659
  • gh-132657: Avoid locks and refcounts in frozenset operations #136107
  • GH-132657: Add lock-free set contains implementation #132290
  • gh-132657: Make deepcopy and copy scale with free-threading #138429
  • gh-132657: optimize PySet_Contains for frozensets #141183
  • [3.14] gh-132657: avoid locks and refcounting in frozenset lookups (GH-136107) #141772
  • [3.14] gh-132657: optimize PySet_Contains for frozenset (GH-141183) #141773
  • gh-132657: revert lock-free set contains #142733
  • gh-132657: Use stronger memory ordering for so->mask. #142735
  • gh-132657: Add maybe_enable_deferred_ref_count(). #142843
  • gh-132657: Improve performance in specialize_dict_access_hint() via adding specialization for PY_GIL_DISABLED #144886

Activity

Next
  1. eendebakpt
    changed the title [-]copy.copy and copy.deepcopy scale with free-threading[/-] [+]copy.copy and copy.deepcopy scale poorly with free-threading[/+] on Apr 17, 2025
  2. picnixz
    added
    performancePerformance or resource usagePerformance or resource usage
    stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directory
    topic-free-threading
    type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    on Apr 17, 2025
  3. eendebakpt commented on Apr 17, 2025

    @eendebakpt
    eendebakpt
    on Apr 17, 2025
    ContributorAuthor
    More actions

    I have no good approach yet to improve performance of the module level variables. This diff

    diff --git a/Lib/copy.py b/Lib/copy.py
    index c64fc076179..084b2873b65 100644
    --- a/Lib/copy.py
    +++ b/Lib/copy.py
    @@ -67,7 +67,8 @@ def copy(x):
    
         cls = type(x)
    
    -    if cls in _copy_atomic_types:
    +    _local_atomic_types =  {int, float, bool, complex, bytes, str, type, range, property}
    +    if cls in _local_atomic_types or cls in _atomic_types:
             return x
         if cls in _copy_builtin_containers:
             return cls.copy(x)
    

    improves the scaling of copy.deepcopy a lot, but the solution is not very elegant and does not work for all test cases.

  4. colesbury commented on Apr 18, 2025

    @colesbury
    colesbury
    on Apr 18, 2025
    Contributor
    More actions

    The copy module uses module level variables in the copy.copy and copy.deepcopy methods

    We've discussed making module level variables use deferred reference counting, which would address this problem, but haven't decided exactly how we want to implement it. (All global variables? Only some frequently accessed ones?)

  5. kumaraditya303
    added a commit that references this issue on Apr 18, 2025

    gh-132657: Avoid locking in frozenset.__contains__ (#132659)

    e77d678
  6. eendebakpt commented on Apr 21, 2025

    @eendebakpt
    eendebakpt
    on Apr 21, 2025
    ContributorAuthor
    More actions

    Ok, I will leave this open until the deferred reference counting is in place. I tried making the relevant variables (e.g. copy._copy_atomic_types immortal, but that did not seem to improve performance).

  7. eendebakpt commented on Jun 26, 2025

    @eendebakpt
    eendebakpt
    on Jun 26, 2025
    ContributorAuthor
    More actions

    There is a bit more going on than just refcount contention. The code

    s = frozenset({1, 2, 3})
    z in s
    

    scales poorly in the FT build.

    • The reason is that the z in s is specialized to _CONTAINS_OP_SET which calls _PySet_Contains (here) which has a lock

    cpython/Objects/setobject.c

    Lines 2235 to 2242 in 0d76dcc

    _PySet_Contains(PySetObject *so, PyObject *key)
    {
    int rv;
    Py_BEGIN_CRITICAL_SECTION(so);
    rv = set_contains_lock_held(so, key);
    Py_END_CRITICAL_SECTION();
    return rv;
    }

    • A second reason is there might be refcount contention in set_lookkey

    cpython/Objects/setobject.c

    Lines 103 to 110 in 0d76dcc

    table = so->table;
    Py_INCREF(startkey);
    cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
    Py_DECREF(startkey);
    if (cmp < 0)
    return NULL;
    if (table != so->table || entry->key != startkey)
    return set_lookkey(so, key, hash);

    The increment is there because startkey is a borrowed reference and the rich compare might remove startkey from the set. For a frozenset this cannot happen, so we can skip the incref/decref. For a frozenset we can also skip the guard if (table != so->table || entry->key != startkey).

    To address the issues we can either

    • Make dedicated methods for frozenset (this requires some duplication of code and a new _CONTAINS_OP_FROZENSET bytecode)
    • Add a quick check using PyFrozenSet_CheckExact at the appropriate places and act accordingly.

    @colesbury @markshannon Any opinion on which way to go?

  8. colesbury commented on Jun 26, 2025

    @colesbury
    colesbury
    on Jun 26, 2025
    Contributor
    More actions

    I think adding a code path for PyFrozenSet_CheckExact that avoids the locking and incref/decref makes sense.

  9. MEDX000
    added this to MEDX-arabic-programmeon Jun 26, 2025
  10. github-project-automation
    moved this to Todo in MEDX-arabic-programmeon Jun 26, 2025
  11. kumaraditya303
    added 2 commits that reference this issue on Aug 30, 2025

    gh-132657: avoid locks and refcounting in `frozenset` lookups (#136107)

    f58a7c7

    gh-132657: improve `deepcopy` and `copy` scaling on free-threading (#…

    e46d403
  12. 19 remaining items

  13. nascheme
    added a commit that references this issue on Dec 15, 2025

    gh-132657: Use stronger memory ordering for so->mask. (gh-142735)

    19c72d2
  14. nascheme commented on Dec 15, 2025

    @nascheme
    nascheme
    on Dec 15, 2025
    Member
    More actions

    @colesbury Perhaps this can be closed now? Results below are for 8 threads after merging #132290 . The modified ftscaling script is linked from that PR. Maybe we want to revise the benchmark before closing this.

    FT scaling benchmark 3.14.2t main
    dict_contains 7.6x faster 7.9x faster
    tuple_contains 7.7x faster 7.8x faster
    list_contains 7.6x faster 7.6x faster
    frozenset_contains 7.5x faster 7.8x faster
    frozenset_contains_dunder 7.7x faster 7.8x faster
    set_contains 4.8x slower 7.5x faster
    set_contains_dunder 3.4x slower 7.7x faster
    shallow_copy 2.3x slower 1.2x faster
    deepcopy 1.6x slower 3.0x faster
    Reacted by Sergey Miryanov
  15. colesbury commented on Dec 15, 2025

    @colesbury
    colesbury
    on Dec 15, 2025
    Contributor
    More actions

    @nascheme - that seems improved, but it looks like there are still scaling bottlenecks. I guess they're from reference count contention when loading the same global variable in multiple threads?

  16. eendebakpt commented on Dec 15, 2025

    @eendebakpt
    eendebakpt
    on Dec 15, 2025
    ContributorAuthor
    More actions

    @nascheme @colesbury There is still contention on the module level attributes. By addressing that the scaling can be improved:

    Main:

    shallow_copy_atomic_type   1.6x slower
    shallow_copy_list          1.2x faster
    deepcopy                   2.6x faster
    deepcopy_dataclass         3.3x faster
    

    PR

    shallow_copy_atomic_type   5.8x faster
    shallow_copy_list          4.7x faster
    deepcopy                   4.4x faster
    deepcopy_dataclass         3.0x faster
    

    This improvement is by adding the following 5 lines to the copy module:

    import testcapi
    testcapi.pyobject_enable_deferred_refcount(_copy_atomic_types)
    _testcapi.pyobject_enable_deferred_refcount(_copy_builtin_containers)
    _testcapi.pyobject_enable_deferred_refcount(_atomic_types)
    _testcapi.pyobject_enable_deferred_refcount(_deepcopy_dispatch)
    

    I do not really like the solution, as we could end up with many of these statemements in the codebase. (I would be in favor of adding pyobject_enable_deferred_refcount to sys , but there is issue #134819 for that already)

    We could also make all module level attributes use deferred reference counting. A branch to test this is

    main...eendebakpt:cpython:module_attributes_deferred_ref_counting

    (the implementation needs some work though)

  17. fatelei
    added a commit that references this issue on Dec 16, 2025

    pythongh-132657: Use stronger memory ordering for so->mask. (pythongh…

    460c423
  18. nascheme commented on Dec 16, 2025

    @nascheme
    nascheme
    on Dec 16, 2025
    Member
    More actions

    Maybe a crazy idea but what about something like the following. The idea is when we specialize to LOAD_GLOBAL_MODULE we also enable deferred ref counting for the value.

    --- a/Python/specialize.c
    +++ b/Python/specialize.c
    @@ -1305,6 +1305,13 @@ specialize_load_global_lock_held(
                 SPECIALIZATION_FAIL(LOAD_GLOBAL, SPEC_FAIL_OUT_OF_RANGE);
                 goto fail;
             }
    +#ifdef Py_GIL_DISABLED
    +        PyObject *value;
    +        if (PyDict_GetItemRef(globals, name, &value) == 1) {
    +            PyUnstable_Object_EnableDeferredRefcount(value);
    +            Py_DECREF(value);
    +        }
    +#endif
             cache->index = (uint16_t)index;
             cache->module_keys_version = (uint16_t)keys_version;
             specialize(instr, LOAD_GLOBAL_MODULE);
    

    With this change, the copy/deepcopy benchmarks scale quite a bit better:

    shallow_copy               5.6x faster
    deepcopy                   6.2x faster
    

    Better change, still quick and dirty:
    #142843
    It seems we only need to do it for frozensets, at least to help the copy scaling.

    Reacted by Pieter Eendebak
    Reacted by Sergey Miryanov
  19. colesbury commented on Dec 16, 2025

    @colesbury
    colesbury
    on Dec 16, 2025
    Contributor
    More actions

    I think something like that makes sense. The downside is that it means that the value won't get freed immediately if someone overwrites the global variable -- it will only get freed during GC. We might want to limit it to globals that are accessed by another thread.

    @nascheme - do you want to open a PR that we can use as a basis for discussion and investigation?

  20. nascheme
    added a commit that references this issue on Jan 17, 2026

    gh-132657: Add maybe_enable_deferred_ref_count() (gh-142843)

    bb25f72
  21. encukou commented on Jan 19, 2026

    @encukou
    encukou
    on Jan 19, 2026
    Member
    More actions

    Commit bb25f72 broke free-threading refleak tests:

    $ ./python -m test -R2:3 test__interpchannels
    [...]
    0:00:00 load avg: 1.25 [1/1] test__interpchannels
    beginning 5 repetitions. Showing number of leaks (. for 0 or less, X for 10 or more)
    12:345
    XX 222
    test__interpchannels leaked [1, 1, 1] references, sum=3
    test__interpchannels leaked [2, 2, 2] memory blocks, sum=6
    0:00:07 load avg: 1.29 [1/1/1] test__interpchannels failed (reference leak)
    
  22. nascheme commented on Jan 19, 2026

    @nascheme
    nascheme
    on Jan 19, 2026
    Member
    More actions

    I created a bug, see gh-144054.

  23. eendebakpt commented on Jan 20, 2026

    @eendebakpt
    eendebakpt
    on Jan 20, 2026
    ContributorAuthor
    More actions

    On current main I have these results (8 threads) which look good:

    shallow_copy               5.1x faster
    deepcopy                   4.3x faster
    deepcopy_dataclass         3.5x faster
    

    For the dataclass performance there is a separate issue open: #139103.

    If no objections, we can close the issue as completed.

  24. kumaraditya303
    closed this as completedon Jan 21, 2026
  25. github-project-automation
    moved this from Todo to Done in MEDX-arabic-programmeon Jan 21, 2026
  26. thunder-coding
    added a commit that references this issue on Feb 15, 2026

    pythongh-132657: Add maybe_enable_deferred_ref_count() (pythongh-142843)

    5a55f82
  27. bernhard-42
    added a commit that references this issue on Oct 12, 2026

    Compound._global_elements: cut the links to the ancestors explicitly,…

    9d3db2b
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 usagestdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-free-threadingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

    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.