Skip to content

gh-159136: Avoid stop-the-world when setting function name attributes - #159269

Open
nascheme wants to merge 3 commits into
python:mainfrom
nascheme:gh-159136-func-attrs
Open

nascheme wants to merge 3 commits into
python:mainfrom
nascheme:gh-159136-func-attrs

Conversation

@nascheme

@nascheme nascheme commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Setting __name__, __qualname__, __doc__ or __module__ on a function stopped the world in the free-threaded build since gh-154826 and gh-154851, making functools.wraps() orders of magnitude slower with multiple threads.

Use the per-object critical section and _PyObject_XSetRefDelayed() instead, as gen_set_name() already does. Use acquire loads in the readers and hold strong references across error reporting, tracing, and eval-frame recording that may execute Python or detach the thread. The __code__, __defaults__, __kwdefaults__ and __closure__ setters still stop the world to clear func_version.

Document synchronization requirements for direct field access and the lifetime of borrowed function attributes.

Benchmark results (release free-threaded build, each thread creates its own function, 5000 iterations, time in seconds):

workload build 1t 2t 4t 8t
set __name__ main 0.002 4.95 4.86 5.99
set __name__ this PR 0.001 0.001 0.001 0.002
set __name__ + __doc__ main 0.004 7.70 10.02 11.47
set __name__ + __doc__ this PR 0.002 0.002 0.001 0.002
functools.wraps main 0.014 18.53 22.58 28.79
functools.wraps this PR 0.010 0.010 0.010 0.014

Note: this PR was created with assistance from LLM agents (Claude Fable and GPT 6.1 Sol).

…ibutes

Setting __name__, __qualname__, __doc__ or __module__ on a function
stopped the world in the free-threaded build since pythongh-154826 and
pythongh-154851, making functools.wraps() orders of magnitude slower with
multiple threads.

Use the per-object critical section and _PyObject_XSetRefDelayed()
instead, as gen_set_name() already does.  Use acquire loads in the
readers and hold strong references across error reporting, tracing, and
eval-frame recording that may execute Python or detach the thread.  The
__code__, __defaults__, __kwdefaults__ and __closure__ setters still
stop the world to clear func_version.

Document synchronization requirements for direct field access and the
lifetime of borrowed function attributes.
@read-the-docs-community

read-the-docs-community Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Documentation build overview

📚 cpython-previews | 🛠️ Build #35106100 | 📁 Comparing 7837be5 against main (0768eed)

  🔍 Preview build  

6 files changed · ± 6 modified

± Modified

@nascheme
nascheme marked this pull request as ready for review October 13, 2026 16:31
Comment on lines +101 to +106
#ifdef Py_GIL_DISABLED
return (PyObject *)_Py_atomic_load_ptr_acquire(
&_PyFunction_CAST(func)->func_module);
#else
return _PyFunction_CAST(func)->func_module;
#endif

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like this work? It's a little shorter and should have the same effect. (Guessing there's a macro-expansion related reason you didn't use FT_ATOMIC_LOAD_PTR_ACQUIRE here because it's used liberally elsewhere in the PR.)

Suggested change
#ifdef Py_GIL_DISABLED
return (PyObject *)_Py_atomic_load_ptr_acquire(
&_PyFunction_CAST(func)->func_module);
#else
return _PyFunction_CAST(func)->func_module;
#endif
PyFunctionObject *fn = _PyFunction_CAST(func);
return FT_ATOMIC_LOAD_PTR_ACQUIRE(fn->func_module);

Comment thread Objects/funcobject.c
Comment on lines +615 to +616
Py_XSETREF(func->func_annotations, annotations);
Py_CLEAR(func->func_annotate);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to use the same pattern as before, otherwise I think it's possible to end up with a race like:

  1. Thread A stores the new annotations dictionary.
  2. Decrefing the old dictionary runs a value’s __del__, which blocks and suspends the critical section.
  3. Thread B sets f.__annotate__ = callback, installing the callback and clearing func_annotations.
  4. Thread A resumes, reacquires the function lock, and executes Py_CLEAR(func->func_annotate), deleting B’s callback.

Something like this would work:

Suggested change
Py_XSETREF(func->func_annotations, annotations);
Py_CLEAR(func->func_annotate);
Py_BEGIN_CRITICAL_SECTION(func);
PyObject *old_annotations = func->func_annotations;
PyObject *old_annotate = func->func_annotate;
func->func_annotations = annotations;
func->func_annotate = NULL;
Py_END_CRITICAL_SECTION();
Py_XDECREF(old_annotations);
Py_XDECREF(old_annotate);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separately, codex tells me that we need to update PyFunction_GetAnnotations in light of these changes. There are a lot of non-atomic reads that were previously fine b/c of the STW pause in PyFunction_SetAnnotations.

@bedevere-app

bedevere-app Bot commented Oct 13, 2026

Copy link
Copy Markdown

When you're done making the requested changes, leave the comment: I have made the requested changes; please review again.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants