From 71b5afc173d693ad2635966c8788d5853296a9fa Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Thu, 21 Nov 2024 17:47:03 -0800 Subject: [PATCH 1/5] gh-115999: Specialize `LOAD_SUPER_ATTR` in free-threaded builds Use existing helpers to atomically modify the bytecode. Add unit tests to ensure specializing is happening as expected. Add test_specialize.py that can be used with ThreadSanitizer to detect data races. --- Lib/test/test_opcache.py | 41 ++++++++++++++++++++++++++++++++++++++ Python/bytecodes.c | 4 ++-- Python/generated_cases.c.h | 4 ++-- Python/specialize.c | 19 +++++------------- 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py index ee7fd178b1c02e..c20fb15588c70f 100644 --- a/Lib/test/test_opcache.py +++ b/Lib/test/test_opcache.py @@ -1272,6 +1272,47 @@ def g(): self.assert_specialized(g, "CONTAINS_OP_SET") self.assert_no_opcode(g, "CONTAINS_OP") + @cpython_only + @requires_specialization_ft + def test_load_super_attr(self): + """Ensure that LOAD_SUPER_ATTR is specialized as expected.""" + + class A: + def __init__(self): + meth = super().__init__ + super().__init__() + + for _ in range(100): + A() + + self.assert_specialized(A.__init__, "LOAD_SUPER_ATTR_ATTR") + self.assert_specialized(A.__init__, "LOAD_SUPER_ATTR_METHOD") + self.assert_no_opcode(A.__init__, "LOAD_SUPER_ATTR") + + # Temporarily replace super() with something else. + real_super = super + + def fake_super(): + def init(self): + pass + + return init + + # Force unspecialize + globals()['super'] = fake_super + try: + # Should be unspecialized after enough calls. + for _ in range(100): + A() + finally: + globals()['super'] = real_super + + # Ensure the specialized instructions are not present + self.assert_no_opcode(A.__init__, "LOAD_SUPER_ATTR_ATTR") + self.assert_no_opcode(A.__init__, "LOAD_SUPER_ATTR_METHOD") + + if 0: + dis.dis(A.__init__, adaptive=True) if __name__ == "__main__": diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 71b1dc05fc390d..257c06e796d123 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -1932,7 +1932,7 @@ dummy_func( }; specializing op(_SPECIALIZE_LOAD_SUPER_ATTR, (counter/1, global_super_st, class_st, unused -- global_super_st, class_st, unused)) { - #if ENABLE_SPECIALIZATION + #if ENABLE_SPECIALIZATION_FT int load_method = oparg & 1; if (ADAPTIVE_COUNTER_TRIGGERS(counter)) { next_instr = this_instr; @@ -1941,7 +1941,7 @@ dummy_func( } OPCODE_DEFERRED_INC(LOAD_SUPER_ATTR); ADVANCE_ADAPTIVE_COUNTER(this_instr[1].counter); - #endif /* ENABLE_SPECIALIZATION */ + #endif /* ENABLE_SPECIALIZATION_FT */ } tier1 op(_LOAD_SUPER_ATTR, (global_super_st, class_st, self_st -- attr, null if (oparg & 1))) { diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 8896229bbf3874..2c29004f76dddf 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -6330,7 +6330,7 @@ global_super_st = stack_pointer[-3]; uint16_t counter = read_u16(&this_instr[1].cache); (void)counter; - #if ENABLE_SPECIALIZATION + #if ENABLE_SPECIALIZATION_FT int load_method = oparg & 1; if (ADAPTIVE_COUNTER_TRIGGERS(counter)) { next_instr = this_instr; @@ -6341,7 +6341,7 @@ } OPCODE_DEFERRED_INC(LOAD_SUPER_ATTR); ADVANCE_ADAPTIVE_COUNTER(this_instr[1].counter); - #endif /* ENABLE_SPECIALIZATION */ + #endif /* ENABLE_SPECIALIZATION_FT */ } // _LOAD_SUPER_ATTR { diff --git a/Python/specialize.c b/Python/specialize.c index af37e241965b48..b8ce2321d214e3 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -791,9 +791,8 @@ _Py_Specialize_LoadSuperAttr(_PyStackRef global_super_st, _PyStackRef cls_st, _P PyObject *global_super = PyStackRef_AsPyObjectBorrow(global_super_st); PyObject *cls = PyStackRef_AsPyObjectBorrow(cls_st); - assert(ENABLE_SPECIALIZATION); + assert(ENABLE_SPECIALIZATION_FT); assert(_PyOpcode_Caches[LOAD_SUPER_ATTR] == INLINE_CACHE_ENTRIES_LOAD_SUPER_ATTR); - _PySuperAttrCache *cache = (_PySuperAttrCache *)(instr + 1); if (global_super != (PyObject *)&PySuper_Type) { SPECIALIZATION_FAIL(LOAD_SUPER_ATTR, SPEC_FAIL_SUPER_SHADOWED); goto fail; @@ -802,19 +801,11 @@ _Py_Specialize_LoadSuperAttr(_PyStackRef global_super_st, _PyStackRef cls_st, _P SPECIALIZATION_FAIL(LOAD_SUPER_ATTR, SPEC_FAIL_SUPER_BAD_CLASS); goto fail; } - instr->op.code = load_method ? LOAD_SUPER_ATTR_METHOD : LOAD_SUPER_ATTR_ATTR; - goto success; - -fail: - STAT_INC(LOAD_SUPER_ATTR, failure); - assert(!PyErr_Occurred()); - instr->op.code = LOAD_SUPER_ATTR; - cache->counter = adaptive_counter_backoff(cache->counter); + uint8_t load_code = load_method ? LOAD_SUPER_ATTR_METHOD : LOAD_SUPER_ATTR_ATTR; + specialize(instr, load_code); return; -success: - STAT_INC(LOAD_SUPER_ATTR, success); - assert(!PyErr_Occurred()); - cache->counter = adaptive_counter_cooldown(); +fail: + unspecialize(instr); } typedef enum { From a515a0d99cdc9f305d937e75234dd42e6a6bfd09 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Mon, 25 Nov 2024 11:22:18 -0800 Subject: [PATCH 2/5] Add type lock for _PySuper_Lookup(). --- Include/internal/pycore_typeobject.h | 7 +++++-- Objects/typeobject.c | 26 ++++++++++++++++++++++++-- Python/bytecodes.c | 7 +++---- Python/ceval.c | 1 - Python/executor_cases.c.h | 5 ++--- Python/generated_cases.c.h | 5 ++--- 6 files changed, 36 insertions(+), 15 deletions(-) diff --git a/Include/internal/pycore_typeobject.h b/Include/internal/pycore_typeobject.h index 5debdd68fe94ca..fbf2f276683396 100644 --- a/Include/internal/pycore_typeobject.h +++ b/Include/internal/pycore_typeobject.h @@ -247,8 +247,11 @@ extern PyObject* _Py_slot_tp_getattr_hook(PyObject *self, PyObject *name); extern PyTypeObject _PyBufferWrapper_Type; -PyAPI_FUNC(PyObject*) _PySuper_Lookup(PyTypeObject *su_type, PyObject *su_obj, - PyObject *name, int *meth_found); +PyAPI_FUNC(PyObject*) _PySuper_LookupAttr(PyTypeObject *su_type, PyObject *su_obj, + PyObject *name); + +PyAPI_FUNC(PyObject*) _PySuper_LookupMethod(PyTypeObject *su_type, PyObject *su_obj, + PyObject *name, int *method_found); extern PyObject* _PyType_GetFullyQualifiedName(PyTypeObject *type, char sep); diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 840d004d3d98c7..d1bf0de2077d88 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -11600,8 +11600,8 @@ supercheck(PyTypeObject *type, PyObject *obj) return NULL; } -PyObject * -_PySuper_Lookup(PyTypeObject *su_type, PyObject *su_obj, PyObject *name, int *method) +static PyObject * +super_lookup_lock_held(PyTypeObject *su_type, PyObject *su_obj, PyObject *name, int *method) { PyTypeObject *su_obj_type = supercheck(su_type, su_obj); if (su_obj_type == NULL) { @@ -11612,6 +11612,28 @@ _PySuper_Lookup(PyTypeObject *su_type, PyObject *su_obj, PyObject *name, int *me return res; } +PyObject * +_PySuper_LookupAttr(PyTypeObject *su_type, PyObject *su_obj, PyObject *name) +{ + PyObject *res; + BEGIN_TYPE_LOCK(); + res = super_lookup_lock_held(su_type, su_obj, name, NULL); + END_TYPE_LOCK(); + return res; +} + +PyObject * +_PySuper_LookupMethod(PyTypeObject *su_type, PyObject *su_obj, PyObject *name, int *method_found) +{ + PyObject *res; + BEGIN_TYPE_LOCK(); + *method_found = 0; + res = super_lookup_lock_held(su_type, su_obj, name, + Py_TYPE(su_obj)->tp_getattro == PyObject_GenericGetAttr ? method_found : NULL); + END_TYPE_LOCK() + return res; +} + static PyObject * super_descr_get(PyObject *self, PyObject *obj, PyObject *type) { diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 257c06e796d123..ecd782ef142b12 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -29,7 +29,7 @@ #include "pycore_setobject.h" // _PySet_NextEntry() #include "pycore_sliceobject.h" // _PyBuildSlice_ConsumeRefs #include "pycore_tuple.h" // _PyTuple_ITEMS() -#include "pycore_typeobject.h" // _PySuper_Lookup() +#include "pycore_typeobject.h" // _PySuper_LookupAttr() _PySuper_LookupMethod() #include "pycore_dict.h" #include "dictobject.h" @@ -2001,7 +2001,7 @@ dummy_func( DEOPT_IF(!PyType_Check(class)); STAT_INC(LOAD_SUPER_ATTR, hit); PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 2); - PyObject *attr = _PySuper_Lookup((PyTypeObject *)class, self, name, NULL); + PyObject *attr = _PySuper_LookupAttr((PyTypeObject *)class, self, name); DECREF_INPUTS(); ERROR_IF(attr == NULL, error); attr_st = PyStackRef_FromPyObjectSteal(attr); @@ -2019,8 +2019,7 @@ dummy_func( PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 2); PyTypeObject *cls = (PyTypeObject *)class; int method_found = 0; - PyObject *attr_o = _PySuper_Lookup(cls, self, name, - Py_TYPE(self)->tp_getattro == PyObject_GenericGetAttr ? &method_found : NULL); + PyObject *attr_o = _PySuper_LookupMethod(cls, self, name, &method_found); PyStackRef_CLOSE(global_super_st); PyStackRef_CLOSE(class_st); if (attr_o == NULL) { diff --git a/Python/ceval.c b/Python/ceval.c index 2a3938572c1569..a192f9290ef308 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -28,7 +28,6 @@ #include "pycore_setobject.h" // _PySet_Update() #include "pycore_sliceobject.h" // _PyBuildSlice_ConsumeRefs #include "pycore_tuple.h" // _PyTuple_ITEMS() -#include "pycore_typeobject.h" // _PySuper_Lookup() #include "pycore_uop_ids.h" // Uops #include "pycore_pyerrors.h" diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 8acf7a43c08fca..73e01cf5b5b3e4 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -2424,7 +2424,7 @@ STAT_INC(LOAD_SUPER_ATTR, hit); PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 2); _PyFrame_SetStackPointer(frame, stack_pointer); - PyObject *attr = _PySuper_Lookup((PyTypeObject *)class, self, name, NULL); + PyObject *attr = _PySuper_LookupAttr((PyTypeObject *)class, self, name); stack_pointer = _PyFrame_GetStackPointer(frame); PyStackRef_CLOSE(global_super_st); PyStackRef_CLOSE(class_st); @@ -2464,8 +2464,7 @@ PyTypeObject *cls = (PyTypeObject *)class; int method_found = 0; _PyFrame_SetStackPointer(frame, stack_pointer); - PyObject *attr_o = _PySuper_Lookup(cls, self, name, - Py_TYPE(self)->tp_getattro == PyObject_GenericGetAttr ? &method_found : NULL); + PyObject *attr_o = _PySuper_LookupMethod(cls, self, name, &method_found); stack_pointer = _PyFrame_GetStackPointer(frame); PyStackRef_CLOSE(global_super_st); PyStackRef_CLOSE(class_st); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 2c29004f76dddf..d23b253be48790 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -6433,7 +6433,7 @@ STAT_INC(LOAD_SUPER_ATTR, hit); PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 2); _PyFrame_SetStackPointer(frame, stack_pointer); - PyObject *attr = _PySuper_Lookup((PyTypeObject *)class, self, name, NULL); + PyObject *attr = _PySuper_LookupAttr((PyTypeObject *)class, self, name); stack_pointer = _PyFrame_GetStackPointer(frame); PyStackRef_CLOSE(global_super_st); PyStackRef_CLOSE(class_st); @@ -6471,8 +6471,7 @@ PyTypeObject *cls = (PyTypeObject *)class; int method_found = 0; _PyFrame_SetStackPointer(frame, stack_pointer); - PyObject *attr_o = _PySuper_Lookup(cls, self, name, - Py_TYPE(self)->tp_getattro == PyObject_GenericGetAttr ? &method_found : NULL); + PyObject *attr_o = _PySuper_LookupMethod(cls, self, name, &method_found); stack_pointer = _PyFrame_GetStackPointer(frame); PyStackRef_CLOSE(global_super_st); PyStackRef_CLOSE(class_st); From 1f5f2a2ba36139c047a7a6c8105c128fdfbd2a66 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 26 Nov 2024 11:41:40 -0800 Subject: [PATCH 3/5] Revert change to _PySuper_Lookup(). It seems `do_super_lookup()` and `supercheck()` are safe to call and so `_PySuper_Lookup()` doesn't need a lock for the type object. This avoids a performance regression in FT builds. --- Include/internal/pycore_typeobject.h | 7 ++----- Objects/typeobject.c | 26 ++------------------------ Python/bytecodes.c | 7 ++++--- Python/executor_cases.c.h | 5 +++-- Python/generated_cases.c.h | 5 +++-- 5 files changed, 14 insertions(+), 36 deletions(-) diff --git a/Include/internal/pycore_typeobject.h b/Include/internal/pycore_typeobject.h index fbf2f276683396..5debdd68fe94ca 100644 --- a/Include/internal/pycore_typeobject.h +++ b/Include/internal/pycore_typeobject.h @@ -247,11 +247,8 @@ extern PyObject* _Py_slot_tp_getattr_hook(PyObject *self, PyObject *name); extern PyTypeObject _PyBufferWrapper_Type; -PyAPI_FUNC(PyObject*) _PySuper_LookupAttr(PyTypeObject *su_type, PyObject *su_obj, - PyObject *name); - -PyAPI_FUNC(PyObject*) _PySuper_LookupMethod(PyTypeObject *su_type, PyObject *su_obj, - PyObject *name, int *method_found); +PyAPI_FUNC(PyObject*) _PySuper_Lookup(PyTypeObject *su_type, PyObject *su_obj, + PyObject *name, int *meth_found); extern PyObject* _PyType_GetFullyQualifiedName(PyTypeObject *type, char sep); diff --git a/Objects/typeobject.c b/Objects/typeobject.c index d1bf0de2077d88..840d004d3d98c7 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -11600,8 +11600,8 @@ supercheck(PyTypeObject *type, PyObject *obj) return NULL; } -static PyObject * -super_lookup_lock_held(PyTypeObject *su_type, PyObject *su_obj, PyObject *name, int *method) +PyObject * +_PySuper_Lookup(PyTypeObject *su_type, PyObject *su_obj, PyObject *name, int *method) { PyTypeObject *su_obj_type = supercheck(su_type, su_obj); if (su_obj_type == NULL) { @@ -11612,28 +11612,6 @@ super_lookup_lock_held(PyTypeObject *su_type, PyObject *su_obj, PyObject *name, return res; } -PyObject * -_PySuper_LookupAttr(PyTypeObject *su_type, PyObject *su_obj, PyObject *name) -{ - PyObject *res; - BEGIN_TYPE_LOCK(); - res = super_lookup_lock_held(su_type, su_obj, name, NULL); - END_TYPE_LOCK(); - return res; -} - -PyObject * -_PySuper_LookupMethod(PyTypeObject *su_type, PyObject *su_obj, PyObject *name, int *method_found) -{ - PyObject *res; - BEGIN_TYPE_LOCK(); - *method_found = 0; - res = super_lookup_lock_held(su_type, su_obj, name, - Py_TYPE(su_obj)->tp_getattro == PyObject_GenericGetAttr ? method_found : NULL); - END_TYPE_LOCK() - return res; -} - static PyObject * super_descr_get(PyObject *self, PyObject *obj, PyObject *type) { diff --git a/Python/bytecodes.c b/Python/bytecodes.c index ecd782ef142b12..257c06e796d123 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -29,7 +29,7 @@ #include "pycore_setobject.h" // _PySet_NextEntry() #include "pycore_sliceobject.h" // _PyBuildSlice_ConsumeRefs #include "pycore_tuple.h" // _PyTuple_ITEMS() -#include "pycore_typeobject.h" // _PySuper_LookupAttr() _PySuper_LookupMethod() +#include "pycore_typeobject.h" // _PySuper_Lookup() #include "pycore_dict.h" #include "dictobject.h" @@ -2001,7 +2001,7 @@ dummy_func( DEOPT_IF(!PyType_Check(class)); STAT_INC(LOAD_SUPER_ATTR, hit); PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 2); - PyObject *attr = _PySuper_LookupAttr((PyTypeObject *)class, self, name); + PyObject *attr = _PySuper_Lookup((PyTypeObject *)class, self, name, NULL); DECREF_INPUTS(); ERROR_IF(attr == NULL, error); attr_st = PyStackRef_FromPyObjectSteal(attr); @@ -2019,7 +2019,8 @@ dummy_func( PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 2); PyTypeObject *cls = (PyTypeObject *)class; int method_found = 0; - PyObject *attr_o = _PySuper_LookupMethod(cls, self, name, &method_found); + PyObject *attr_o = _PySuper_Lookup(cls, self, name, + Py_TYPE(self)->tp_getattro == PyObject_GenericGetAttr ? &method_found : NULL); PyStackRef_CLOSE(global_super_st); PyStackRef_CLOSE(class_st); if (attr_o == NULL) { diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 73e01cf5b5b3e4..8acf7a43c08fca 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -2424,7 +2424,7 @@ STAT_INC(LOAD_SUPER_ATTR, hit); PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 2); _PyFrame_SetStackPointer(frame, stack_pointer); - PyObject *attr = _PySuper_LookupAttr((PyTypeObject *)class, self, name); + PyObject *attr = _PySuper_Lookup((PyTypeObject *)class, self, name, NULL); stack_pointer = _PyFrame_GetStackPointer(frame); PyStackRef_CLOSE(global_super_st); PyStackRef_CLOSE(class_st); @@ -2464,7 +2464,8 @@ PyTypeObject *cls = (PyTypeObject *)class; int method_found = 0; _PyFrame_SetStackPointer(frame, stack_pointer); - PyObject *attr_o = _PySuper_LookupMethod(cls, self, name, &method_found); + PyObject *attr_o = _PySuper_Lookup(cls, self, name, + Py_TYPE(self)->tp_getattro == PyObject_GenericGetAttr ? &method_found : NULL); stack_pointer = _PyFrame_GetStackPointer(frame); PyStackRef_CLOSE(global_super_st); PyStackRef_CLOSE(class_st); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index d23b253be48790..2c29004f76dddf 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -6433,7 +6433,7 @@ STAT_INC(LOAD_SUPER_ATTR, hit); PyObject *name = GETITEM(FRAME_CO_NAMES, oparg >> 2); _PyFrame_SetStackPointer(frame, stack_pointer); - PyObject *attr = _PySuper_LookupAttr((PyTypeObject *)class, self, name); + PyObject *attr = _PySuper_Lookup((PyTypeObject *)class, self, name, NULL); stack_pointer = _PyFrame_GetStackPointer(frame); PyStackRef_CLOSE(global_super_st); PyStackRef_CLOSE(class_st); @@ -6471,7 +6471,8 @@ PyTypeObject *cls = (PyTypeObject *)class; int method_found = 0; _PyFrame_SetStackPointer(frame, stack_pointer); - PyObject *attr_o = _PySuper_LookupMethod(cls, self, name, &method_found); + PyObject *attr_o = _PySuper_Lookup(cls, self, name, + Py_TYPE(self)->tp_getattro == PyObject_GenericGetAttr ? &method_found : NULL); stack_pointer = _PyFrame_GetStackPointer(frame); PyStackRef_CLOSE(global_super_st); PyStackRef_CLOSE(class_st); From ab3af140a88b7929c266406bfedd1d6339d23bc2 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 26 Nov 2024 11:43:21 -0800 Subject: [PATCH 4/5] Fix thread safety issue with cell_set_contents(). --- Objects/cellobject.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Objects/cellobject.c b/Objects/cellobject.c index 590c8a80857699..4ab9083af5e300 100644 --- a/Objects/cellobject.c +++ b/Objects/cellobject.c @@ -145,8 +145,9 @@ cell_get_contents(PyObject *self, void *closure) static int cell_set_contents(PyObject *self, PyObject *obj, void *Py_UNUSED(ignored)) { - PyCellObject *op = _PyCell_CAST(self); - Py_XSETREF(op->ob_ref, Py_XNewRef(obj)); + PyCellObject *cell = _PyCell_CAST(self); + Py_XINCREF(obj); + PyCell_SetTakeRef((PyCellObject *)cell, obj); return 0; } From 93242ba8589d155040fa85b1cde895fcc013d3fd Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 27 Nov 2024 15:25:43 -0800 Subject: [PATCH 5/5] Remove stray whitespace from github merge. --- Lib/test/test_opcache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py index d7e3e82a6257d6..0460aa40bd4e5a 100644 --- a/Lib/test/test_opcache.py +++ b/Lib/test/test_opcache.py @@ -1248,7 +1248,7 @@ def g(): g() self.assert_specialized(g, "BINARY_OP_ADD_UNICODE") self.assert_no_opcode(g, "BINARY_OP") - + @cpython_only @requires_specialization_ft def test_load_super_attr(self):