Skip to content

Free-threaded build: deque iteration and list(deque) take a critical section per item #161850

Description

@113xiaoji

Feature or enhancement

In the free-threaded build, advancing a collections.deque iterator enters a two-object critical section on the iterator and the deque (dequeiter_next, dequereviter_next, dequeiter_reduce), which normally acquires and releases both mutexes in the uncontended path.

Materializing a deque does this for every item. This covers list(d), [*d], lst.extend(d), and PySequence_Fast(d), and therefore also ''.join(d), which passes the deque's iterator to list.extend().

This repeated synchronization is costly on the tested Kunpeng 950 (AArch64, LSE). In a hot, uncontended 8-bit CAS-pair C loop, the measured CPU cost was about 10 ns per logical CAS on the 950 and 2.5 ns on an EPYC 9654; these figures include loop/accounting work and are not isolated instruction latencies. Mako's buffer is a deque joined with ''.join(), and its bm_mako render took about 2.26 times as long in the free-threaded build as in the default build on the 950.

Two independent improvements:

  1. Lock only the deque when advancing an iterator. All writes to the iterator's fields already happen under the deque's lock, so that lock alone serializes concurrent next() calls on a shared iterator.
  2. Copy a deque into a list under one critical section. Add a fast path in list.extend() for deques and forward deque iterators, the same way list.extend() already handles sets and dicts. Since the deque type lives in _collections, the module registers the fast path with listobject.c.

Measured on 3.14.6, PGO+LTO, GCC 12.3.1, pinned to one core. Times are in µs for a deque of 1000 strings, on the Kunpeng 950:

Operation free-threaded before free-threaded after default before default after
''.join(d) 54.8 9.4 8.8 5.9
list(d) 50.3 5.1 4.7 1.8
for x in d: pass 52.3 30.6 6.4 6.4

bm_mako (unmodified Mako 1.1.4) on the free-threaded build, before → after:

  • Kunpeng 950: 19.09 → 12.75 ms (−33%)
  • EPYC 9654: 10.15 → 9.21 ms (−9%)

The default build also gets slightly faster.

The proposed branches target CPython main. The measurements above are from a CPython 3.14.6 backport of both changes; they are not measurements of the main-branch port.

Has this already been discussed elsewhere?

No response given

Links to previous discussion of this feature:

No response

Linked PRs

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

    interpreter-core(Objects, Python, Grammar, and Parser dirs)performancePerformance or resource usagetype-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions