FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

gh-159558: Avoid re-entering an already held critical section in ctypes setters by johng · Pull Request #159562 · python/cpython · GitHub

Repository navigation

gh-159558: Avoid re-entering an already held critical section in ctypes setters - #159562

Open
johng wants to merge 4 commits into
python:mainfrom
johng:ft-ctypes-reentry
Open

johng wants to merge 4 commits into
python:mainfrom
johng:ft-ctypes-reentry

Conversation

johng commented Oct 16, 2026 •
edited
Loading

Copy link
Copy Markdown
Contributor

Fixing re-entering an already held lock in ctypes. This involves creating lock free version of implementation and also providing wrappers which also lock for callers that do not already hold a critical section.

Micro benchmark estimates.

Operation main this PR
c_int().value = 1 35 ns 22 ns
c_char_p().value = b"x" 39 ns 22 ns
pointer.contents = c_int(2) 86 ns 58 ns
Structure.i = 1 38 ns 24 ns
Structure.p = b"x" 69 ns 54 ns

johng added 4 commits October 16, 2026 18:23
…ree-threaded build

KeepRef() takes the critical section on its target so that it is safe
from any caller, and for a type that keeps a reference to the stored
object it then calls PyCData_GetContainer(), which takes the critical
section on the root object.  The Simple.value setter already holds the
lock on self, taken by its Argument Clinic wrapper, and for a top-level
object the root is self as well, so `c_char_p().value = b"x"` entered the
same critical section three times and `c_int().value = 1` twice (the
"nothing to keep" early return sits inside the locked region).

Add PyCData_GetContainer_lock_held() for a caller that holds the root's
lock, use it from KeepRef_lock_held() when the target is its own root,
and call KeepRef_lock_held() from the setter.

Measured on the free-threaded build: c_int().value = 1 35 -> 22 ns,
c_char_p().value = b"x" 39 -> 22 ns.

(cherry picked from commit 5ca398f)
…nts setter

The setter runs under a two-object critical section on the pointer and
the new target, then calls KeepRef() twice on the pointer and
GetKeepedObjects() on the target; both helpers lock their argument
again.  Call the lock-held variants instead, adding
GetKeepedObjects_lock_held() for the target.

Measured on the free-threaded build: p.contents = c_int(2) 86 -> 58 ns.

(cherry picked from commit 576731e)
PyCData_set() already requires its caller to hold the critical section
on the destination object and asserts it, but finished by calling
KeepRef(), which takes that lock again.  Every store of a Python object
into a Structure or Union field, an Array item or a Pointer item went
through it.  Call KeepRef_lock_held() instead.

Measured on the free-threaded build: Structure int field assignment
38 -> 24 ns, c_char_p field assignment 69 -> 54 ns.

(cherry picked from commit ead0976)
bedevere-app Bot added the type-feature A feature request or enhancement label Oct 16, 2026
picnixz requested a review from ZeroIntensity October 16, 2026 17:38

ZeroIntensity commented Oct 17, 2026 •
edited
Loading

Copy link
Copy Markdown
Member

Can I see the full benchmark scripts that were used? I'm not opposed to doing this, but the critical section API already has an optimization for exactly this kind of thing.

johng commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author
import ctypes
import sys
import timeit

NUMBER = 200_000
REPEAT = 7

ci = ctypes.c_int()
cp = ctypes.c_char_p()
pt = ctypes.pointer(ctypes.c_int(1))
val = ctypes.c_int(2)


class S(ctypes.Structure):
    _fields_ = [("p", ctypes.c_char_p), ("i", ctypes.c_int)]


st = S()

CASES = [
    ('c_int().value = 1', 'ci.value = 1'),
    ('c_char_p().value = b"x"', 'cp.value = b"x"'),
    ('pointer.contents = c_int(2)', 'pt.contents = val'),
    ('Structure.i = 1', 'st.i = 1'),
    ('Structure.p = b"x"', 'st.p = b"x"'),
]

label = sys.argv[1] if len(sys.argv) > 1 else ''
print(f"== {label} {sys.version.split()[0]} gil={sys._is_gil_enabled()}")
for name, stmt in CASES:
    t = min(timeit.repeat(stmt, globals=globals(), number=NUMBER, repeat=REPEAT))
    print(f"{name:30s} {t / NUMBER * 1e9:6.1f} ns")

Checking the code you referenced seems like in a critical section we still do a CAS in PyMutex_LockFast before falling back to the optimisation you mentioned. Note running on MacOS I believe it pays a higher CAS however believe you will still see a difference.

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting review type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants


Back | FazBrowse Home | New Git URL