-
-
Notifications
You must be signed in to change notification settings - Fork 31.6k
gh-127065: Make methodcaller thread-safe and re-entrant #127245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few comments:
- I don't think
_methodcaller_initialize_vectorcall
is thread safe without the GIL. Lazily initializing arguments complicates the code and thread-safety -- I don't think it's worth it. - Generally, I think it's better to support some fixed max number of arguments (like 8), and stack allocate the temporary array. (i.e.,
PyObject **tmp_args[MAX_ARGS];
). If themethodcaller
needs more arguments than that, just don't set thevectorcall
field and let it fall back to thetp_call
.
Modules/_operator.c
Outdated
(PyTuple_GET_SIZE(mc->xargs)) | PY_VECTORCALL_ARGUMENTS_OFFSET, | ||
mc->vectorcall_kwnames); | ||
|
||
PyMem_Free(tmp_args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PyMem_Free
is after the return
statement
PyObject **vectorcall_args; /* Borrowed references */ | ||
PyObject *vectorcall_kwnames; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ownership here seems a bit complicated and I think it can be simplified. As I understand it, vectorcall_kwnames
only exists to ensure that some entries in vectorcall_args
stay alive.
Instead, I'd suggest:
- Make
PyObject *vectorcall_args
a tuple (that holds strong references to its contents as usual) - Get rid of
vectorcall_kwnames
- Use
_PyTuple_ITEMS
for fast access to the contents of the tuple (formemcpy()
) - Visit
vectorcall_args
inmethodcaller_traverse
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The vectorcall_kwnames
is needed as an argument for PyObject_VectorcallMethod
in methodcaller_vectorcall
(https://github.com/python/cpython/blob/main/Modules/_operator.c#L1666), so we cannot get rid of it.
The ownership is not too hard I think: the objects in vectorcall_args
have references borrowed from either mc->args
or (the keys from) mc->kwds
. I added a comment to clarify this.
Making the vectorcall_args
a tuple is still an option though. It requires a bit more memory and a tiny bit of computation in the initialization. It would be the C equivalent of vectorcall_args = args + tuple(kwds)
. I'll work it out in a branch to see how it compares
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, in that case don't worry about it unless you prefer it as a tuple.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The diff between the two approaches is this:
eendebakpt/cpython@methodcaller_ft...eendebakpt:cpython:methodcaller_ft_v2
What is nice about making vectorcall_args
a tuple is that if there are no keyword arguments, we can reuse mc->args
. It does require more operations in the construction though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think either approach is fine! My guess is that the vast majority of uses of methodcaller()
do not use keyword arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Running benchmarks shows what is to be expected from the implementations: using a tuple for vectorcall_args
is a bit slower in initializing, except when there are no keyword arguments (since then we reuse the arg
tuple). Differences are small though.
Since using a tuple leads to cleaner code and the majority of uses is without keywords I slightly prefer the tuple approach. I will open a new PR for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this looks good to me. A few minor formatting suggestions below.
Modules/_operator.c
Outdated
methodcaller_vectorcall( | ||
methodcallerobject *mc, PyObject *const *args, size_t nargsf, PyObject* kwnames) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the more common formatting looks like:
methodcaller_vectorcall(methodcallerobject *mc, PyObject *const *args,
size_t nargsf, PyObject *kwnames)
Modules/_operator.c
Outdated
assert(1 + number_of_arguments <= _METHODCALLER_MAX_ARGS); | ||
memcpy(tmp_args + 1, mc->vectorcall_args, sizeof(PyObject *) * number_of_arguments); | ||
|
||
PyObject *result = PyObject_VectorcallMethod( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Up to you, but I'd write this without the temporary result
variable like:
return PyObject_VectorcallMethod(...);
Modules/_operator.c
Outdated
|
||
return result; | ||
} | ||
|
||
static int _methodcaller_initialize_vectorcall(methodcallerobject* mc) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static int _methodcaller_initialize_vectorcall(methodcallerobject* mc) | |
static int | |
_methodcaller_initialize_vectorcall(methodcallerobject *mc) |
Co-authored-by: Sam Gross <colesbury@gmail.com>
Co-authored-by: Sam Gross <colesbury@gmail.com>
Closing in favor of #127746 |
The function
operator.methodcaller
is not thread-safe since the additional of the vectorcall method in ##107201. In the free-threading build the issue is easy to trigger, for the normal build harder.We could either remove the vectorcall implementation, or adapt the vectorcall implementation to be thread safe (for both the normal and free-threading build). In this PR we make the
methodcaller
safe bytp_call
for more than 8 argumentsBenchmark results:
(old machine, no PGO, comparing this PR with the same PR but the vectorcall disabled)
Benchmark script
methodcaller
is not thread-safe (or re-entrant) #127065