Skip to content

Commit 7276ca2

Browse files
authoredAug 17, 2022
GH-93911: Specialize LOAD_ATTR for custom __getattribute__ (GH-93988)
1 parent 3651710 commit 7276ca2

File tree

11 files changed

+219
-86
lines changed

11 files changed

+219
-86
lines changed
 

‎Include/internal/pycore_code.h

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ struct callable_cache {
118118
PyObject *isinstance;
119119
PyObject *len;
120120
PyObject *list_append;
121+
PyObject *object__getattribute__;
121122
};
122123

123124
/* "Locals plus" for a code object is the set of locals + cell vars +

‎Include/internal/pycore_opcode.h

+12-12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Include/internal/pycore_typeobject.h

+3
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ extern void _PyStaticType_ClearWeakRefs(PyTypeObject *type);
7575
extern void _PyStaticType_Dealloc(PyTypeObject *type);
7676

7777

78+
PyObject *_Py_slot_tp_getattro(PyObject *self, PyObject *name);
79+
PyObject *_Py_slot_tp_getattr_hook(PyObject *self, PyObject *name);
80+
7881
#ifdef __cplusplus
7982
}
8083
#endif

‎Include/opcode.h

+30-29
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Lib/opcode.py

+1
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ def pseudo_op(name, op, real_ops):
338338
"LOAD_ATTR_ADAPTIVE",
339339
# These potentially push [NULL, bound method] onto the stack.
340340
"LOAD_ATTR_CLASS",
341+
"LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN",
341342
"LOAD_ATTR_INSTANCE_VALUE",
342343
"LOAD_ATTR_MODULE",
343344
"LOAD_ATTR_PROPERTY",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Specialize ``LOAD_ATTR`` for objects with custom ``__getattr__`` and ``__getattribute__``.

‎Objects/typeobject.c

+13-13
Original file line numberDiff line numberDiff line change
@@ -8102,17 +8102,17 @@ slot_tp_call(PyObject *self, PyObject *args, PyObject *kwds)
81028102

81038103
/* There are two slot dispatch functions for tp_getattro.
81048104
8105-
- slot_tp_getattro() is used when __getattribute__ is overridden
8105+
- _Py_slot_tp_getattro() is used when __getattribute__ is overridden
81068106
but no __getattr__ hook is present;
81078107
8108-
- slot_tp_getattr_hook() is used when a __getattr__ hook is present.
8108+
- _Py_slot_tp_getattr_hook() is used when a __getattr__ hook is present.
81098109
8110-
The code in update_one_slot() always installs slot_tp_getattr_hook(); this
8111-
detects the absence of __getattr__ and then installs the simpler slot if
8112-
necessary. */
8110+
The code in update_one_slot() always installs _Py_slot_tp_getattr_hook();
8111+
this detects the absence of __getattr__ and then installs the simpler
8112+
slot if necessary. */
81138113

8114-
static PyObject *
8115-
slot_tp_getattro(PyObject *self, PyObject *name)
8114+
PyObject *
8115+
_Py_slot_tp_getattro(PyObject *self, PyObject *name)
81168116
{
81178117
PyObject *stack[2] = {self, name};
81188118
return vectorcall_method(&_Py_ID(__getattribute__), stack, 2);
@@ -8143,8 +8143,8 @@ call_attribute(PyObject *self, PyObject *attr, PyObject *name)
81438143
return res;
81448144
}
81458145

8146-
static PyObject *
8147-
slot_tp_getattr_hook(PyObject *self, PyObject *name)
8146+
PyObject *
8147+
_Py_slot_tp_getattr_hook(PyObject *self, PyObject *name)
81488148
{
81498149
PyTypeObject *tp = Py_TYPE(self);
81508150
PyObject *getattr, *getattribute, *res;
@@ -8157,8 +8157,8 @@ slot_tp_getattr_hook(PyObject *self, PyObject *name)
81578157
getattr = _PyType_Lookup(tp, &_Py_ID(__getattr__));
81588158
if (getattr == NULL) {
81598159
/* No __getattr__ hook: use a simpler dispatcher */
8160-
tp->tp_getattro = slot_tp_getattro;
8161-
return slot_tp_getattro(self, name);
8160+
tp->tp_getattro = _Py_slot_tp_getattro;
8161+
return _Py_slot_tp_getattro(self, name);
81628162
}
81638163
Py_INCREF(getattr);
81648164
/* speed hack: we could use lookup_maybe, but that would resolve the
@@ -8519,10 +8519,10 @@ static slotdef slotdefs[] = {
85198519
PyWrapperFlag_KEYWORDS),
85208520
TPSLOT("__str__", tp_str, slot_tp_str, wrap_unaryfunc,
85218521
"__str__($self, /)\n--\n\nReturn str(self)."),
8522-
TPSLOT("__getattribute__", tp_getattro, slot_tp_getattr_hook,
8522+
TPSLOT("__getattribute__", tp_getattro, _Py_slot_tp_getattr_hook,
85238523
wrap_binaryfunc,
85248524
"__getattribute__($self, name, /)\n--\n\nReturn getattr(self, name)."),
8525-
TPSLOT("__getattr__", tp_getattro, slot_tp_getattr_hook, NULL, ""),
8525+
TPSLOT("__getattr__", tp_getattro, _Py_slot_tp_getattr_hook, NULL, ""),
85268526
TPSLOT("__setattr__", tp_setattro, slot_tp_setattro, wrap_setattr,
85278527
"__setattr__($self, name, value, /)\n--\n\nImplement setattr(self, name, value)."),
85288528
TPSLOT("__delattr__", tp_setattro, slot_tp_setattro, wrap_delattr,

‎Python/ceval.c

+41-2
Original file line numberDiff line numberDiff line change
@@ -3698,6 +3698,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
36983698
DEOPT_IF(cls->tp_version_tag != type_version, LOAD_ATTR);
36993699
assert(type_version != 0);
37003700
PyObject *fget = read_obj(cache->descr);
3701+
assert(Py_IS_TYPE(fget, &PyFunction_Type));
37013702
PyFunctionObject *f = (PyFunctionObject *)fget;
37023703
uint32_t func_version = read_u32(cache->keys_version);
37033704
assert(func_version != 0);
@@ -3709,8 +3710,8 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
37093710
Py_INCREF(fget);
37103711
_PyInterpreterFrame *new_frame = _PyFrame_PushUnchecked(tstate, f);
37113712
SET_TOP(NULL);
3712-
int push_null = !(oparg & 1);
3713-
STACK_SHRINK(push_null);
3713+
int shrink_stack = !(oparg & 1);
3714+
STACK_SHRINK(shrink_stack);
37143715
new_frame->localsplus[0] = owner;
37153716
for (int i = 1; i < code->co_nlocalsplus; i++) {
37163717
new_frame->localsplus[i] = NULL;
@@ -3724,6 +3725,44 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
37243725
goto start_frame;
37253726
}
37263727

3728+
TARGET(LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN) {
3729+
assert(cframe.use_tracing == 0);
3730+
DEOPT_IF(tstate->interp->eval_frame, LOAD_ATTR);
3731+
_PyLoadMethodCache *cache = (_PyLoadMethodCache *)next_instr;
3732+
PyObject *owner = TOP();
3733+
PyTypeObject *cls = Py_TYPE(owner);
3734+
uint32_t type_version = read_u32(cache->type_version);
3735+
DEOPT_IF(cls->tp_version_tag != type_version, LOAD_ATTR);
3736+
assert(type_version != 0);
3737+
PyObject *getattribute = read_obj(cache->descr);
3738+
assert(Py_IS_TYPE(getattribute, &PyFunction_Type));
3739+
PyFunctionObject *f = (PyFunctionObject *)getattribute;
3740+
PyCodeObject *code = (PyCodeObject *)f->func_code;
3741+
DEOPT_IF(((PyCodeObject *)f->func_code)->co_argcount != 2, LOAD_ATTR);
3742+
DEOPT_IF(!_PyThreadState_HasStackSpace(tstate, code->co_framesize), CALL);
3743+
STAT_INC(LOAD_ATTR, hit);
3744+
3745+
PyObject *name = GETITEM(names, oparg >> 1);
3746+
Py_INCREF(f);
3747+
_PyInterpreterFrame *new_frame = _PyFrame_PushUnchecked(tstate, f);
3748+
SET_TOP(NULL);
3749+
int shrink_stack = !(oparg & 1);
3750+
STACK_SHRINK(shrink_stack);
3751+
Py_INCREF(name);
3752+
new_frame->localsplus[0] = owner;
3753+
new_frame->localsplus[1] = name;
3754+
for (int i = 2; i < code->co_nlocalsplus; i++) {
3755+
new_frame->localsplus[i] = NULL;
3756+
}
3757+
_PyFrame_SetStackPointer(frame, stack_pointer);
3758+
JUMPBY(INLINE_CACHE_ENTRIES_LOAD_ATTR);
3759+
frame->prev_instr = next_instr - 1;
3760+
new_frame->previous = frame;
3761+
frame = cframe.current_frame = new_frame;
3762+
CALL_STAT_INC(inlined_py_calls);
3763+
goto start_frame;
3764+
}
3765+
37273766
TARGET(STORE_ATTR_ADAPTIVE) {
37283767
assert(cframe.use_tracing == 0);
37293768
_PyAttrCache *cache = (_PyAttrCache *)next_instr;

‎Python/opcode_targets.h

+11-11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Python/pylifecycle.c

+3
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,9 @@ pycore_init_builtins(PyThreadState *tstate)
787787
PyObject *list_append = _PyType_Lookup(&PyList_Type, &_Py_ID(append));
788788
assert(list_append);
789789
interp->callable_cache.list_append = list_append;
790+
PyObject *object__getattribute__ = _PyType_Lookup(&PyBaseObject_Type, &_Py_ID(__getattribute__));
791+
assert(object__getattribute__);
792+
interp->callable_cache.object__getattribute__ = object__getattribute__;
790793

791794
if (_PyBuiltins_AddExceptions(bimod) < 0) {
792795
return _PyStatus_ERR("failed to add exceptions to builtins");

‎Python/specialize.c

+103-19
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,8 @@ miss_counter_start(void) {
362362
#define SPEC_FAIL_OUT_OF_RANGE 4
363363
#define SPEC_FAIL_EXPECTED_ERROR 5
364364
#define SPEC_FAIL_WRONG_NUMBER_ARGUMENTS 6
365+
#define SPEC_FAIL_NOT_PY_FUNCTION 7
366+
365367

366368
#define SPEC_FAIL_LOAD_GLOBAL_NON_STRING_OR_SPLIT 18
367369

@@ -387,6 +389,7 @@ miss_counter_start(void) {
387389
#define SPEC_FAIL_ATTR_HAS_MANAGED_DICT 25
388390
#define SPEC_FAIL_ATTR_INSTANCE_ATTRIBUTE 26
389391
#define SPEC_FAIL_ATTR_METACLASS_ATTRIBUTE 27
392+
#define SPEC_FAIL_ATTR_PROPERTY_NOT_PY_FUNCTION 28
390393

391394
/* Binary subscr and store subscr */
392395

@@ -498,6 +501,9 @@ miss_counter_start(void) {
498501
#define SPEC_FAIL_UNPACK_SEQUENCE_ITERATOR 8
499502
#define SPEC_FAIL_UNPACK_SEQUENCE_SEQUENCE 9
500503

504+
static int function_kind(PyCodeObject *code);
505+
static bool function_check_args(PyObject *o, int expected_argcount, int opcode);
506+
static uint32_t function_get_version(PyObject *o, int opcode);
501507

502508
static int
503509
specialize_module_load_attr(PyObject *owner, _Py_CODEUNIT *instr,
@@ -557,21 +563,58 @@ typedef enum {
557563
MUTABLE, /* Instance of a mutable class; might, or might not, be a descriptor */
558564
ABSENT, /* Attribute is not present on the class */
559565
DUNDER_CLASS, /* __class__ attribute */
560-
GETSET_OVERRIDDEN /* __getattribute__ or __setattr__ has been overridden */
566+
GETSET_OVERRIDDEN, /* __getattribute__ or __setattr__ has been overridden */
567+
GETATTRIBUTE_IS_PYTHON_FUNCTION /* Descriptor requires calling a Python __getattribute__ */
561568
} DescriptorClassification;
562569

563570

564571
static DescriptorClassification
565572
analyze_descriptor(PyTypeObject *type, PyObject *name, PyObject **descr, int store)
566573
{
574+
bool has_getattr = false;
567575
if (store) {
568576
if (type->tp_setattro != PyObject_GenericSetAttr) {
569577
*descr = NULL;
570578
return GETSET_OVERRIDDEN;
571579
}
572580
}
573581
else {
574-
if (type->tp_getattro != PyObject_GenericGetAttr) {
582+
getattrofunc getattro_slot = type->tp_getattro;
583+
if (getattro_slot == PyObject_GenericGetAttr) {
584+
/* Normal attribute lookup; */
585+
has_getattr = false;
586+
}
587+
else if (getattro_slot == _Py_slot_tp_getattr_hook ||
588+
getattro_slot == _Py_slot_tp_getattro) {
589+
/* One or both of __getattribute__ or __getattr__ may have been
590+
overridden See typeobject.c for why these functions are special. */
591+
PyObject *getattribute = _PyType_Lookup(type,
592+
&_Py_ID(__getattribute__));
593+
PyInterpreterState *interp = _PyInterpreterState_GET();
594+
bool has_custom_getattribute = getattribute != NULL &&
595+
getattribute != interp->callable_cache.object__getattribute__;
596+
has_getattr = _PyType_Lookup(type, &_Py_ID(__getattr__)) != NULL;
597+
if (has_custom_getattribute) {
598+
if (getattro_slot == _Py_slot_tp_getattro &&
599+
!has_getattr &&
600+
Py_IS_TYPE(getattribute, &PyFunction_Type)) {
601+
*descr = getattribute;
602+
return GETATTRIBUTE_IS_PYTHON_FUNCTION;
603+
}
604+
/* Potentially both __getattr__ and __getattribute__ are set.
605+
Too complicated */
606+
*descr = NULL;
607+
return GETSET_OVERRIDDEN;
608+
}
609+
/* Potentially has __getattr__ but no custom __getattribute__.
610+
Fall through to usual descriptor analysis.
611+
Usual attribute lookup should only be allowed at runtime
612+
if we can guarantee that there is no way an exception can be
613+
raised. This means some specializations, e.g. specializing
614+
for property() isn't safe.
615+
*/
616+
}
617+
else {
575618
*descr = NULL;
576619
return GETSET_OVERRIDDEN;
577620
}
@@ -595,7 +638,10 @@ analyze_descriptor(PyTypeObject *type, PyObject *name, PyObject **descr, int sto
595638
return OTHER_SLOT;
596639
}
597640
if (desc_cls == &PyProperty_Type) {
598-
return PROPERTY;
641+
/* We can't detect at runtime whether an attribute exists
642+
with property. So that means we may have to call
643+
__getattr__. */
644+
return has_getattr ? GETSET_OVERRIDDEN : PROPERTY;
599645
}
600646
if (PyUnicode_CompareWithASCIIString(name, "__class__") == 0) {
601647
if (descriptor == _PyType_Lookup(&PyBaseObject_Type, name)) {
@@ -674,7 +720,6 @@ specialize_dict_access(
674720
static int specialize_attr_loadmethod(PyObject* owner, _Py_CODEUNIT* instr, PyObject* name,
675721
PyObject* descr, DescriptorClassification kind);
676722
static int specialize_class_load_attr(PyObject* owner, _Py_CODEUNIT* instr, PyObject* name);
677-
static int function_kind(PyCodeObject *code);
678723

679724
int
680725
_Py_Specialize_LoadAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name)
@@ -729,24 +774,13 @@ _Py_Specialize_LoadAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name)
729774
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_EXPECTED_ERROR);
730775
goto fail;
731776
}
732-
if (Py_TYPE(fget) != &PyFunction_Type) {
733-
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_ATTR_PROPERTY);
777+
if (!Py_IS_TYPE(fget, &PyFunction_Type)) {
778+
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_ATTR_PROPERTY_NOT_PY_FUNCTION);
734779
goto fail;
735780
}
736-
PyFunctionObject *func = (PyFunctionObject *)fget;
737-
PyCodeObject *fcode = (PyCodeObject *)func->func_code;
738-
int kind = function_kind(fcode);
739-
if (kind != SIMPLE_FUNCTION) {
740-
SPECIALIZATION_FAIL(LOAD_ATTR, kind);
741-
goto fail;
742-
}
743-
if (fcode->co_argcount != 1) {
744-
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_WRONG_NUMBER_ARGUMENTS);
745-
goto fail;
746-
}
747-
int version = _PyFunction_GetVersionForCurrentState(func);
781+
uint32_t version = function_check_args(fget, 1, LOAD_ATTR) &&
782+
function_get_version(fget, LOAD_ATTR);
748783
if (version == 0) {
749-
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OUT_OF_VERSIONS);
750784
goto fail;
751785
}
752786
write_u32(lm_cache->keys_version, version);
@@ -795,6 +829,22 @@ _Py_Specialize_LoadAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name)
795829
case GETSET_OVERRIDDEN:
796830
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OVERRIDDEN);
797831
goto fail;
832+
case GETATTRIBUTE_IS_PYTHON_FUNCTION:
833+
{
834+
assert(type->tp_getattro == _Py_slot_tp_getattro);
835+
assert(Py_IS_TYPE(descr, &PyFunction_Type));
836+
_PyLoadMethodCache *lm_cache = (_PyLoadMethodCache *)(instr + 1);
837+
uint32_t func_version = function_check_args(descr, 2, LOAD_ATTR) &&
838+
function_get_version(descr, LOAD_ATTR);
839+
if (func_version == 0) {
840+
goto fail;
841+
}
842+
/* borrowed */
843+
write_obj(lm_cache->descr, descr);
844+
write_u32(lm_cache->type_version, type->tp_version_tag);
845+
_Py_SET_OPCODE(*instr, LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN);
846+
goto success;
847+
}
798848
case BUILTIN_CLASSMETHOD:
799849
case PYTHON_CLASSMETHOD:
800850
case NON_OVERRIDING:
@@ -873,6 +923,7 @@ _Py_Specialize_StoreAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name)
873923
case MUTABLE:
874924
SPECIALIZATION_FAIL(STORE_ATTR, SPEC_FAIL_ATTR_MUTABLE_CLASS);
875925
goto fail;
926+
case GETATTRIBUTE_IS_PYTHON_FUNCTION:
876927
case GETSET_OVERRIDDEN:
877928
SPECIALIZATION_FAIL(STORE_ATTR, SPEC_FAIL_OVERRIDDEN);
878929
goto fail;
@@ -1215,6 +1266,39 @@ function_kind(PyCodeObject *code) {
12151266
return SIMPLE_FUNCTION;
12161267
}
12171268

1269+
/* Returning false indicates a failure. */
1270+
static bool
1271+
function_check_args(PyObject *o, int expected_argcount, int opcode)
1272+
{
1273+
assert(Py_IS_TYPE(o, &PyFunction_Type));
1274+
PyFunctionObject *func = (PyFunctionObject *)o;
1275+
PyCodeObject *fcode = (PyCodeObject *)func->func_code;
1276+
int kind = function_kind(fcode);
1277+
if (kind != SIMPLE_FUNCTION) {
1278+
SPECIALIZATION_FAIL(opcode, kind);
1279+
return false;
1280+
}
1281+
if (fcode->co_argcount != expected_argcount) {
1282+
SPECIALIZATION_FAIL(opcode, SPEC_FAIL_WRONG_NUMBER_ARGUMENTS);
1283+
return false;
1284+
}
1285+
return true;
1286+
}
1287+
1288+
/* Returning 0 indicates a failure. */
1289+
static uint32_t
1290+
function_get_version(PyObject *o, int opcode)
1291+
{
1292+
assert(Py_IS_TYPE(o, &PyFunction_Type));
1293+
PyFunctionObject *func = (PyFunctionObject *)o;
1294+
uint32_t version = _PyFunction_GetVersionForCurrentState(func);
1295+
if (version == 0) {
1296+
SPECIALIZATION_FAIL(opcode, SPEC_FAIL_OUT_OF_VERSIONS);
1297+
return 0;
1298+
}
1299+
return version;
1300+
}
1301+
12181302
int
12191303
_Py_Specialize_BinarySubscr(
12201304
PyObject *container, PyObject *sub, _Py_CODEUNIT *instr)

0 commit comments

Comments
 (0)
Please sign in to comment.