| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
…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)
|
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. |
Sorry, something went wrong.
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. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
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.