Skip to content

Commit 70b0a53

Browse files
committed
gh-145779: Improve classmethod/staticmethod scaling in free-threaded build
1 parent 7a1da45 commit 70b0a53

File tree

10 files changed

+193
-103
lines changed

10 files changed

+193
-103
lines changed

Include/internal/pycore_call.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ PyAPI_FUNC(PyObject*) _PyObject_CallMethod(
6565
const char *format, ...);
6666

6767

68+
extern PyObject *_PyObject_VectorcallPrepend(
69+
PyThreadState *tstate,
70+
PyObject *callable,
71+
PyObject *arg,
72+
PyObject *const *args,
73+
size_t nargsf,
74+
PyObject *kwnames);
75+
6876
/* === Vectorcall protocol (PEP 590) ============================= */
6977

7078
// Call callable using tp_call. Arguments are like PyObject_Vectorcall(),

Include/internal/pycore_function.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ static inline PyObject* _PyFunction_GET_BUILTINS(PyObject *func) {
4646
#define _PyFunction_GET_BUILTINS(func) _PyFunction_GET_BUILTINS(_PyObject_CAST(func))
4747

4848

49+
/* Get the callable wrapped by a classmethod.
50+
Returns a borrowed reference.
51+
The caller must ensure 'cm' is a classmethod object. */
52+
extern PyObject *_PyClassMethod_GetFunc(PyObject *cm);
53+
4954
/* Get the callable wrapped by a staticmethod.
5055
Returns a borrowed reference.
5156
The caller must ensure 'sm' is a staticmethod object. */

Include/internal/pycore_object.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -895,7 +895,7 @@ extern PyObject *_PyType_LookupRefAndVersion(PyTypeObject *, PyObject *,
895895
extern unsigned int
896896
_PyType_LookupStackRefAndVersion(PyTypeObject *type, PyObject *name, _PyStackRef *out);
897897

898-
PyAPI_FUNC(int) _PyObject_GetMethodStackRef(PyThreadState *ts, PyObject *obj,
898+
extern int _PyObject_GetMethodStackRef(PyThreadState *ts, _PyStackRef *self,
899899
PyObject *name, _PyStackRef *method);
900900

901901
// Like PyObject_GetAttr but returns a _PyStackRef. For types, this can
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve scaling of :func:`classmethod` and :func:`staticmethod` calls in
2+
the free-threaded build by avoiding the descriptor ``__get__`` call.

Objects/call.c

Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,60 @@ object_vacall(PyThreadState *tstate, PyObject *base,
828828
return result;
829829
}
830830

831+
PyObject *
832+
_PyObject_VectorcallPrepend(PyThreadState *tstate, PyObject *callable,
833+
PyObject *arg, PyObject *const *args,
834+
size_t nargsf, PyObject *kwnames)
835+
{
836+
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
837+
assert(nargs == 0 || args[nargs-1]);
838+
839+
PyObject *result;
840+
if (nargsf & PY_VECTORCALL_ARGUMENTS_OFFSET) {
841+
/* PY_VECTORCALL_ARGUMENTS_OFFSET is set, so we are allowed to mutate the vector */
842+
PyObject **newargs = (PyObject**)args - 1;
843+
nargs += 1;
844+
PyObject *tmp = newargs[0];
845+
newargs[0] = arg;
846+
assert(newargs[nargs-1]);
847+
result = _PyObject_VectorcallTstate(tstate, callable, newargs,
848+
nargs, kwnames);
849+
newargs[0] = tmp;
850+
}
851+
else {
852+
Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames);
853+
Py_ssize_t totalargs = nargs + nkwargs;
854+
if (totalargs == 0) {
855+
return _PyObject_VectorcallTstate(tstate, callable, &arg, 1, NULL);
856+
}
857+
858+
PyObject *newargs_stack[_PY_FASTCALL_SMALL_STACK];
859+
PyObject **newargs;
860+
if (totalargs <= (Py_ssize_t)Py_ARRAY_LENGTH(newargs_stack) - 1) {
861+
newargs = newargs_stack;
862+
}
863+
else {
864+
newargs = PyMem_Malloc((totalargs+1) * sizeof(PyObject *));
865+
if (newargs == NULL) {
866+
_PyErr_NoMemory(tstate);
867+
return NULL;
868+
}
869+
}
870+
/* use borrowed references */
871+
newargs[0] = arg;
872+
/* bpo-37138: since totalargs > 0, it's impossible that args is NULL.
873+
* We need this, since calling memcpy() with a NULL pointer is
874+
* undefined behaviour. */
875+
assert(args != NULL);
876+
memcpy(newargs + 1, args, totalargs * sizeof(PyObject *));
877+
result = _PyObject_VectorcallTstate(tstate, callable,
878+
newargs, nargs+1, kwnames);
879+
if (newargs != newargs_stack) {
880+
PyMem_Free(newargs);
881+
}
882+
}
883+
return result;
884+
}
831885

832886
PyObject *
833887
PyObject_VectorcallMethod(PyObject *name, PyObject *const *args,
@@ -838,31 +892,44 @@ PyObject_VectorcallMethod(PyObject *name, PyObject *const *args,
838892
assert(PyVectorcall_NARGS(nargsf) >= 1);
839893

840894
PyThreadState *tstate = _PyThreadState_GET();
841-
_PyCStackRef method;
895+
_PyCStackRef self, method;
896+
_PyThreadState_PushCStackRef(tstate, &self);
842897
_PyThreadState_PushCStackRef(tstate, &method);
843898
/* Use args[0] as "self" argument */
844-
int unbound = _PyObject_GetMethodStackRef(tstate, args[0], name, &method.ref);
845-
if (PyStackRef_IsNull(method.ref)) {
899+
self.ref = PyStackRef_FromPyObjectBorrow(args[0]);
900+
int unbound = _PyObject_GetMethodStackRef(tstate, &self.ref, name, &method.ref);
901+
if (unbound < 0) {
846902
_PyThreadState_PopCStackRef(tstate, &method);
903+
_PyThreadState_PopCStackRef(tstate, &self);
847904
return NULL;
848905
}
906+
849907
PyObject *callable = PyStackRef_AsPyObjectBorrow(method.ref);
908+
PyObject *self_obj = PyStackRef_AsPyObjectBorrow(self.ref);
909+
PyObject *result;
850910

851-
if (unbound) {
911+
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_METHOD, callable);
912+
if (self_obj == NULL) {
913+
/* Skip "self". We can keep PY_VECTORCALL_ARGUMENTS_OFFSET since
914+
* args[-1] in the onward call is args[0] here. */
915+
result = _PyObject_VectorcallTstate(tstate, callable,
916+
args + 1, nargsf - 1, kwnames);
917+
}
918+
else if (self_obj == args[0]) {
852919
/* We must remove PY_VECTORCALL_ARGUMENTS_OFFSET since
853920
* that would be interpreted as allowing to change args[-1] */
854-
nargsf &= ~PY_VECTORCALL_ARGUMENTS_OFFSET;
921+
result = _PyObject_VectorcallTstate(tstate, callable, args,
922+
nargsf & ~PY_VECTORCALL_ARGUMENTS_OFFSET,
923+
kwnames);
855924
}
856925
else {
857-
/* Skip "self". We can keep PY_VECTORCALL_ARGUMENTS_OFFSET since
858-
* args[-1] in the onward call is args[0] here. */
859-
args++;
860-
nargsf--;
926+
/* classmethod: self_obj is the type, not args[0]. Replace
927+
* args[0] with self_obj and call the underlying callable. */
928+
result = _PyObject_VectorcallPrepend(tstate, callable, self_obj,
929+
args + 1, nargsf - 1, kwnames);
861930
}
862-
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_METHOD, callable);
863-
PyObject *result = _PyObject_VectorcallTstate(tstate, callable,
864-
args, nargsf, kwnames);
865931
_PyThreadState_PopCStackRef(tstate, &method);
932+
_PyThreadState_PopCStackRef(tstate, &self);
866933
return result;
867934
}
868935

@@ -875,22 +942,26 @@ PyObject_CallMethodObjArgs(PyObject *obj, PyObject *name, ...)
875942
return null_error(tstate);
876943
}
877944

878-
_PyCStackRef method;
945+
_PyCStackRef self, method;
946+
_PyThreadState_PushCStackRef(tstate, &self);
879947
_PyThreadState_PushCStackRef(tstate, &method);
880-
int is_method = _PyObject_GetMethodStackRef(tstate, obj, name, &method.ref);
881-
if (PyStackRef_IsNull(method.ref)) {
948+
self.ref = PyStackRef_FromPyObjectBorrow(obj);
949+
int res = _PyObject_GetMethodStackRef(tstate, &self.ref, name, &method.ref);
950+
if (res < 0) {
882951
_PyThreadState_PopCStackRef(tstate, &method);
952+
_PyThreadState_PopCStackRef(tstate, &self);
883953
return NULL;
884954
}
885955
PyObject *callable = PyStackRef_AsPyObjectBorrow(method.ref);
886-
obj = is_method ? obj : NULL;
956+
PyObject *self_obj = PyStackRef_AsPyObjectBorrow(self.ref);
887957

888958
va_list vargs;
889959
va_start(vargs, name);
890-
PyObject *result = object_vacall(tstate, obj, callable, vargs);
960+
PyObject *result = object_vacall(tstate, self_obj, callable, vargs);
891961
va_end(vargs);
892962

893963
_PyThreadState_PopCStackRef(tstate, &method);
964+
_PyThreadState_PopCStackRef(tstate, &self);
894965
return result;
895966
}
896967

Objects/classobject.c

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -52,54 +52,7 @@ method_vectorcall(PyObject *method, PyObject *const *args,
5252
PyThreadState *tstate = _PyThreadState_GET();
5353
PyObject *self = PyMethod_GET_SELF(method);
5454
PyObject *func = PyMethod_GET_FUNCTION(method);
55-
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
56-
assert(nargs == 0 || args[nargs-1]);
57-
58-
PyObject *result;
59-
if (nargsf & PY_VECTORCALL_ARGUMENTS_OFFSET) {
60-
/* PY_VECTORCALL_ARGUMENTS_OFFSET is set, so we are allowed to mutate the vector */
61-
PyObject **newargs = (PyObject**)args - 1;
62-
nargs += 1;
63-
PyObject *tmp = newargs[0];
64-
newargs[0] = self;
65-
assert(newargs[nargs-1]);
66-
result = _PyObject_VectorcallTstate(tstate, func, newargs,
67-
nargs, kwnames);
68-
newargs[0] = tmp;
69-
}
70-
else {
71-
Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames);
72-
Py_ssize_t totalargs = nargs + nkwargs;
73-
if (totalargs == 0) {
74-
return _PyObject_VectorcallTstate(tstate, func, &self, 1, NULL);
75-
}
76-
77-
PyObject *newargs_stack[_PY_FASTCALL_SMALL_STACK];
78-
PyObject **newargs;
79-
if (totalargs <= (Py_ssize_t)Py_ARRAY_LENGTH(newargs_stack) - 1) {
80-
newargs = newargs_stack;
81-
}
82-
else {
83-
newargs = PyMem_Malloc((totalargs+1) * sizeof(PyObject *));
84-
if (newargs == NULL) {
85-
_PyErr_NoMemory(tstate);
86-
return NULL;
87-
}
88-
}
89-
/* use borrowed references */
90-
newargs[0] = self;
91-
/* bpo-37138: since totalargs > 0, it's impossible that args is NULL.
92-
* We need this, since calling memcpy() with a NULL pointer is
93-
* undefined behaviour. */
94-
assert(args != NULL);
95-
memcpy(newargs + 1, args, totalargs * sizeof(PyObject *));
96-
result = _PyObject_VectorcallTstate(tstate, func,
97-
newargs, nargs+1, kwnames);
98-
if (newargs != newargs_stack) {
99-
PyMem_Free(newargs);
100-
}
101-
}
102-
return result;
55+
return _PyObject_VectorcallPrepend(tstate, func, self, args, nargsf, kwnames);
10356
}
10457

10558

Objects/funcobject.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,6 +1470,7 @@ cm_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
14701470
if (cm == NULL) {
14711471
return NULL;
14721472
}
1473+
PyUnstable_Object_EnableDeferredRefcount((PyObject *)cm);
14731474
if (cm_set_callable(cm, callable) < 0) {
14741475
Py_DECREF(cm);
14751476
return NULL;
@@ -1906,6 +1907,13 @@ PyStaticMethod_New(PyObject *callable)
19061907
return (PyObject *)sm;
19071908
}
19081909

1910+
PyObject *
1911+
_PyClassMethod_GetFunc(PyObject *self)
1912+
{
1913+
classmethod *cm = _PyClassMethod_CAST(self);
1914+
return cm->cm_callable;
1915+
}
1916+
19091917
PyObject *
19101918
_PyStaticMethod_GetFunc(PyObject *self)
19111919
{

0 commit comments

Comments
 (0)