From 60ab351a01c54d5cc3268e17c78703c90ddf053a Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sat, 7 Aug 2021 00:11:04 +0800 Subject: [PATCH 01/40] WIP: implement LOAD_METHOD_HINT --- Include/internal/pycore_code.h | 1 + Include/opcode.h | 3 ++ Lib/opcode.py | 3 ++ Python/ceval.c | 83 ++++++++++++++++++++++++++++++++++ Python/opcode_targets.h | 6 +-- Python/specialize.c | 74 ++++++++++++++++++++++++++++++ 6 files changed, 167 insertions(+), 3 deletions(-) diff --git a/Include/internal/pycore_code.h b/Include/internal/pycore_code.h index 312939ee1d26e6..e7dac8fbf5f1ea 100644 --- a/Include/internal/pycore_code.h +++ b/Include/internal/pycore_code.h @@ -298,6 +298,7 @@ cache_backoff(_PyAdaptiveEntry *entry) { int _Py_Specialize_LoadAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SpecializedCacheEntry *cache); int _Py_Specialize_LoadGlobal(PyObject *globals, PyObject *builtins, _Py_CODEUNIT *instr, PyObject *name, SpecializedCacheEntry *cache); +int _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SpecializedCacheEntry *cache); int _Py_Specialize_BinarySubscr(PyObject *sub, PyObject *container, _Py_CODEUNIT *instr); #define SPECIALIZATION_STATS 0 diff --git a/Include/opcode.h b/Include/opcode.h index 7bebb871edb444..88e9136d0599d6 100644 --- a/Include/opcode.h +++ b/Include/opcode.h @@ -149,6 +149,9 @@ extern "C" { #define LOAD_GLOBAL_ADAPTIVE 41 #define LOAD_GLOBAL_MODULE 42 #define LOAD_GLOBAL_BUILTIN 43 +#define LOAD_METHOD_ADAPTIVE 44 +#define LOAD_METHOD_SPLIT_KEYS 45 +#define LOAD_METHOD_WITH_HINT 46 #ifdef NEED_OPCODE_JUMP_TABLES static uint32_t _PyOpcode_RelativeJump[8] = { 0U, diff --git a/Lib/opcode.py b/Lib/opcode.py index 7735848db0958d..6ae02249e3a986 100644 --- a/Lib/opcode.py +++ b/Lib/opcode.py @@ -233,6 +233,9 @@ def jabs_op(name, op): "LOAD_GLOBAL_ADAPTIVE", "LOAD_GLOBAL_MODULE", "LOAD_GLOBAL_BUILTIN", + "LOAD_METHOD_ADAPTIVE", + "LOAD_METHOD_SPLIT_KEYS", + "LOAD_METHOD_WITH_HINT", ] _specialization_stats = [ diff --git a/Python/ceval.c b/Python/ceval.c index e3658b81388321..8825d83efd6f44 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4069,10 +4069,12 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr } case TARGET(LOAD_METHOD): { + PREDICTED(LOAD_METHOD); /* Designed to work in tandem with CALL_METHOD. */ PyObject *name = GETITEM(names, oparg); PyObject *obj = TOP(); PyObject *meth = NULL; + STAT_INC(LOAD_ATTR, unquickened); int meth_found = _PyObject_GetMethod(obj, name, &meth); @@ -4105,6 +4107,86 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr DISPATCH(); } + case TARGET(LOAD_METHOD_ADAPTIVE): { + assert(cframe.use_tracing == 0); + SpecializedCacheEntry *cache = GET_CACHE(); + if (cache->adaptive.counter == 0) { + PyObject *owner = TOP(); + PyObject *name = GETITEM(names, cache->adaptive.original_oparg); + next_instr--; + if (_Py_Specialize_LoadMethod(owner, next_instr, name, cache) < 0) { + goto error; + } + DISPATCH(); + } + else { + STAT_INC(LOAD_METHOD, deferred); + cache->adaptive.counter--; + oparg = cache->adaptive.original_oparg; + STAT_DEC(LOAD_METHOD, unquickened); + JUMP_TO_INSTRUCTION(LOAD_METHOD); + } + } + + //case TARGET(LOAD_METHOD_SPLIT_KEYS): { + // assert(cframe.use_tracing == 0); + // PyObject *owner = TOP(); + // PyObject *res; + // PyTypeObject *tp = Py_TYPE(owner); + // SpecializedCacheEntry *caches = GET_CACHE(); + // _PyAdaptiveEntry *cache0 = &caches[0].adaptive; + // _PyLoadAttrCache *cache1 = &caches[-1].load_attr; + // assert(cache1->tp_version != 0); + // DEOPT_IF(tp->tp_version_tag != cache1->tp_version, LOAD_METHOD); + // assert(tp->tp_dictoffset > 0); + // PyDictObject *dict = *(PyDictObject **)(((char *)owner) + tp->tp_dictoffset); + // DEOPT_IF(dict == NULL, LOAD_METHOD); + // assert(PyDict_CheckExact((PyObject *)dict)); + // DEOPT_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, LOAD_METHOD); + // res = dict->ma_values[cache0->index]; + // DEOPT_IF(res == NULL, LOAD_METHOD); + // STAT_INC(LOAD_METHOD, hit); + // record_cache_hit(cache0); + // Py_INCREF(res); + // SET_TOP(res); + // Py_DECREF(owner); + // DISPATCH(); + //} + + case TARGET(LOAD_METHOD_WITH_HINT): { + assert(cframe.use_tracing == 0); + PyObject *self = TOP(); + PyObject *owner = (PyObject *)Py_TYPE(self); + PyObject *res; + PyTypeObject *tp = Py_TYPE(owner); + SpecializedCacheEntry *caches = GET_CACHE(); + _PyAdaptiveEntry *cache0 = &caches[0].adaptive; + _PyLoadAttrCache *cache1 = &caches[-1].load_attr; + assert(cache1->tp_version != 0); + DEOPT_IF(tp->tp_version_tag != cache1->tp_version, LOAD_METHOD); + assert(tp->tp_dictoffset > 0); + PyDictObject *dict = *(PyDictObject **)(((char *)owner) + tp->tp_dictoffset); + DEOPT_IF(dict == NULL, LOAD_METHOD); + assert(PyDict_CheckExact((PyObject *)dict)); + PyObject *name = GETITEM(names, cache0->original_oparg); + uint32_t hint = cache1->dk_version_or_hint; + DEOPT_IF(hint >= dict->ma_keys->dk_nentries, LOAD_METHOD); + PyDictKeyEntry *ep = DK_ENTRIES(dict->ma_keys) + hint; + DEOPT_IF(ep->me_key != name, LOAD_METHOD); + res = ep->me_value; + DEOPT_IF( + res == NULL || + Py_TYPE(res)->tp_descr_get == NULL || + !_PyType_HasFeature(Py_TYPE(res), Py_TPFLAGS_METHOD_DESCRIPTOR), + LOAD_METHOD); + STAT_INC(LOAD_METHOD, hit); + record_cache_hit(cache0); + Py_INCREF(res); + SET_TOP(res); + PUSH(self); + DISPATCH(); + } + case TARGET(CALL_METHOD): { /* Designed to work in tamdem with LOAD_METHOD. */ PyObject **sp, *res; @@ -4430,6 +4512,7 @@ opname ## _miss: \ MISS_WITH_CACHE(LOAD_ATTR) MISS_WITH_CACHE(LOAD_GLOBAL) +MISS_WITH_CACHE(LOAD_METHOD) MISS_WITH_OPARG_COUNTER(BINARY_SUBSCR) binary_subscr_dict_error: diff --git a/Python/opcode_targets.h b/Python/opcode_targets.h index d88c766c07ab46..a807c83131efb0 100644 --- a/Python/opcode_targets.h +++ b/Python/opcode_targets.h @@ -43,9 +43,9 @@ static void *opcode_targets[256] = { &&TARGET_LOAD_GLOBAL_ADAPTIVE, &&TARGET_LOAD_GLOBAL_MODULE, &&TARGET_LOAD_GLOBAL_BUILTIN, - &&_unknown_opcode, - &&_unknown_opcode, - &&_unknown_opcode, + &&TARGET_LOAD_METHOD_ADAPTIVE, + &&TARGET_LOAD_METHOD_SPLIT_KEYS, + &&TARGET_LOAD_METHOD_WITH_HINT, &&_unknown_opcode, &&_unknown_opcode, &&TARGET_WITH_EXCEPT_START, diff --git a/Python/specialize.c b/Python/specialize.c index 1ca49d244a87b0..034126a9cb0914 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -261,6 +261,7 @@ get_cache_count(SpecializedCacheOrInstruction *quickened) { static uint8_t adaptive_opcodes[256] = { [LOAD_ATTR] = LOAD_ATTR_ADAPTIVE, [LOAD_GLOBAL] = LOAD_GLOBAL_ADAPTIVE, + [LOAD_METHOD] = LOAD_METHOD_ADAPTIVE, [BINARY_SUBSCR] = BINARY_SUBSCR_ADAPTIVE, }; @@ -268,6 +269,7 @@ static uint8_t adaptive_opcodes[256] = { static uint8_t cache_requirements[256] = { [LOAD_ATTR] = 2, /* _PyAdaptiveEntry and _PyLoadAttrCache */ [LOAD_GLOBAL] = 2, /* _PyAdaptiveEntry and _PyLoadGlobalCache */ + [LOAD_METHOD] = 2, [BINARY_SUBSCR] = 0, }; @@ -675,6 +677,78 @@ _Py_Specialize_LoadAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, Sp return 0; } +int +_Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SpecializedCacheEntry *cache) +{ + _PyAdaptiveEntry *cache0 = &cache->adaptive; + _PyLoadAttrCache *cache1 = &cache[-1].load_attr; + PyTypeObject *type = Py_TYPE(owner); + if (PyModule_CheckExact(owner)) { + // rare - LOAD_ATTR is usually emitted for modules imported + // at global level, not LOAD_METHOD + SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "module method"); + goto fail; + } + if (type->tp_dict == NULL) { + if (PyType_Ready(type) < 0) { + return -1; + } + } + PyObject *descr = NULL; + DesciptorClassification kind = analyze_descriptor(type, name, &descr); + if (kind != METHOD || descr == NULL) { + SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "not a method"); + goto fail; + } + if (type->tp_dictoffset > 0) { + LookupAttrStatus status = lookup_attr_dict( + Py_TYPE(type), (PyObject *)type, name, + &cache1->dk_version_or_hint, &cache1->tp_version, &cache0->index); + + switch (status) { + case ATTR_HINT: + *instr = _Py_MAKECODEUNIT(LOAD_METHOD_WITH_HINT, _Py_OPARG(*instr)); + goto success; + case BAIL: + return -1; + case ATTR_SPLIT_KEYS: + // Rare -- usually only when meth is in o.__dict__ + // rather than type(o).__dict__. Requires more profiling + // to decide if it's worth it. + SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "split keys"); + break; + #if SPECIALIZATION_STATS + case ERR_NOT_DICT: + SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "no dict or not a dict"); + break; + case ERR_NOT_FOUND: + SPECIALIZATION_FAIL(LOAD_ATTR, type, name, "attribute not in dict"); + break; + case ERR_INDEX_OUT_OF_RANGE: + SPECIALIZATION_FAIL(LOAD_ATTR, type, name, "index out of range"); + break; + case ERR_OUT_OF_KEY_VERSIONS: + case ERR_OUT_OF_TYPE_VERSIONS + SPECIALIZATION_FAIL(LOAD_ATTR, type, name, "no more key versions") + break; + case ERR_HINT_OUT_OF_RANGE: + SPECIALIZATION_FAIL(LOAD_ATTR, type, name, "hint out of range") + break; + #endif + } + goto fail; + } +fail: + STAT_INC(LOAD_METHOD, specialization_failure); + assert(!PyErr_Occurred()); + cache_backoff(cache0); + return 0; +success: + STAT_INC(LOAD_METHOD, specialization_success); + assert(!PyErr_Occurred()); + cache0->counter = saturating_start(); + return 0; +} int _Py_Specialize_LoadGlobal( PyObject *globals, PyObject *builtins, From e0f7ebe3cb2b89b6dacd0a52198c625deb9b6ea4 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sat, 7 Aug 2021 01:36:24 +0800 Subject: [PATCH 02/40] fix most tests: validate object __dict__ didn't modify keys --- Python/ceval.c | 29 +++++++++++----- Python/specialize.c | 81 ++++++++++++++++++++++++++------------------- 2 files changed, 68 insertions(+), 42 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index 8825d83efd6f44..ccbf8a8299e018 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4156,20 +4156,33 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr case TARGET(LOAD_METHOD_WITH_HINT): { assert(cframe.use_tracing == 0); PyObject *self = TOP(); - PyObject *owner = (PyObject *)Py_TYPE(self); + PyTypeObject *self_cls = Py_TYPE(self); PyObject *res; - PyTypeObject *tp = Py_TYPE(owner); + PyTypeObject *tp = Py_TYPE(self_cls); SpecializedCacheEntry *caches = GET_CACHE(); _PyAdaptiveEntry *cache0 = &caches[0].adaptive; _PyLoadAttrCache *cache1 = &caches[-1].load_attr; + + assert(cache1->dk_version_or_hint != 0); assert(cache1->tp_version != 0); - DEOPT_IF(tp->tp_version_tag != cache1->tp_version, LOAD_METHOD); - assert(tp->tp_dictoffset > 0); - PyDictObject *dict = *(PyDictObject **)(((char *)owner) + tp->tp_dictoffset); - DEOPT_IF(dict == NULL, LOAD_METHOD); - assert(PyDict_CheckExact((PyObject *)dict)); + DEOPT_IF(self_cls->tp_version_tag != cache1->tp_version, LOAD_METHOD); + assert(self_cls->tp_dictoffset > 0); + + // ensure self.__dict__ didn't modify keys + PyObject **dictptr = _PyObject_GetDictPtr(self); + DEOPT_IF(dictptr == NULL || *dictptr == NULL, LOAD_METHOD); + PyDictObject *dict = (PyDictObject *)*dictptr; + assert(PyDict_CheckExact(dict)); + // printf("%p: %ld %ld\n", self, dict->ma_keys->dk_version, cache1->dk_version_or_hint); + DEOPT_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, LOAD_METHOD); PyObject *name = GETITEM(names, cache0->original_oparg); - uint32_t hint = cache1->dk_version_or_hint; + + // validate against self_cls.__dict__ + uint32_t hint = cache0->index; + dictptr = _PyObject_GetDictPtr((PyObject *)self_cls); + DEOPT_IF(dictptr == NULL || *dictptr == NULL, LOAD_METHOD); + dict = (PyDictObject *)*dictptr; + assert(PyDict_CheckExact((PyObject *)*dictptr)); DEOPT_IF(hint >= dict->ma_keys->dk_nentries, LOAD_METHOD); PyDictKeyEntry *ep = DK_ENTRIES(dict->ma_keys) + hint; DEOPT_IF(ep->me_key != name, LOAD_METHOD); diff --git a/Python/specialize.c b/Python/specialize.c index 034126a9cb0914..152b641107271c 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -701,42 +701,55 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, goto fail; } if (type->tp_dictoffset > 0) { - LookupAttrStatus status = lookup_attr_dict( - Py_TYPE(type), (PyObject *)type, name, - &cache1->dk_version_or_hint, &cache1->tp_version, &cache0->index); - - switch (status) { - case ATTR_HINT: - *instr = _Py_MAKECODEUNIT(LOAD_METHOD_WITH_HINT, _Py_OPARG(*instr)); - goto success; - case BAIL: - return -1; - case ATTR_SPLIT_KEYS: - // Rare -- usually only when meth is in o.__dict__ - // rather than type(o).__dict__. Requires more profiling - // to decide if it's worth it. - SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "split keys"); - break; - #if SPECIALIZATION_STATS - case ERR_NOT_DICT: - SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "no dict or not a dict"); - break; - case ERR_NOT_FOUND: - SPECIALIZATION_FAIL(LOAD_ATTR, type, name, "attribute not in dict"); - break; - case ERR_INDEX_OUT_OF_RANGE: - SPECIALIZATION_FAIL(LOAD_ATTR, type, name, "index out of range"); - break; - case ERR_OUT_OF_KEY_VERSIONS: - case ERR_OUT_OF_TYPE_VERSIONS - SPECIALIZATION_FAIL(LOAD_ATTR, type, name, "no more key versions") - break; - case ERR_HINT_OUT_OF_RANGE: - SPECIALIZATION_FAIL(LOAD_ATTR, type, name, "hint out of range") - break; - #endif + PyObject **dictptr = _PyObject_GetDictPtr((PyObject *)type); + if (*dictptr == NULL || !PyDict_CheckExact(*dictptr)) { + // possibly a builtin type + SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "no dict or not a dict"); + goto fail; } + // We found a type with a __dict__. + PyDictObject *dict = (PyDictObject *)*dictptr; + if ((type->tp_flags & Py_TPFLAGS_HEAPTYPE) + && dict->ma_keys == ((PyHeapTypeObject*)type)->ht_cached_keys) { + // Keys are shared. Rare for LOAD_METHOD. Needs more profiling to + // determine if this is worth it. Usually it applies to methods in + // o.__dict__ rather than the common type(o).__dict__. + SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "shared keys"); goto fail; + } + else { + PyObject *value = NULL; + Py_ssize_t hint = + _PyDict_GetItemHint(dict, name, -1, &value); + if (hint != (uint16_t)hint) { + SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "hint out of range"); + goto fail; + } + uint32_t keys_version = 0; + // if o.__dict__ changes, the method might be found in o.__dict__ + // instead of type lookup. + PyObject **odictptr = _PyObject_GetDictPtr(owner); + if (*odictptr != NULL && PyDict_CheckExact(*odictptr)) { + PyDictObject *dict = (PyDictObject *)*odictptr; + keys_version = _PyDictKeys_GetVersionForCurrentState(dict); + if (keys_version == 0) { + SPECIALIZATION_FAIL(LOAD_ATTR, type, name, + "no more key versions"); + goto fail; + } + } + // else object has no dict -- probably a builtin type + else { + SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "builtin's method"); + goto fail; + } + // printf("owner is %p with %ld\n", owner, keys_version); + cache0->index = (uint16_t)hint; + cache1->dk_version_or_hint = keys_version; + cache1->tp_version = type->tp_version_tag; + *instr = _Py_MAKECODEUNIT(LOAD_METHOD_WITH_HINT, _Py_OPARG(*instr)); + goto success; + } } fail: STAT_INC(LOAD_METHOD, specialization_failure); From ffd7ff31471b7829979b0c45ec35a6cbfc99843b Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sat, 7 Aug 2021 16:53:36 +0800 Subject: [PATCH 03/40] fix all tests and allow no owner.__dict__ to be specialized! --- Include/internal/pycore_code.h | 4 +- Python/ceval.c | 21 +++--- Python/specialize.c | 115 +++++++++++++++++++-------------- 3 files changed, 81 insertions(+), 59 deletions(-) diff --git a/Include/internal/pycore_code.h b/Include/internal/pycore_code.h index e7dac8fbf5f1ea..e76e33bd3ffd73 100644 --- a/Include/internal/pycore_code.h +++ b/Include/internal/pycore_code.h @@ -301,8 +301,8 @@ int _Py_Specialize_LoadGlobal(PyObject *globals, PyObject *builtins, _Py_CODEUNI int _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SpecializedCacheEntry *cache); int _Py_Specialize_BinarySubscr(PyObject *sub, PyObject *container, _Py_CODEUNIT *instr); -#define SPECIALIZATION_STATS 0 -#define SPECIALIZATION_STATS_DETAILED 0 +#define SPECIALIZATION_STATS 1 +#define SPECIALIZATION_STATS_DETAILED 1 #define SPECIALIZATION_STATS_TO_FILE 0 #if SPECIALIZATION_STATS diff --git a/Python/ceval.c b/Python/ceval.c index ccbf8a8299e018..ff32f1da77ae4a 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4074,7 +4074,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr PyObject *name = GETITEM(names, oparg); PyObject *obj = TOP(); PyObject *meth = NULL; - STAT_INC(LOAD_ATTR, unquickened); + STAT_INC(LOAD_METHOD, unquickened); int meth_found = _PyObject_GetMethod(obj, name, &meth); @@ -4158,7 +4158,6 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr PyObject *self = TOP(); PyTypeObject *self_cls = Py_TYPE(self); PyObject *res; - PyTypeObject *tp = Py_TYPE(self_cls); SpecializedCacheEntry *caches = GET_CACHE(); _PyAdaptiveEntry *cache0 = &caches[0].adaptive; _PyLoadAttrCache *cache1 = &caches[-1].load_attr; @@ -4170,14 +4169,18 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr // ensure self.__dict__ didn't modify keys PyObject **dictptr = _PyObject_GetDictPtr(self); - DEOPT_IF(dictptr == NULL || *dictptr == NULL, LOAD_METHOD); - PyDictObject *dict = (PyDictObject *)*dictptr; - assert(PyDict_CheckExact(dict)); - // printf("%p: %ld %ld\n", self, dict->ma_keys->dk_version, cache1->dk_version_or_hint); - DEOPT_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, LOAD_METHOD); + PyDictObject *dict = NULL; + // DEOPT_IF(dictptr == NULL || *dictptr == NULL, LOAD_METHOD); + + if (dictptr != NULL && *dictptr != NULL) { + dict = (PyDictObject *)*dictptr; + assert(PyDict_CheckExact(dict)); + // printf("%p: %ld %ld\n", self, dict->ma_keys->dk_version, cache1->dk_version_or_hint); + DEOPT_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, LOAD_METHOD); + } // don't care if owner has no dict, could be builtin or __slots__ + + // look in self_cls.__dict__, avoiding _PyType_Lookup PyObject *name = GETITEM(names, cache0->original_oparg); - - // validate against self_cls.__dict__ uint32_t hint = cache0->index; dictptr = _PyObject_GetDictPtr((PyObject *)self_cls); DEOPT_IF(dictptr == NULL || *dictptr == NULL, LOAD_METHOD); diff --git a/Python/specialize.c b/Python/specialize.c index 152b641107271c..7ecdbaf5b43071 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -172,6 +172,7 @@ _Py_PrintSpecializationStats(void) #endif print_stats(out, &_specialization_stats[LOAD_ATTR], "load_attr"); print_stats(out, &_specialization_stats[LOAD_GLOBAL], "load_global"); + print_stats(out, &_specialization_stats[LOAD_METHOD], "load_method"); print_stats(out, &_specialization_stats[BINARY_SUBSCR], "binary_subscr"); if (out != stderr) { fclose(out); @@ -682,75 +683,93 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, { _PyAdaptiveEntry *cache0 = &cache->adaptive; _PyLoadAttrCache *cache1 = &cache[-1].load_attr; - PyTypeObject *type = Py_TYPE(owner); + PyTypeObject *owner_cls = Py_TYPE(owner); if (PyModule_CheckExact(owner)) { - // rare - LOAD_ATTR is usually emitted for modules imported - // at global level, not LOAD_METHOD - SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "module method"); + // somewhat common -- maybe todo. + SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "module method"); goto fail; } - if (type->tp_dict == NULL) { - if (PyType_Ready(type) < 0) { + if (owner_cls->tp_dict == NULL) { + if (PyType_Ready(owner_cls) < 0) { return -1; } } PyObject *descr = NULL; - DesciptorClassification kind = analyze_descriptor(type, name, &descr); + DesciptorClassification kind = analyze_descriptor(owner_cls, name, &descr); if (kind != METHOD || descr == NULL) { - SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "not a method"); + SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "not a method"); goto fail; } - if (type->tp_dictoffset > 0) { - PyObject **dictptr = _PyObject_GetDictPtr((PyObject *)type); - if (*dictptr == NULL || !PyDict_CheckExact(*dictptr)) { - // possibly a builtin type - SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "no dict or not a dict"); + if (owner_cls->tp_dictoffset > 0) { + PyObject **cls_dictptr = _PyObject_GetDictPtr((PyObject *)owner_cls); + if (cls_dictptr == NULL || *cls_dictptr == NULL || + !PyDict_CheckExact(*cls_dictptr)) { + // Maybe a builtin type + SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, + "cls no dict or not a dict"); goto fail; } // We found a type with a __dict__. - PyDictObject *dict = (PyDictObject *)*dictptr; - if ((type->tp_flags & Py_TPFLAGS_HEAPTYPE) - && dict->ma_keys == ((PyHeapTypeObject*)type)->ht_cached_keys) { - // Keys are shared. Rare for LOAD_METHOD. Needs more profiling to - // determine if this is worth it. Usually it applies to methods in - // o.__dict__ rather than the common type(o).__dict__. - SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "shared keys"); + PyDictObject *cls_dict = (PyDictObject *)*cls_dictptr; + if ((owner_cls->tp_flags & Py_TPFLAGS_HEAPTYPE) + && cls_dict->ma_keys == ((PyHeapTypeObject*)owner_cls)->ht_cached_keys) { + // Keys are shared. Rare for LOAD_METHOD. + SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "shared keys"); goto fail; } - else { - PyObject *value = NULL; - Py_ssize_t hint = - _PyDict_GetItemHint(dict, name, -1, &value); - if (hint != (uint16_t)hint) { - SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "hint out of range"); - goto fail; + + // if o.__dict__ changes, the method might be found in o.__dict__ + // instead of type lookup. So record o.__dict__. + PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); + uint32_t keys_version = UINT32_MAX; + // o.__dict__ exists + if (*owner_dictptr != NULL && !PyDict_CheckExact(*owner_dictptr)) { + PyDictObject *owner_dict = (PyDictObject *)*owner_dictptr; + // check if its an attr + int is_attr = PyDict_Contains((PyObject *)owner_dict, name); + if (is_attr < 0) { + return -1; } - uint32_t keys_version = 0; - // if o.__dict__ changes, the method might be found in o.__dict__ - // instead of type lookup. - PyObject **odictptr = _PyObject_GetDictPtr(owner); - if (*odictptr != NULL && PyDict_CheckExact(*odictptr)) { - PyDictObject *dict = (PyDictObject *)*odictptr; - keys_version = _PyDictKeys_GetVersionForCurrentState(dict); - if (keys_version == 0) { - SPECIALIZATION_FAIL(LOAD_ATTR, type, name, - "no more key versions"); - goto fail; - } + if (is_attr) { + SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "is attr"); + goto fail; } - // else object has no dict -- probably a builtin type - else { - SPECIALIZATION_FAIL(LOAD_METHOD, type, name, "builtin's method"); + keys_version = _PyDictKeys_GetVersionForCurrentState(owner_dict); + if (keys_version == 0) { + SPECIALIZATION_FAIL(LOAD_ATTR, owner_cls, name, + "no more key versions"); goto fail; } - // printf("owner is %p with %ld\n", owner, keys_version); - cache0->index = (uint16_t)hint; - cache1->dk_version_or_hint = keys_version; - cache1->tp_version = type->tp_version_tag; - *instr = _Py_MAKECODEUNIT(LOAD_METHOD_WITH_HINT, _Py_OPARG(*instr)); - goto success; + } // else owner is maybe a builtin with no dict, or __slots__ + + PyObject *value = NULL; + Py_ssize_t hint = _PyDict_GetItemHint(cls_dict, name, -1, &value); + if (hint != (uint16_t)hint) { + SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "hint out of range"); + goto fail; + } + + // printf("owner is %p with %ld\n", owner, keys_version); + cache0->index = (uint16_t)hint; + cache1->dk_version_or_hint = keys_version; + cache1->tp_version = owner_cls->tp_version_tag; + *instr = _Py_MAKECODEUNIT(LOAD_METHOD_WITH_HINT, _Py_OPARG(*instr)); + goto success; + + } +#if SPECIALIZATION_STATS + else if (owner_cls->tp_dictoffset == 0) { + PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); + if (owner_dictptr == NULL || + *owner_dictptr == NULL || + !PyDict_CheckExact(*owner_dictptr)) { + SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "builtin no dict"); + goto fail; } + SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "builtin with dict"); + goto fail; } +#endif fail: STAT_INC(LOAD_METHOD, specialization_failure); assert(!PyErr_Occurred()); From 2b014a22c0bda554be2769187a18e29ef2cd9f3f Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sat, 7 Aug 2021 17:36:50 +0800 Subject: [PATCH 04/40] specialize builtins too --- Include/opcode.h | 3 +-- Lib/opcode.py | 1 - Python/ceval.c | 30 +++--------------------------- Python/opcode_targets.h | 2 +- Python/specialize.c | 41 +++++++++++++---------------------------- 5 files changed, 18 insertions(+), 59 deletions(-) diff --git a/Include/opcode.h b/Include/opcode.h index 88e9136d0599d6..69a728339466a7 100644 --- a/Include/opcode.h +++ b/Include/opcode.h @@ -150,8 +150,7 @@ extern "C" { #define LOAD_GLOBAL_MODULE 42 #define LOAD_GLOBAL_BUILTIN 43 #define LOAD_METHOD_ADAPTIVE 44 -#define LOAD_METHOD_SPLIT_KEYS 45 -#define LOAD_METHOD_WITH_HINT 46 +#define LOAD_METHOD_WITH_HINT 45 #ifdef NEED_OPCODE_JUMP_TABLES static uint32_t _PyOpcode_RelativeJump[8] = { 0U, diff --git a/Lib/opcode.py b/Lib/opcode.py index 6ae02249e3a986..80237bd95d1d98 100644 --- a/Lib/opcode.py +++ b/Lib/opcode.py @@ -234,7 +234,6 @@ def jabs_op(name, op): "LOAD_GLOBAL_MODULE", "LOAD_GLOBAL_BUILTIN", "LOAD_METHOD_ADAPTIVE", - "LOAD_METHOD_SPLIT_KEYS", "LOAD_METHOD_WITH_HINT", ] diff --git a/Python/ceval.c b/Python/ceval.c index ff32f1da77ae4a..d0902690e49cc1 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4128,31 +4128,6 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr } } - //case TARGET(LOAD_METHOD_SPLIT_KEYS): { - // assert(cframe.use_tracing == 0); - // PyObject *owner = TOP(); - // PyObject *res; - // PyTypeObject *tp = Py_TYPE(owner); - // SpecializedCacheEntry *caches = GET_CACHE(); - // _PyAdaptiveEntry *cache0 = &caches[0].adaptive; - // _PyLoadAttrCache *cache1 = &caches[-1].load_attr; - // assert(cache1->tp_version != 0); - // DEOPT_IF(tp->tp_version_tag != cache1->tp_version, LOAD_METHOD); - // assert(tp->tp_dictoffset > 0); - // PyDictObject *dict = *(PyDictObject **)(((char *)owner) + tp->tp_dictoffset); - // DEOPT_IF(dict == NULL, LOAD_METHOD); - // assert(PyDict_CheckExact((PyObject *)dict)); - // DEOPT_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, LOAD_METHOD); - // res = dict->ma_values[cache0->index]; - // DEOPT_IF(res == NULL, LOAD_METHOD); - // STAT_INC(LOAD_METHOD, hit); - // record_cache_hit(cache0); - // Py_INCREF(res); - // SET_TOP(res); - // Py_DECREF(owner); - // DISPATCH(); - //} - case TARGET(LOAD_METHOD_WITH_HINT): { assert(cframe.use_tracing == 0); PyObject *self = TOP(); @@ -4165,13 +4140,14 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr assert(cache1->dk_version_or_hint != 0); assert(cache1->tp_version != 0); DEOPT_IF(self_cls->tp_version_tag != cache1->tp_version, LOAD_METHOD); - assert(self_cls->tp_dictoffset > 0); + assert(self_cls->tp_dictoffset >= 0); // ensure self.__dict__ didn't modify keys PyObject **dictptr = _PyObject_GetDictPtr(self); PyDictObject *dict = NULL; - // DEOPT_IF(dictptr == NULL || *dictptr == NULL, LOAD_METHOD); + // maybe todo: don't need this branch if + // LOAD_METHOD_WITH_HINT_NO_DICT exists if (dictptr != NULL && *dictptr != NULL) { dict = (PyDictObject *)*dictptr; assert(PyDict_CheckExact(dict)); diff --git a/Python/opcode_targets.h b/Python/opcode_targets.h index a807c83131efb0..061baebfbcc89e 100644 --- a/Python/opcode_targets.h +++ b/Python/opcode_targets.h @@ -44,10 +44,10 @@ static void *opcode_targets[256] = { &&TARGET_LOAD_GLOBAL_MODULE, &&TARGET_LOAD_GLOBAL_BUILTIN, &&TARGET_LOAD_METHOD_ADAPTIVE, - &&TARGET_LOAD_METHOD_SPLIT_KEYS, &&TARGET_LOAD_METHOD_WITH_HINT, &&_unknown_opcode, &&_unknown_opcode, + &&_unknown_opcode, &&TARGET_WITH_EXCEPT_START, &&TARGET_GET_AITER, &&TARGET_GET_ANEXT, diff --git a/Python/specialize.c b/Python/specialize.c index 7ecdbaf5b43071..a1c463315a8581 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -700,17 +700,18 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "not a method"); goto fail; } - if (owner_cls->tp_dictoffset > 0) { - PyObject **cls_dictptr = _PyObject_GetDictPtr((PyObject *)owner_cls); - if (cls_dictptr == NULL || *cls_dictptr == NULL || - !PyDict_CheckExact(*cls_dictptr)) { - // Maybe a builtin type - SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, - "cls no dict or not a dict"); - goto fail; - } + PyObject **cls_dictptr = _PyObject_GetDictPtr((PyObject *)owner_cls); + int cls_has_dict = (cls_dictptr != NULL && + *cls_dictptr != NULL && + PyDict_CheckExact(*cls_dictptr)); + PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); + int owner_has_dict = (owner_dictptr != NULL && + *owner_dictptr != NULL && + !PyDict_CheckExact(*owner_dictptr)); + PyDictObject *cls_dict = cls_has_dict ? (PyDictObject *)*cls_dictptr : NULL; + PyDictObject *owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; + if (owner_cls->tp_dictoffset >= 0 && cls_has_dict) { // We found a type with a __dict__. - PyDictObject *cls_dict = (PyDictObject *)*cls_dictptr; if ((owner_cls->tp_flags & Py_TPFLAGS_HEAPTYPE) && cls_dict->ma_keys == ((PyHeapTypeObject*)owner_cls)->ht_cached_keys) { // Keys are shared. Rare for LOAD_METHOD. @@ -719,12 +720,9 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, } // if o.__dict__ changes, the method might be found in o.__dict__ - // instead of type lookup. So record o.__dict__. - PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); + // instead of old type lookup. So record o.__dict__. uint32_t keys_version = UINT32_MAX; - // o.__dict__ exists - if (*owner_dictptr != NULL && !PyDict_CheckExact(*owner_dictptr)) { - PyDictObject *owner_dict = (PyDictObject *)*owner_dictptr; + if (owner_has_dict) { // check if its an attr int is_attr = PyDict_Contains((PyObject *)owner_dict, name); if (is_attr < 0) { @@ -757,19 +755,6 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, goto success; } -#if SPECIALIZATION_STATS - else if (owner_cls->tp_dictoffset == 0) { - PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); - if (owner_dictptr == NULL || - *owner_dictptr == NULL || - !PyDict_CheckExact(*owner_dictptr)) { - SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "builtin no dict"); - goto fail; - } - SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "builtin with dict"); - goto fail; - } -#endif fail: STAT_INC(LOAD_METHOD, specialization_failure); assert(!PyErr_Occurred()); From b20088724c37b303ea0792a6a4a8413fa256c96c Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sat, 7 Aug 2021 17:42:15 +0800 Subject: [PATCH 05/40] turn off specialization stats --- Include/internal/pycore_code.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Include/internal/pycore_code.h b/Include/internal/pycore_code.h index e76e33bd3ffd73..e7dac8fbf5f1ea 100644 --- a/Include/internal/pycore_code.h +++ b/Include/internal/pycore_code.h @@ -301,8 +301,8 @@ int _Py_Specialize_LoadGlobal(PyObject *globals, PyObject *builtins, _Py_CODEUNI int _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SpecializedCacheEntry *cache); int _Py_Specialize_BinarySubscr(PyObject *sub, PyObject *container, _Py_CODEUNIT *instr); -#define SPECIALIZATION_STATS 1 -#define SPECIALIZATION_STATS_DETAILED 1 +#define SPECIALIZATION_STATS 0 +#define SPECIALIZATION_STATS_DETAILED 0 #define SPECIALIZATION_STATS_TO_FILE 0 #if SPECIALIZATION_STATS From d977986a1e26f6f40511383692751fea19da77f9 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sat, 7 Aug 2021 21:53:27 +0800 Subject: [PATCH 06/40] improve specialization --- Python/ceval.c | 17 +++++++++-------- Python/specialize.c | 5 +++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index d0902690e49cc1..ab2fe1112caece 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4139,15 +4139,16 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr assert(cache1->dk_version_or_hint != 0); assert(cache1->tp_version != 0); - DEOPT_IF(self_cls->tp_version_tag != cache1->tp_version, LOAD_METHOD); assert(self_cls->tp_dictoffset >= 0); + assert(Py_TYPE(self_cls)->tp_dictoffset > 0); + DEOPT_IF(self_cls->tp_version_tag != cache1->tp_version, LOAD_METHOD); + + // inline version of _PyObject_GetDictPtr for offset >= 0 + PyObject **dictptr = self_cls->tp_dictoffset != 0 ? + (PyObject **) ((char *)self + self_cls->tp_dictoffset) : NULL; + PyDictObject *dict = NULL; // ensure self.__dict__ didn't modify keys - PyObject **dictptr = _PyObject_GetDictPtr(self); - PyDictObject *dict = NULL; - - // maybe todo: don't need this branch if - // LOAD_METHOD_WITH_HINT_NO_DICT exists if (dictptr != NULL && *dictptr != NULL) { dict = (PyDictObject *)*dictptr; assert(PyDict_CheckExact(dict)); @@ -4158,10 +4159,10 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr // look in self_cls.__dict__, avoiding _PyType_Lookup PyObject *name = GETITEM(names, cache0->original_oparg); uint32_t hint = cache0->index; - dictptr = _PyObject_GetDictPtr((PyObject *)self_cls); + dictptr = (PyObject **)((char *)self_cls + Py_TYPE(self_cls)->tp_dictoffset); DEOPT_IF(dictptr == NULL || *dictptr == NULL, LOAD_METHOD); dict = (PyDictObject *)*dictptr; - assert(PyDict_CheckExact((PyObject *)*dictptr)); + assert(PyDict_CheckExact(dict)); DEOPT_IF(hint >= dict->ma_keys->dk_nentries, LOAD_METHOD); PyDictKeyEntry *ep = DK_ENTRIES(dict->ma_keys) + hint; DEOPT_IF(ep->me_key != name, LOAD_METHOD); diff --git a/Python/specialize.c b/Python/specialize.c index a1c463315a8581..2687746f26781c 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -685,7 +685,7 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, _PyLoadAttrCache *cache1 = &cache[-1].load_attr; PyTypeObject *owner_cls = Py_TYPE(owner); if (PyModule_CheckExact(owner)) { - // somewhat common -- maybe todo. + // Mabe TODO: LOAD_METHOD_MODULE SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "module method"); goto fail; } @@ -707,7 +707,7 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); int owner_has_dict = (owner_dictptr != NULL && *owner_dictptr != NULL && - !PyDict_CheckExact(*owner_dictptr)); + PyDict_CheckExact(*owner_dictptr)); PyDictObject *cls_dict = cls_has_dict ? (PyDictObject *)*cls_dictptr : NULL; PyDictObject *owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; if (owner_cls->tp_dictoffset >= 0 && cls_has_dict) { @@ -743,6 +743,7 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, PyObject *value = NULL; Py_ssize_t hint = _PyDict_GetItemHint(cls_dict, name, -1, &value); if (hint != (uint16_t)hint) { + // possibly classmethod, maybe TODO: LOAD_METHOD_CLASS SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "hint out of range"); goto fail; } From 187fddbc18138f043fe9bc7e001284101064c3a2 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sat, 7 Aug 2021 22:28:34 +0800 Subject: [PATCH 07/40] fix a comment --- Python/specialize.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Python/specialize.c b/Python/specialize.c index 2687746f26781c..ed20c5953c7895 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -714,7 +714,8 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, // We found a type with a __dict__. if ((owner_cls->tp_flags & Py_TPFLAGS_HEAPTYPE) && cls_dict->ma_keys == ((PyHeapTypeObject*)owner_cls)->ht_cached_keys) { - // Keys are shared. Rare for LOAD_METHOD. + // Keys are shared. Rare for LOAD_METHOD. Might change + // when new object layouts are implemented. SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "shared keys"); goto fail; } From c71f142e085d0cf0c0f781b30f7231e780afbf23 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 8 Aug 2021 02:11:32 +0800 Subject: [PATCH 08/40] better attribute detection - fix all tests --- Python/specialize.c | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Python/specialize.c b/Python/specialize.c index ed20c5953c7895..171ecefb183ec1 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -725,14 +725,21 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, uint32_t keys_version = UINT32_MAX; if (owner_has_dict) { // check if its an attr - int is_attr = PyDict_Contains((PyObject *)owner_dict, name); - if (is_attr < 0) { - return -1; - } - if (is_attr) { + PyObject *attr = _PyObject_GenericGetAttrWithDict(owner, name, (PyObject *)owner_dict, 1); + if (attr != descr) { + Py_DECREF(attr); SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "is attr"); goto fail; } + Py_DECREF(attr); + //int is_attr = PyDict_Contains((PyObject *)owner_dict, name); + //if (is_attr < 0) { + // return -1; + //} + //if (is_attr) { + // SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "is attr"); + // goto fail; + //} keys_version = _PyDictKeys_GetVersionForCurrentState(owner_dict); if (keys_version == 0) { SPECIALIZATION_FAIL(LOAD_ATTR, owner_cls, name, From f3082f4de80f122a030671be7e99129ade9d3081 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 8 Aug 2021 02:40:49 +0800 Subject: [PATCH 09/40] always check attr inside instance __dict__ --- Python/ceval.c | 7 ++++++- Python/specialize.c | 18 +++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index ab2fe1112caece..854d235393436d 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4143,6 +4143,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr assert(Py_TYPE(self_cls)->tp_dictoffset > 0); DEOPT_IF(self_cls->tp_version_tag != cache1->tp_version, LOAD_METHOD); + PyObject *name = GETITEM(names, cache0->original_oparg); // inline version of _PyObject_GetDictPtr for offset >= 0 PyObject **dictptr = self_cls->tp_dictoffset != 0 ? (PyObject **) ((char *)self + self_cls->tp_dictoffset) : NULL; @@ -4154,10 +4155,14 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr assert(PyDict_CheckExact(dict)); // printf("%p: %ld %ld\n", self, dict->ma_keys->dk_version, cache1->dk_version_or_hint); DEOPT_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, LOAD_METHOD); + int is_attr = PyDict_Contains((PyObject *)dict, name); + if (is_attr < 0) { + goto error; + } + DEOPT_IF(is_attr, LOAD_METHOD); } // don't care if owner has no dict, could be builtin or __slots__ // look in self_cls.__dict__, avoiding _PyType_Lookup - PyObject *name = GETITEM(names, cache0->original_oparg); uint32_t hint = cache0->index; dictptr = (PyObject **)((char *)self_cls + Py_TYPE(self_cls)->tp_dictoffset); DEOPT_IF(dictptr == NULL || *dictptr == NULL, LOAD_METHOD); diff --git a/Python/specialize.c b/Python/specialize.c index 171ecefb183ec1..16a5decf235ce9 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -724,22 +724,14 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, // instead of old type lookup. So record o.__dict__. uint32_t keys_version = UINT32_MAX; if (owner_has_dict) { - // check if its an attr - PyObject *attr = _PyObject_GenericGetAttrWithDict(owner, name, (PyObject *)owner_dict, 1); - if (attr != descr) { - Py_DECREF(attr); + int is_attr = PyDict_Contains((PyObject *)owner_dict, name); + if (is_attr < 0) { + return -1; + } + if (is_attr) { SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "is attr"); goto fail; } - Py_DECREF(attr); - //int is_attr = PyDict_Contains((PyObject *)owner_dict, name); - //if (is_attr < 0) { - // return -1; - //} - //if (is_attr) { - // SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "is attr"); - // goto fail; - //} keys_version = _PyDictKeys_GetVersionForCurrentState(owner_dict); if (keys_version == 0) { SPECIALIZATION_FAIL(LOAD_ATTR, owner_cls, name, From 0f32d60e2a2b57f91b607fa549e2dd6edafc9e82 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 8 Aug 2021 12:02:48 +0800 Subject: [PATCH 10/40] backoff immediately for LOAD_METHOD, fail on dict subclasses, optimize attr checking --- Include/internal/pycore_code.h | 5 ++++ Python/ceval.c | 49 +++++++++++++++++++++++++--------- Python/specialize.c | 27 ++++++++++++------- 3 files changed, 60 insertions(+), 21 deletions(-) diff --git a/Include/internal/pycore_code.h b/Include/internal/pycore_code.h index e7dac8fbf5f1ea..34dd39354241e6 100644 --- a/Include/internal/pycore_code.h +++ b/Include/internal/pycore_code.h @@ -287,6 +287,11 @@ too_many_cache_misses(_PyAdaptiveEntry *entry) { return entry->counter == saturating_zero(); } +static inline void +set_cache_counter_zero(_PyAdaptiveEntry *entry) { + entry->counter = saturating_zero(); +} + #define ADAPTIVE_CACHE_BACKOFF 64 static inline void diff --git a/Python/ceval.c b/Python/ceval.c index 854d235393436d..4d307d1439f0a5 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1384,7 +1384,10 @@ eval_frame_handle_pending(PyThreadState *tstate) _GetSpecializedCacheEntryForInstruction(first_instr, INSTR_OFFSET(), oparg) +/* Maybe deoptimize if too many cache misses */ #define DEOPT_IF(cond, instname) if (cond) { goto instname ## _miss; } +/* Immediately deoptimize entire instruction back to adaptive entry. */ +#define BACKOFF_IF(cond, instname) if (cond) { goto instname ## _backoff; } #define UPDATE_PREV_INSTR_OPARG(instr, oparg) ((uint8_t*)(instr))[-1] = (oparg) @@ -4141,7 +4144,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr assert(cache1->tp_version != 0); assert(self_cls->tp_dictoffset >= 0); assert(Py_TYPE(self_cls)->tp_dictoffset > 0); - DEOPT_IF(self_cls->tp_version_tag != cache1->tp_version, LOAD_METHOD); + BACKOFF_IF(self_cls->tp_version_tag != cache1->tp_version, LOAD_METHOD); PyObject *name = GETITEM(names, cache0->original_oparg); // inline version of _PyObject_GetDictPtr for offset >= 0 @@ -4153,26 +4156,33 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr if (dictptr != NULL && *dictptr != NULL) { dict = (PyDictObject *)*dictptr; assert(PyDict_CheckExact(dict)); + // some objects support overriding __dict__ with a custom dict subclass + // BACKOFF_IF(!PyDict_CheckExact(dict), LOAD_METHOD); + BACKOFF_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, LOAD_METHOD); + // printf("%p: %ld %ld\n", self, dict->ma_keys->dk_version, cache1->dk_version_or_hint); - DEOPT_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, LOAD_METHOD); - int is_attr = PyDict_Contains((PyObject *)dict, name); - if (is_attr < 0) { - goto error; - } - DEOPT_IF(is_attr, LOAD_METHOD); + //int is_attr = PyDict_Contains((PyObject *)dict, name); + //if (is_attr < 0) { + // goto error; + //} + //if (is_attr) { + // printf("%p: %ld %ld, has_split: %d\n", self_cls, dict->ma_keys->dk_version, cache1->dk_version_or_hint, _PyDict_HasSplitTable(dict)); + // PyObject_Print((PyObject *)dict, stderr, Py_PRINT_RAW); + // BACKOFF_IF(is_attr, LOAD_METHOD); + //} } // don't care if owner has no dict, could be builtin or __slots__ // look in self_cls.__dict__, avoiding _PyType_Lookup uint32_t hint = cache0->index; dictptr = (PyObject **)((char *)self_cls + Py_TYPE(self_cls)->tp_dictoffset); - DEOPT_IF(dictptr == NULL || *dictptr == NULL, LOAD_METHOD); + BACKOFF_IF(dictptr == NULL || *dictptr == NULL, LOAD_METHOD); dict = (PyDictObject *)*dictptr; assert(PyDict_CheckExact(dict)); - DEOPT_IF(hint >= dict->ma_keys->dk_nentries, LOAD_METHOD); + BACKOFF_IF(hint >= dict->ma_keys->dk_nentries, LOAD_METHOD); PyDictKeyEntry *ep = DK_ENTRIES(dict->ma_keys) + hint; - DEOPT_IF(ep->me_key != name, LOAD_METHOD); + BACKOFF_IF(ep->me_key != name, LOAD_METHOD); res = ep->me_value; - DEOPT_IF( + BACKOFF_IF( res == NULL || Py_TYPE(res)->tp_descr_get == NULL || !_PyType_HasFeature(Py_TYPE(res), Py_TPFLAGS_METHOD_DESCRIPTOR), @@ -4508,9 +4518,24 @@ opname ## _miss: \ JUMP_TO_INSTRUCTION(opname); \ } +#define BACKOFF_WITH_CACHE(opname) \ +opname ## _backoff: \ + { \ + STAT_INC(opname, miss); \ + _PyAdaptiveEntry *cache = &GET_CACHE()->adaptive; \ + record_cache_miss(cache); \ + set_cache_counter_zero(cache); \ + next_instr[-1] = _Py_MAKECODEUNIT(opname ## _ADAPTIVE, _Py_OPARG(next_instr[-1])); \ + STAT_INC(opname, deopt); \ + cache_backoff(cache); \ + oparg = cache->original_oparg; \ + STAT_DEC(opname, unquickened); \ + JUMP_TO_INSTRUCTION(opname); \ + } \ + MISS_WITH_CACHE(LOAD_ATTR) MISS_WITH_CACHE(LOAD_GLOBAL) -MISS_WITH_CACHE(LOAD_METHOD) +BACKOFF_WITH_CACHE(LOAD_METHOD) MISS_WITH_OPARG_COUNTER(BINARY_SUBSCR) binary_subscr_dict_error: diff --git a/Python/specialize.c b/Python/specialize.c index 16a5decf235ce9..615ec754a4f987 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -705,18 +705,17 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, *cls_dictptr != NULL && PyDict_CheckExact(*cls_dictptr)); PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); - int owner_has_dict = (owner_dictptr != NULL && - *owner_dictptr != NULL && - PyDict_CheckExact(*owner_dictptr)); + int owner_has_dict = (owner_dictptr != NULL && *owner_dictptr != NULL); PyDictObject *cls_dict = cls_has_dict ? (PyDictObject *)*cls_dictptr : NULL; PyDictObject *owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; - if (owner_cls->tp_dictoffset >= 0 && cls_has_dict) { + if (Py_TYPE(owner_cls)->tp_dictoffset >= 0 && cls_has_dict && + owner_cls->tp_dictoffset >= 0) { // We found a type with a __dict__. if ((owner_cls->tp_flags & Py_TPFLAGS_HEAPTYPE) && cls_dict->ma_keys == ((PyHeapTypeObject*)owner_cls)->ht_cached_keys) { // Keys are shared. Rare for LOAD_METHOD. Might change // when new object layouts are implemented. - SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "shared keys"); + SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "type dict shared keys"); goto fail; } @@ -724,14 +723,24 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, // instead of old type lookup. So record o.__dict__. uint32_t keys_version = UINT32_MAX; if (owner_has_dict) { - int is_attr = PyDict_Contains((PyObject *)owner_dict, name); - if (is_attr < 0) { + if (!PyDict_CheckExact(owner_dict)) { + // _PyDictKeys_GetVersionForCurrentState isn't accurate for + // custom dict subclasses at the moment + SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "dict subclass"); + goto fail; + } + assert(PyUnicode_CheckExact(name)); + Py_hash_t hash = PyObject_Hash(name); + if (hash == -1) { return -1; } - if (is_attr) { + PyObject *value = NULL; + Py_ssize_t ix = _Py_dict_lookup(owner_dict, name, hash, &value); + assert(ix != DKIX_ERROR); + if (ix != DKIX_EMPTY) { SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "is attr"); goto fail; - } + } keys_version = _PyDictKeys_GetVersionForCurrentState(owner_dict); if (keys_version == 0) { SPECIALIZATION_FAIL(LOAD_ATTR, owner_cls, name, From 8a31db3bf5d3b9de8a742a0e45b4d6490b7abfa3 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Mon, 9 Aug 2021 22:34:23 +0800 Subject: [PATCH 11/40] Improve comments and specialization stats --- Python/ceval.c | 13 ------------- Python/specialize.c | 11 ++++++++++- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index 4d307d1439f0a5..c3d19bb9e98ac3 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4156,20 +4156,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr if (dictptr != NULL && *dictptr != NULL) { dict = (PyDictObject *)*dictptr; assert(PyDict_CheckExact(dict)); - // some objects support overriding __dict__ with a custom dict subclass - // BACKOFF_IF(!PyDict_CheckExact(dict), LOAD_METHOD); BACKOFF_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, LOAD_METHOD); - - // printf("%p: %ld %ld\n", self, dict->ma_keys->dk_version, cache1->dk_version_or_hint); - //int is_attr = PyDict_Contains((PyObject *)dict, name); - //if (is_attr < 0) { - // goto error; - //} - //if (is_attr) { - // printf("%p: %ld %ld, has_split: %d\n", self_cls, dict->ma_keys->dk_version, cache1->dk_version_or_hint, _PyDict_HasSplitTable(dict)); - // PyObject_Print((PyObject *)dict, stderr, Py_PRINT_RAW); - // BACKOFF_IF(is_attr, LOAD_METHOD); - //} } // don't care if owner has no dict, could be builtin or __slots__ // look in self_cls.__dict__, avoiding _PyType_Lookup diff --git a/Python/specialize.c b/Python/specialize.c index 615ec754a4f987..170be4da71ce05 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -697,7 +697,15 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, PyObject *descr = NULL; DesciptorClassification kind = analyze_descriptor(owner_cls, name, &descr); if (kind != METHOD || descr == NULL) { +#if SPECIALIZATION_STATS + if (kind == GETATTRIBUTE_OVERRIDDEN && + PyObject_TypeCheck(owner, &PyBaseObject_Type)) { + // Maybe TODO: LOAD_METHOD_CLASS + SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "class method"); + goto fail; + } SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "not a method"); +#endif goto fail; } PyObject **cls_dictptr = _PyObject_GetDictPtr((PyObject *)owner_cls); @@ -752,7 +760,8 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, PyObject *value = NULL; Py_ssize_t hint = _PyDict_GetItemHint(cls_dict, name, -1, &value); if (hint != (uint16_t)hint) { - // possibly classmethod, maybe TODO: LOAD_METHOD_CLASS + // Happens when the method is inherited. TODO: make this work with inherited + // methods. SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "hint out of range"); goto fail; } From 608f6d04028bcb2333c8f9c0b96680d2da5bb5ba Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Tue, 10 Aug 2021 22:06:45 +0800 Subject: [PATCH 12/40] regen opcode --- Include/opcode.h | 10 ++++++---- Python/opcode_targets.h | 6 +----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Include/opcode.h b/Include/opcode.h index 50b783289590da..8245457a747f3d 100644 --- a/Include/opcode.h +++ b/Include/opcode.h @@ -149,10 +149,12 @@ extern "C" { #define LOAD_GLOBAL_ADAPTIVE 41 #define LOAD_GLOBAL_MODULE 42 #define LOAD_GLOBAL_BUILTIN 43 -#define STORE_ATTR_ADAPTIVE 44 -#define STORE_ATTR_SPLIT_KEYS 45 -#define STORE_ATTR_SLOT 46 -#define STORE_ATTR_WITH_HINT 47 +#define LOAD_METHOD_ADAPTIVE 44 +#define LOAD_METHOD_WITH_HINT 45 +#define STORE_ATTR_ADAPTIVE 46 +#define STORE_ATTR_SPLIT_KEYS 47 +#define STORE_ATTR_SLOT 48 +#define STORE_ATTR_WITH_HINT 58 #ifdef NEED_OPCODE_JUMP_TABLES static uint32_t _PyOpcode_RelativeJump[8] = { 0U, diff --git a/Python/opcode_targets.h b/Python/opcode_targets.h index 13ae1efb5b356b..15884635695413 100644 --- a/Python/opcode_targets.h +++ b/Python/opcode_targets.h @@ -45,13 +45,9 @@ static void *opcode_targets[256] = { &&TARGET_LOAD_GLOBAL_BUILTIN, &&TARGET_LOAD_METHOD_ADAPTIVE, &&TARGET_LOAD_METHOD_WITH_HINT, - &&_unknown_opcode, - &&_unknown_opcode, &&TARGET_STORE_ATTR_ADAPTIVE, &&TARGET_STORE_ATTR_SPLIT_KEYS, &&TARGET_STORE_ATTR_SLOT, - &&TARGET_STORE_ATTR_WITH_HINT, - &&_unknown_opcode, &&TARGET_WITH_EXCEPT_START, &&TARGET_GET_AITER, &&TARGET_GET_ANEXT, @@ -61,7 +57,7 @@ static void *opcode_targets[256] = { &&TARGET_INPLACE_ADD, &&TARGET_INPLACE_SUBTRACT, &&TARGET_INPLACE_MULTIPLY, - &&_unknown_opcode, + &&TARGET_STORE_ATTR_WITH_HINT, &&TARGET_INPLACE_MODULO, &&TARGET_STORE_SUBSCR, &&TARGET_DELETE_SUBSCR, From 1f5e64a0b3558e674a9fa2ee7bdfe150ed5f0b5f Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Tue, 10 Aug 2021 22:47:25 +0800 Subject: [PATCH 13/40] simplify code, use new spec_fail stats style --- Python/ceval.c | 2 +- Python/specialize.c | 35 +++++++++++++++++------------------ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index daee119a2213ce..d321ac2e4476f7 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4256,7 +4256,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr PyObject *res; SpecializedCacheEntry *caches = GET_CACHE(); _PyAdaptiveEntry *cache0 = &caches[0].adaptive; - _PyLoadAttrCache *cache1 = &caches[-1].load_attr; + _PyAttrCache *cache1 = &caches[-1].attr; assert(cache1->dk_version_or_hint != 0); assert(cache1->tp_version != 0); diff --git a/Python/specialize.c b/Python/specialize.c index 142ed4ce7522fc..0ae987b8b2666f 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -400,6 +400,14 @@ _Py_Quicken(PyCodeObject *code) { #define SPEC_FAIL_READ_ONLY 15 #define SPEC_FAIL_AUDITED_SLOT 16 +/* Methods */ + +#define SPEC_FAIL_IS_ATTR 15 +#define SPEC_FAIL_DICT_SUBCLASS 16 +#define SPEC_FAIL_MODULE_METHOD 17 +#define SPEC_FAIL_CLASS_METHOD 18 +#define SPEC_FAIL_NOT_METHOD 19 + /* Binary subscr */ #define SPEC_FAIL_LIST_NON_INT_SUBSCRIPT 8 @@ -785,11 +793,11 @@ int _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SpecializedCacheEntry *cache) { _PyAdaptiveEntry *cache0 = &cache->adaptive; - _PyLoadAttrCache *cache1 = &cache[-1].load_attr; + _PyAttrCache *cache1 = &cache[-1].attr; PyTypeObject *owner_cls = Py_TYPE(owner); if (PyModule_CheckExact(owner)) { // Mabe TODO: LOAD_METHOD_MODULE - SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "module method"); + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_MODULE_METHOD); goto fail; } if (owner_cls->tp_dict == NULL) { @@ -798,16 +806,16 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, } } PyObject *descr = NULL; - DesciptorClassification kind = analyze_descriptor(owner_cls, name, &descr); + DesciptorClassification kind = analyze_descriptor(owner_cls, name, &descr, 0); if (kind != METHOD || descr == NULL) { #if SPECIALIZATION_STATS if (kind == GETATTRIBUTE_OVERRIDDEN && PyObject_TypeCheck(owner, &PyBaseObject_Type)) { // Maybe TODO: LOAD_METHOD_CLASS - SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "class method"); + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_CLASS_METHOD); goto fail; } - SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "not a method"); + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NOT_METHOD); #endif goto fail; } @@ -821,14 +829,6 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, PyDictObject *owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; if (Py_TYPE(owner_cls)->tp_dictoffset >= 0 && cls_has_dict && owner_cls->tp_dictoffset >= 0) { - // We found a type with a __dict__. - if ((owner_cls->tp_flags & Py_TPFLAGS_HEAPTYPE) - && cls_dict->ma_keys == ((PyHeapTypeObject*)owner_cls)->ht_cached_keys) { - // Keys are shared. Rare for LOAD_METHOD. Might change - // when new object layouts are implemented. - SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "type dict shared keys"); - goto fail; - } // if o.__dict__ changes, the method might be found in o.__dict__ // instead of old type lookup. So record o.__dict__. @@ -837,7 +837,7 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, if (!PyDict_CheckExact(owner_dict)) { // _PyDictKeys_GetVersionForCurrentState isn't accurate for // custom dict subclasses at the moment - SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "dict subclass"); + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_DICT_SUBCLASS); goto fail; } assert(PyUnicode_CheckExact(name)); @@ -849,13 +849,12 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, Py_ssize_t ix = _Py_dict_lookup(owner_dict, name, hash, &value); assert(ix != DKIX_ERROR); if (ix != DKIX_EMPTY) { - SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "is attr"); + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_IS_ATTR); goto fail; } keys_version = _PyDictKeys_GetVersionForCurrentState(owner_dict); if (keys_version == 0) { - SPECIALIZATION_FAIL(LOAD_ATTR, owner_cls, name, - "no more key versions"); + SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OUT_OF_VERSIONS); goto fail; } } // else owner is maybe a builtin with no dict, or __slots__ @@ -865,7 +864,7 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, if (hint != (uint16_t)hint) { // Happens when the method is inherited. TODO: make this work with inherited // methods. - SPECIALIZATION_FAIL(LOAD_METHOD, owner_cls, name, "hint out of range"); + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_OUT_OF_RANGE); goto fail; } From 29daf446c638448c8d3f4527bd70631fb71265a0 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Tue, 10 Aug 2021 23:59:49 +0800 Subject: [PATCH 14/40] LOAD_METHOD_WITH_HINT now supports inherited methods --- Include/internal/pycore_code.h | 6 ++++++ Python/ceval.c | 27 ++++++++++----------------- Python/specialize.c | 34 ++++++++++++++++------------------ 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/Include/internal/pycore_code.h b/Include/internal/pycore_code.h index 529cc5c9033a8b..30465eb7a6f30c 100644 --- a/Include/internal/pycore_code.h +++ b/Include/internal/pycore_code.h @@ -30,6 +30,11 @@ typedef struct { uint32_t builtin_keys_version; } _PyLoadGlobalCache; +typedef union { + _PyAttrCache attr; + PyObject *meth; /* borrowed */ +} _PyLoadMethodCache; + /* Add specialized versions of entries to this union. * * Do not break the invariant: sizeof(SpecializedCacheEntry) == 8 @@ -45,6 +50,7 @@ typedef union { _PyAdaptiveEntry adaptive; _PyAttrCache attr; _PyLoadGlobalCache load_global; + _PyLoadMethodCache load_method; } SpecializedCacheEntry; #define INSTRUCTIONS_PER_ENTRY (sizeof(SpecializedCacheEntry)/sizeof(_Py_CODEUNIT)) diff --git a/Python/ceval.c b/Python/ceval.c index d321ac2e4476f7..935c44c57ab321 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4253,16 +4253,17 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr assert(cframe.use_tracing == 0); PyObject *self = TOP(); PyTypeObject *self_cls = Py_TYPE(self); - PyObject *res; + PyObject *res = NULL; SpecializedCacheEntry *caches = GET_CACHE(); _PyAdaptiveEntry *cache0 = &caches[0].adaptive; - _PyAttrCache *cache1 = &caches[-1].attr; - - assert(cache1->dk_version_or_hint != 0); - assert(cache1->tp_version != 0); + _PyLoadMethodCache *cache1 = &caches[-1].load_method; + _PyLoadMethodCache *cache2 = &caches[-2].load_method; + + assert(cache1->attr.dk_version_or_hint != 0); + assert(cache1->attr.tp_version != 0); assert(self_cls->tp_dictoffset >= 0); assert(Py_TYPE(self_cls)->tp_dictoffset > 0); - BACKOFF_IF(self_cls->tp_version_tag != cache1->tp_version, LOAD_METHOD); + BACKOFF_IF(self_cls->tp_version_tag != cache1->attr.tp_version, LOAD_METHOD); PyObject *name = GETITEM(names, cache0->original_oparg); // inline version of _PyObject_GetDictPtr for offset >= 0 @@ -4274,19 +4275,11 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr if (dictptr != NULL && *dictptr != NULL) { dict = (PyDictObject *)*dictptr; assert(PyDict_CheckExact(dict)); - BACKOFF_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, LOAD_METHOD); + BACKOFF_IF(dict->ma_keys->dk_version != + cache1->attr.dk_version_or_hint, LOAD_METHOD); } // don't care if owner has no dict, could be builtin or __slots__ - // look in self_cls.__dict__, avoiding _PyType_Lookup - uint32_t hint = cache0->index; - dictptr = (PyObject **)((char *)self_cls + Py_TYPE(self_cls)->tp_dictoffset); - BACKOFF_IF(dictptr == NULL || *dictptr == NULL, LOAD_METHOD); - dict = (PyDictObject *)*dictptr; - assert(PyDict_CheckExact(dict)); - BACKOFF_IF(hint >= dict->ma_keys->dk_nentries, LOAD_METHOD); - PyDictKeyEntry *ep = DK_ENTRIES(dict->ma_keys) + hint; - BACKOFF_IF(ep->me_key != name, LOAD_METHOD); - res = ep->me_value; + res = cache2->meth; BACKOFF_IF( res == NULL || Py_TYPE(res)->tp_descr_get == NULL || diff --git a/Python/specialize.c b/Python/specialize.c index 0ae987b8b2666f..e9d19ead9a4617 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -232,7 +232,7 @@ static uint8_t adaptive_opcodes[256] = { static uint8_t cache_requirements[256] = { [LOAD_ATTR] = 2, /* _PyAdaptiveEntry and _PyAttrCache */ [LOAD_GLOBAL] = 2, /* _PyAdaptiveEntry and _PyLoadGlobalCache */ - [LOAD_METHOD] = 2, + [LOAD_METHOD] = 3, /* _PyAdaptiveEntry and 2 _PyLoadMethodCache */ [BINARY_SUBSCR] = 0, [STORE_ATTR] = 2, /* _PyAdaptiveEntry and _PyAttrCache */ }; @@ -793,7 +793,9 @@ int _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SpecializedCacheEntry *cache) { _PyAdaptiveEntry *cache0 = &cache->adaptive; - _PyAttrCache *cache1 = &cache[-1].attr; + _PyLoadMethodCache *cache1 = &cache[-1].load_method; + _PyLoadMethodCache *cache2 = &cache[-2].load_method; + PyTypeObject *owner_cls = Py_TYPE(owner); if (PyModule_CheckExact(owner)) { // Mabe TODO: LOAD_METHOD_MODULE @@ -809,7 +811,7 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, DesciptorClassification kind = analyze_descriptor(owner_cls, name, &descr, 0); if (kind != METHOD || descr == NULL) { #if SPECIALIZATION_STATS - if (kind == GETATTRIBUTE_OVERRIDDEN && + if (kind == GETSET_OVERRIDDEN && PyObject_TypeCheck(owner, &PyBaseObject_Type)) { // Maybe TODO: LOAD_METHOD_CLASS SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_CLASS_METHOD); @@ -823,11 +825,16 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, int cls_has_dict = (cls_dictptr != NULL && *cls_dictptr != NULL && PyDict_CheckExact(*cls_dictptr)); + if (!cls_has_dict) { + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NO_DICT); + goto fail; + } PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); int owner_has_dict = (owner_dictptr != NULL && *owner_dictptr != NULL); - PyDictObject *cls_dict = cls_has_dict ? (PyDictObject *)*cls_dictptr : NULL; + PyDictObject *cls_dict = (PyDictObject *)*cls_dictptr; PyDictObject *owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; - if (Py_TYPE(owner_cls)->tp_dictoffset >= 0 && cls_has_dict && + if (Py_TYPE(owner_cls)->tp_dictoffset >= 0 && + cls_has_dict && owner_cls->tp_dictoffset >= 0) { // if o.__dict__ changes, the method might be found in o.__dict__ @@ -859,19 +866,10 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, } } // else owner is maybe a builtin with no dict, or __slots__ - PyObject *value = NULL; - Py_ssize_t hint = _PyDict_GetItemHint(cls_dict, name, -1, &value); - if (hint != (uint16_t)hint) { - // Happens when the method is inherited. TODO: make this work with inherited - // methods. - SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_OUT_OF_RANGE); - goto fail; - } - - // printf("owner is %p with %ld\n", owner, keys_version); - cache0->index = (uint16_t)hint; - cache1->dk_version_or_hint = keys_version; - cache1->tp_version = owner_cls->tp_version_tag; + // Borrowed. Check tp_version_tag before accessing in case it's deleted. + cache2->meth = descr; + cache1->attr.dk_version_or_hint = keys_version; + cache1->attr.tp_version = owner_cls->tp_version_tag; *instr = _Py_MAKECODEUNIT(LOAD_METHOD_WITH_HINT, _Py_OPARG(*instr)); goto success; From a3d6dd34a147520c7090a9694867e7bc785ee67f Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 11 Aug 2021 00:21:07 +0800 Subject: [PATCH 15/40] remove unneeded deopt --- Python/ceval.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index 935c44c57ab321..d9c8c0668598a6 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4280,11 +4280,8 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr } // don't care if owner has no dict, could be builtin or __slots__ res = cache2->meth; - BACKOFF_IF( - res == NULL || - Py_TYPE(res)->tp_descr_get == NULL || - !_PyType_HasFeature(Py_TYPE(res), Py_TPFLAGS_METHOD_DESCRIPTOR), - LOAD_METHOD); + assert(res != NULL); + assert(_PyType_HasFeature(Py_TYPE(res), Py_TPFLAGS_METHOD_DESCRIPTOR)); STAT_INC(LOAD_METHOD, hit); record_cache_hit(cache0); Py_INCREF(res); From d7ee4ac7af95ad9c9abe8ee645403881f896bde4 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 11 Aug 2021 17:21:42 +0800 Subject: [PATCH 16/40] remove experiments, rename to LOAD_METHOD_CACHED --- Include/internal/pycore_code.h | 5 ----- Include/opcode.h | 2 +- Lib/opcode.py | 2 +- Python/ceval.c | 29 ++++++----------------------- Python/opcode_targets.h | 2 +- Python/specialize.c | 2 +- 6 files changed, 10 insertions(+), 32 deletions(-) diff --git a/Include/internal/pycore_code.h b/Include/internal/pycore_code.h index 30465eb7a6f30c..3ed89ffefa23a2 100644 --- a/Include/internal/pycore_code.h +++ b/Include/internal/pycore_code.h @@ -293,11 +293,6 @@ too_many_cache_misses(_PyAdaptiveEntry *entry) { return entry->counter == saturating_zero(); } -static inline void -set_cache_counter_zero(_PyAdaptiveEntry *entry) { - entry->counter = saturating_zero(); -} - #define ADAPTIVE_CACHE_BACKOFF 64 static inline void diff --git a/Include/opcode.h b/Include/opcode.h index 8245457a747f3d..b3fee47cbac354 100644 --- a/Include/opcode.h +++ b/Include/opcode.h @@ -150,7 +150,7 @@ extern "C" { #define LOAD_GLOBAL_MODULE 42 #define LOAD_GLOBAL_BUILTIN 43 #define LOAD_METHOD_ADAPTIVE 44 -#define LOAD_METHOD_WITH_HINT 45 +#define LOAD_METHOD_CACHED 45 #define STORE_ATTR_ADAPTIVE 46 #define STORE_ATTR_SPLIT_KEYS 47 #define STORE_ATTR_SLOT 48 diff --git a/Lib/opcode.py b/Lib/opcode.py index 1490898ca94ced..66091a80708157 100644 --- a/Lib/opcode.py +++ b/Lib/opcode.py @@ -234,7 +234,7 @@ def jabs_op(name, op): "LOAD_GLOBAL_MODULE", "LOAD_GLOBAL_BUILTIN", "LOAD_METHOD_ADAPTIVE", - "LOAD_METHOD_WITH_HINT", + "LOAD_METHOD_CACHED", "STORE_ATTR_ADAPTIVE", "STORE_ATTR_SPLIT_KEYS", "STORE_ATTR_SLOT", diff --git a/Python/ceval.c b/Python/ceval.c index d9c8c0668598a6..0a98236dc7e9e9 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1384,10 +1384,7 @@ eval_frame_handle_pending(PyThreadState *tstate) _GetSpecializedCacheEntryForInstruction(first_instr, INSTR_OFFSET(), oparg) -/* Maybe deoptimize if too many cache misses */ #define DEOPT_IF(cond, instname) if (cond) { goto instname ## _miss; } -/* Immediately deoptimize entire instruction back to adaptive entry. */ -#define BACKOFF_IF(cond, instname) if (cond) { goto instname ## _backoff; } #define UPDATE_PREV_INSTR_OPARG(instr, oparg) ((uint8_t*)(instr))[-1] = (oparg) @@ -4191,11 +4188,11 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr case TARGET(LOAD_METHOD): { PREDICTED(LOAD_METHOD); + STAT_INC(LOAD_METHOD, unquickened); /* Designed to work in tandem with CALL_METHOD. */ PyObject *name = GETITEM(names, oparg); PyObject *obj = TOP(); PyObject *meth = NULL; - STAT_INC(LOAD_METHOD, unquickened); int meth_found = _PyObject_GetMethod(obj, name, &meth); @@ -4249,7 +4246,8 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr } } - case TARGET(LOAD_METHOD_WITH_HINT): { + case TARGET(LOAD_METHOD_CACHED): { + /* Designed to work in tandem with CALL_METHOD. */ assert(cframe.use_tracing == 0); PyObject *self = TOP(); PyTypeObject *self_cls = Py_TYPE(self); @@ -4263,7 +4261,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr assert(cache1->attr.tp_version != 0); assert(self_cls->tp_dictoffset >= 0); assert(Py_TYPE(self_cls)->tp_dictoffset > 0); - BACKOFF_IF(self_cls->tp_version_tag != cache1->attr.tp_version, LOAD_METHOD); + DEOPT_IF(self_cls->tp_version_tag != cache1->attr.tp_version, LOAD_METHOD); PyObject *name = GETITEM(names, cache0->original_oparg); // inline version of _PyObject_GetDictPtr for offset >= 0 @@ -4275,7 +4273,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr if (dictptr != NULL && *dictptr != NULL) { dict = (PyDictObject *)*dictptr; assert(PyDict_CheckExact(dict)); - BACKOFF_IF(dict->ma_keys->dk_version != + DEOPT_IF(dict->ma_keys->dk_version != cache1->attr.dk_version_or_hint, LOAD_METHOD); } // don't care if owner has no dict, could be builtin or __slots__ @@ -4613,25 +4611,10 @@ opname ## _miss: \ JUMP_TO_INSTRUCTION(opname); \ } -#define BACKOFF_WITH_CACHE(opname) \ -opname ## _backoff: \ - { \ - STAT_INC(opname, miss); \ - _PyAdaptiveEntry *cache = &GET_CACHE()->adaptive; \ - record_cache_miss(cache); \ - set_cache_counter_zero(cache); \ - next_instr[-1] = _Py_MAKECODEUNIT(opname ## _ADAPTIVE, _Py_OPARG(next_instr[-1])); \ - STAT_INC(opname, deopt); \ - cache_backoff(cache); \ - oparg = cache->original_oparg; \ - STAT_DEC(opname, unquickened); \ - JUMP_TO_INSTRUCTION(opname); \ - } \ - MISS_WITH_CACHE(LOAD_ATTR) MISS_WITH_CACHE(STORE_ATTR) MISS_WITH_CACHE(LOAD_GLOBAL) -BACKOFF_WITH_CACHE(LOAD_METHOD) +MISS_WITH_CACHE(LOAD_METHOD) MISS_WITH_OPARG_COUNTER(BINARY_SUBSCR) binary_subscr_dict_error: diff --git a/Python/opcode_targets.h b/Python/opcode_targets.h index 15884635695413..d73e2d6bce543a 100644 --- a/Python/opcode_targets.h +++ b/Python/opcode_targets.h @@ -44,7 +44,7 @@ static void *opcode_targets[256] = { &&TARGET_LOAD_GLOBAL_MODULE, &&TARGET_LOAD_GLOBAL_BUILTIN, &&TARGET_LOAD_METHOD_ADAPTIVE, - &&TARGET_LOAD_METHOD_WITH_HINT, + &&TARGET_LOAD_METHOD_CACHED, &&TARGET_STORE_ATTR_ADAPTIVE, &&TARGET_STORE_ATTR_SPLIT_KEYS, &&TARGET_STORE_ATTR_SLOT, diff --git a/Python/specialize.c b/Python/specialize.c index e9d19ead9a4617..a75da362745279 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -870,7 +870,7 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, cache2->meth = descr; cache1->attr.dk_version_or_hint = keys_version; cache1->attr.tp_version = owner_cls->tp_version_tag; - *instr = _Py_MAKECODEUNIT(LOAD_METHOD_WITH_HINT, _Py_OPARG(*instr)); + *instr = _Py_MAKECODEUNIT(LOAD_METHOD_CACHED, _Py_OPARG(*instr)); goto success; } From c944af09bfa2c3ea2a900ad45586037c2ee72adf Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 11 Aug 2021 17:36:46 +0800 Subject: [PATCH 17/40] move deopt earlier --- Python/ceval.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/ceval.c b/Python/ceval.c index 0a98236dc7e9e9..1bbca36d13dc07 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4257,11 +4257,11 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr _PyLoadMethodCache *cache1 = &caches[-1].load_method; _PyLoadMethodCache *cache2 = &caches[-2].load_method; + DEOPT_IF(self_cls->tp_version_tag != cache1->attr.tp_version, LOAD_METHOD); assert(cache1->attr.dk_version_or_hint != 0); assert(cache1->attr.tp_version != 0); assert(self_cls->tp_dictoffset >= 0); assert(Py_TYPE(self_cls)->tp_dictoffset > 0); - DEOPT_IF(self_cls->tp_version_tag != cache1->attr.tp_version, LOAD_METHOD); PyObject *name = GETITEM(names, cache0->original_oparg); // inline version of _PyObject_GetDictPtr for offset >= 0 From 2f5cc63e3333eb346ede69b34125899ffda88872 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 11 Aug 2021 20:37:40 +0800 Subject: [PATCH 18/40] Apply Mark's suggestions, fix up comments --- Include/internal/pycore_code.h | 9 ++++---- Python/ceval.c | 38 +++++++++++++++------------------- Python/specialize.c | 20 +++++++++--------- 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/Include/internal/pycore_code.h b/Include/internal/pycore_code.h index 3ed89ffefa23a2..160a3837e5843d 100644 --- a/Include/internal/pycore_code.h +++ b/Include/internal/pycore_code.h @@ -30,10 +30,9 @@ typedef struct { uint32_t builtin_keys_version; } _PyLoadGlobalCache; -typedef union { - _PyAttrCache attr; - PyObject *meth; /* borrowed */ -} _PyLoadMethodCache; +typedef struct { + PyObject *obj; /* borrowed */ +} _PyObjectCache; /* Add specialized versions of entries to this union. * @@ -50,7 +49,7 @@ typedef union { _PyAdaptiveEntry adaptive; _PyAttrCache attr; _PyLoadGlobalCache load_global; - _PyLoadMethodCache load_method; + _PyObjectCache obj; } SpecializedCacheEntry; #define INSTRUCTIONS_PER_ENTRY (sizeof(SpecializedCacheEntry)/sizeof(_Py_CODEUNIT)) diff --git a/Python/ceval.c b/Python/ceval.c index 1bbca36d13dc07..a2957aa1bc2613 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4254,34 +4254,30 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr PyObject *res = NULL; SpecializedCacheEntry *caches = GET_CACHE(); _PyAdaptiveEntry *cache0 = &caches[0].adaptive; - _PyLoadMethodCache *cache1 = &caches[-1].load_method; - _PyLoadMethodCache *cache2 = &caches[-2].load_method; + _PyAttrCache *cache1 = &caches[-1].attr; + _PyObjectCache *cache2 = &caches[-2].obj; - DEOPT_IF(self_cls->tp_version_tag != cache1->attr.tp_version, LOAD_METHOD); - assert(cache1->attr.dk_version_or_hint != 0); - assert(cache1->attr.tp_version != 0); + DEOPT_IF(self_cls->tp_version_tag != cache1->tp_version, LOAD_METHOD); + assert(cache1->dk_version_or_hint != 0); + assert(cache1->tp_version != 0); assert(self_cls->tp_dictoffset >= 0); assert(Py_TYPE(self_cls)->tp_dictoffset > 0); - PyObject *name = GETITEM(names, cache0->original_oparg); // inline version of _PyObject_GetDictPtr for offset >= 0 - PyObject **dictptr = self_cls->tp_dictoffset != 0 ? - (PyObject **) ((char *)self + self_cls->tp_dictoffset) : NULL; - PyDictObject *dict = NULL; - - // ensure self.__dict__ didn't modify keys - if (dictptr != NULL && *dictptr != NULL) { - dict = (PyDictObject *)*dictptr; - assert(PyDict_CheckExact(dict)); - DEOPT_IF(dict->ma_keys->dk_version != - cache1->attr.dk_version_or_hint, LOAD_METHOD); - } // don't care if owner has no dict, could be builtin or __slots__ - - res = cache2->meth; - assert(res != NULL); - assert(_PyType_HasFeature(Py_TYPE(res), Py_TPFLAGS_METHOD_DESCRIPTOR)); + PyObject *dict = self_cls->tp_dictoffset != 0 ? + *(PyObject **) ((char *)self + self_cls->tp_dictoffset) : NULL; + + // Ensure self.__dict__ didn't modify keys. + // Don't care if self has no dict, it could be builtin or __slots__. + DEOPT_IF(dict != NULL && + ((PyDictObject *)dict)->ma_keys->dk_version != + cache1->dk_version_or_hint, LOAD_METHOD); + STAT_INC(LOAD_METHOD, hit); record_cache_hit(cache0); + res = cache2->obj; + assert(res != NULL); + assert(_PyType_HasFeature(Py_TYPE(res), Py_TPFLAGS_METHOD_DESCRIPTOR)); Py_INCREF(res); SET_TOP(res); PUSH(self); diff --git a/Python/specialize.c b/Python/specialize.c index a75da362745279..43c81167a496f1 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -232,7 +232,7 @@ static uint8_t adaptive_opcodes[256] = { static uint8_t cache_requirements[256] = { [LOAD_ATTR] = 2, /* _PyAdaptiveEntry and _PyAttrCache */ [LOAD_GLOBAL] = 2, /* _PyAdaptiveEntry and _PyLoadGlobalCache */ - [LOAD_METHOD] = 3, /* _PyAdaptiveEntry and 2 _PyLoadMethodCache */ + [LOAD_METHOD] = 3, /* _PyAdaptiveEntry, _PyAttrCache and _PyObjectCache */ [BINARY_SUBSCR] = 0, [STORE_ATTR] = 2, /* _PyAdaptiveEntry and _PyAttrCache */ }; @@ -793,8 +793,8 @@ int _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SpecializedCacheEntry *cache) { _PyAdaptiveEntry *cache0 = &cache->adaptive; - _PyLoadMethodCache *cache1 = &cache[-1].load_method; - _PyLoadMethodCache *cache2 = &cache[-2].load_method; + _PyAttrCache *cache1 = &cache[-1].attr; + _PyObjectCache *cache2 = &cache[-2].obj; PyTypeObject *owner_cls = Py_TYPE(owner); if (PyModule_CheckExact(owner)) { @@ -837,13 +837,13 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, cls_has_dict && owner_cls->tp_dictoffset >= 0) { - // if o.__dict__ changes, the method might be found in o.__dict__ - // instead of old type lookup. So record o.__dict__. + // If o.__dict__ changes, the method might be found in o.__dict__ + // instead of old type lookup. So record o.__dict__'s keys. uint32_t keys_version = UINT32_MAX; if (owner_has_dict) { + // _PyDictKeys_GetVersionForCurrentState isn't accurate for + // custom dict subclasses at the moment. if (!PyDict_CheckExact(owner_dict)) { - // _PyDictKeys_GetVersionForCurrentState isn't accurate for - // custom dict subclasses at the moment SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_DICT_SUBCLASS); goto fail; } @@ -867,9 +867,9 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, } // else owner is maybe a builtin with no dict, or __slots__ // Borrowed. Check tp_version_tag before accessing in case it's deleted. - cache2->meth = descr; - cache1->attr.dk_version_or_hint = keys_version; - cache1->attr.tp_version = owner_cls->tp_version_tag; + cache2->obj = descr; + cache1->dk_version_or_hint = keys_version; + cache1->tp_version = owner_cls->tp_version_tag; *instr = _Py_MAKECODEUNIT(LOAD_METHOD_CACHED, _Py_OPARG(*instr)); goto success; From 70ad1ea81e6ef0622d8156d254805ef7c22c8815 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 11 Aug 2021 20:45:22 +0800 Subject: [PATCH 19/40] Create 2021-08-11-20-45-02.bpo-44889.2T3nTn.rst --- .../Core and Builtins/2021-08-11-20-45-02.bpo-44889.2T3nTn.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2021-08-11-20-45-02.bpo-44889.2T3nTn.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-08-11-20-45-02.bpo-44889.2T3nTn.rst b/Misc/NEWS.d/next/Core and Builtins/2021-08-11-20-45-02.bpo-44889.2T3nTn.rst new file mode 100644 index 00000000000000..8d3220799078d2 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2021-08-11-20-45-02.bpo-44889.2T3nTn.rst @@ -0,0 +1,2 @@ +Initial implementation of adaptive specialization of ``LOAD_METHOD``. One +specialized form ``LOAD_METHOD_CACHED`` added. From f4487fcac71179a43f0e9055dbfcbc2b4bbf1fbf Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 11 Aug 2021 22:22:00 +0800 Subject: [PATCH 20/40] remove unused variable --- Python/specialize.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/Python/specialize.c b/Python/specialize.c index 43c81167a496f1..13f00db288f998 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -831,7 +831,6 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, } PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); int owner_has_dict = (owner_dictptr != NULL && *owner_dictptr != NULL); - PyDictObject *cls_dict = (PyDictObject *)*cls_dictptr; PyDictObject *owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; if (Py_TYPE(owner_cls)->tp_dictoffset >= 0 && cls_has_dict && @@ -872,7 +871,6 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, cache1->tp_version = owner_cls->tp_version_tag; *instr = _Py_MAKECODEUNIT(LOAD_METHOD_CACHED, _Py_OPARG(*instr)); goto success; - } fail: STAT_INC(LOAD_METHOD, specialization_failure); From afcf51e67496ce86d5c045d329d8d0a7832885c2 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 11 Aug 2021 23:20:22 +0800 Subject: [PATCH 21/40] round two: apply mark's suggestions --- Include/internal/pycore_code.h | 3 ++- Python/ceval.c | 3 +-- Python/specialize.c | 19 +++++++++++++++++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Include/internal/pycore_code.h b/Include/internal/pycore_code.h index 160a3837e5843d..da6049cba3fcb7 100644 --- a/Include/internal/pycore_code.h +++ b/Include/internal/pycore_code.h @@ -31,7 +31,8 @@ typedef struct { } _PyLoadGlobalCache; typedef struct { - PyObject *obj; /* borrowed */ + /* Borrowed ref in LOAD_METHOD */ + PyObject *obj; } _PyObjectCache; /* Add specialized versions of entries to this union. diff --git a/Python/ceval.c b/Python/ceval.c index a2957aa1bc2613..31661be95ebbc1 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4251,7 +4251,6 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr assert(cframe.use_tracing == 0); PyObject *self = TOP(); PyTypeObject *self_cls = Py_TYPE(self); - PyObject *res = NULL; SpecializedCacheEntry *caches = GET_CACHE(); _PyAdaptiveEntry *cache0 = &caches[0].adaptive; _PyAttrCache *cache1 = &caches[-1].attr; @@ -4275,7 +4274,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr STAT_INC(LOAD_METHOD, hit); record_cache_hit(cache0); - res = cache2->obj; + PyObject *res = cache2->obj; assert(res != NULL); assert(_PyType_HasFeature(Py_TYPE(res), Py_TPFLAGS_METHOD_DESCRIPTOR)); Py_INCREF(res); diff --git a/Python/specialize.c b/Python/specialize.c index 13f00db288f998..524eb9007f1401 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -809,6 +809,9 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, } PyObject *descr = NULL; DesciptorClassification kind = analyze_descriptor(owner_cls, name, &descr, 0); + // Store the version right away, in case it's modified halfway through. + cache1->tp_version = owner_cls->tp_version_tag; + if (kind != METHOD || descr == NULL) { #if SPECIALIZATION_STATS if (kind == GETSET_OVERRIDDEN && @@ -865,10 +868,22 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, } } // else owner is maybe a builtin with no dict, or __slots__ - // Borrowed. Check tp_version_tag before accessing in case it's deleted. + /* `descr` is borrowed. Just check tp_version_tag before accessing in case + * it's deleted. This is safe for methods as long as tp_version_tag is + * validated for two main reasons: + * + * 1. The class will always hold a reference to the method so it will + * usually not be GC-ed. Should it be deleted in Python, e.g. + * `del obj.meth`, tp_version_tag will be invalidated, because of reason 2. + * + * 2. The pre-existing type method cache (MCACHE) uses the same principles + * of caching a borrowed descriptor. It does all the heavy lifting for us. + * E.g. it invalidates on every MRO modification, on every type object + * change, etc. (see PyType_Modified usages in typeobject.c). The type method + * cache has been working since Python 2.6 and it's battle tested. + */ cache2->obj = descr; cache1->dk_version_or_hint = keys_version; - cache1->tp_version = owner_cls->tp_version_tag; *instr = _Py_MAKECODEUNIT(LOAD_METHOD_CACHED, _Py_OPARG(*instr)); goto success; } From 6f1f1f094847a9afc23f719fc96e2b28ed7aa0a0 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 11 Aug 2021 23:27:47 +0800 Subject: [PATCH 22/40] remove TARGET --- Python/ceval.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index 1f55f00242004b..035efb61042c50 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4212,7 +4212,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr DISPATCH(); } - case TARGET(LOAD_METHOD): { + TARGET(LOAD_METHOD): { PREDICTED(LOAD_METHOD); STAT_INC(LOAD_METHOD, unquickened); /* Designed to work in tandem with CALL_METHOD. */ @@ -4251,7 +4251,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr DISPATCH(); } - case TARGET(LOAD_METHOD_ADAPTIVE): { + TARGET(LOAD_METHOD_ADAPTIVE): { assert(cframe.use_tracing == 0); SpecializedCacheEntry *cache = GET_CACHE(); if (cache->adaptive.counter == 0) { @@ -4272,7 +4272,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr } } - case TARGET(LOAD_METHOD_CACHED): { + TARGET(LOAD_METHOD_CACHED): { /* Designed to work in tandem with CALL_METHOD. */ assert(cframe.use_tracing == 0); PyObject *self = TOP(); @@ -4309,7 +4309,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr DISPATCH(); } - case TARGET(CALL_METHOD): { + TARGET(CALL_METHOD): { /* Designed to work in tamdem with LOAD_METHOD. */ PyObject **sp, *res; int meth_found; From dc152ed93b6091df6c4aa7cc378b3b420331e46a Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 11 Aug 2021 23:36:25 +0800 Subject: [PATCH 23/40] add to specialization stats dict --- Python/specialize.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Python/specialize.c b/Python/specialize.c index 524eb9007f1401..3388878d005bdc 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -121,6 +121,7 @@ _Py_GetSpecializationStats(void) { int err = 0; err += add_stat_dict(stats, LOAD_ATTR, "load_attr"); err += add_stat_dict(stats, LOAD_GLOBAL, "load_global"); + err += add_stat_dict(stats, LOAD_GLOBAL, "load_method"); err += add_stat_dict(stats, BINARY_SUBSCR, "binary_subscr"); if (err < 0) { Py_DECREF(stats); From dd77f75bb1f9a50488a9687dc7d4de06e29c465a Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 11 Aug 2021 23:46:14 +0800 Subject: [PATCH 24/40] improve borrowed ref safety explanation --- Python/specialize.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Python/specialize.c b/Python/specialize.c index 3388878d005bdc..9ba4608d406476 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -870,18 +870,19 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, } // else owner is maybe a builtin with no dict, or __slots__ /* `descr` is borrowed. Just check tp_version_tag before accessing in case - * it's deleted. This is safe for methods as long as tp_version_tag is - * validated for two main reasons: + * it's deleted. This is safe for methods (even inherited ones from super + * classes!) as long as tp_version_tag is validated for two main reasons: * * 1. The class will always hold a reference to the method so it will * usually not be GC-ed. Should it be deleted in Python, e.g. * `del obj.meth`, tp_version_tag will be invalidated, because of reason 2. * - * 2. The pre-existing type method cache (MCACHE) uses the same principles - * of caching a borrowed descriptor. It does all the heavy lifting for us. - * E.g. it invalidates on every MRO modification, on every type object - * change, etc. (see PyType_Modified usages in typeobject.c). The type method - * cache has been working since Python 2.6 and it's battle tested. + * 2. The pre-existing type method cache (MCACHE) uses the same principles + * of caching a borrowed descriptor. It does all the heavy lifting for us. + * E.g. it invalidates on any MRO modification, on any type object + * change along said MRO, etc. (see PyType_Modified usages in typeobject.c). + * The type method cache has been working since Python 2.6 and it's + * battle-tested. */ cache2->obj = descr; cache1->dk_version_or_hint = keys_version; From 601e9b800491cf9bbf5333cf08bdd03db44dd2a8 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Thu, 12 Aug 2021 17:47:56 +0800 Subject: [PATCH 25/40] implement LOAD_METHOD_MODULE, and LOAD_METHOD_CLASS (for python classes) --- Include/opcode.h | 10 ++- Lib/opcode.py | 2 + .../2021-08-11-20-45-02.bpo-44889.2T3nTn.rst | 10 ++- Python/ceval.c | 71 ++++++++++++---- Python/opcode_targets.h | 10 +-- Python/specialize.c | 81 ++++++++++++++----- 6 files changed, 135 insertions(+), 49 deletions(-) diff --git a/Include/opcode.h b/Include/opcode.h index b3fee47cbac354..d464a1c3d75bea 100644 --- a/Include/opcode.h +++ b/Include/opcode.h @@ -151,10 +151,12 @@ extern "C" { #define LOAD_GLOBAL_BUILTIN 43 #define LOAD_METHOD_ADAPTIVE 44 #define LOAD_METHOD_CACHED 45 -#define STORE_ATTR_ADAPTIVE 46 -#define STORE_ATTR_SPLIT_KEYS 47 -#define STORE_ATTR_SLOT 48 -#define STORE_ATTR_WITH_HINT 58 +#define LOAD_METHOD_CLASS 46 +#define LOAD_METHOD_MODULE 47 +#define STORE_ATTR_ADAPTIVE 48 +#define STORE_ATTR_SPLIT_KEYS 58 +#define STORE_ATTR_SLOT 80 +#define STORE_ATTR_WITH_HINT 81 #ifdef NEED_OPCODE_JUMP_TABLES static uint32_t _PyOpcode_RelativeJump[8] = { 0U, diff --git a/Lib/opcode.py b/Lib/opcode.py index 66091a80708157..a39075b71146bb 100644 --- a/Lib/opcode.py +++ b/Lib/opcode.py @@ -235,6 +235,8 @@ def jabs_op(name, op): "LOAD_GLOBAL_BUILTIN", "LOAD_METHOD_ADAPTIVE", "LOAD_METHOD_CACHED", + "LOAD_METHOD_CLASS", + "LOAD_METHOD_MODULE", "STORE_ATTR_ADAPTIVE", "STORE_ATTR_SPLIT_KEYS", "STORE_ATTR_SLOT", diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-08-11-20-45-02.bpo-44889.2T3nTn.rst b/Misc/NEWS.d/next/Core and Builtins/2021-08-11-20-45-02.bpo-44889.2T3nTn.rst index 8d3220799078d2..a50b6851c14b7a 100644 --- a/Misc/NEWS.d/next/Core and Builtins/2021-08-11-20-45-02.bpo-44889.2T3nTn.rst +++ b/Misc/NEWS.d/next/Core and Builtins/2021-08-11-20-45-02.bpo-44889.2T3nTn.rst @@ -1,2 +1,8 @@ -Initial implementation of adaptive specialization of ``LOAD_METHOD``. One -specialized form ``LOAD_METHOD_CACHED`` added. +Initial implementation of adaptive specialization of ``LOAD_METHOD``. The +following specialized forms were added: + +* ``LOAD_METHOD_CACHED`` + +* ``LOAD_METHOD_MODULE`` + +* ``LOAD_METHOD_CLASS`` diff --git a/Python/ceval.c b/Python/ceval.c index 035efb61042c50..11c4d515b51002 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3442,22 +3442,26 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr TARGET(LOAD_ATTR_MODULE): { assert(cframe.use_tracing == 0); - PyObject *owner = TOP(); - PyObject *res; - SpecializedCacheEntry *caches = GET_CACHE(); - _PyAdaptiveEntry *cache0 = &caches[0].adaptive; - _PyAttrCache *cache1 = &caches[-1].attr; - DEOPT_IF(!PyModule_CheckExact(owner), LOAD_ATTR); - PyDictObject *dict = (PyDictObject *)((PyModuleObject *)owner)->md_dict; - assert(dict != NULL); - DEOPT_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, LOAD_ATTR); - assert(dict->ma_keys->dk_kind == DICT_KEYS_UNICODE); - assert(cache0->index < dict->ma_keys->dk_nentries); - PyDictKeyEntry *ep = DK_ENTRIES(dict->ma_keys) + cache0->index; - res = ep->me_value; - DEOPT_IF(res == NULL, LOAD_ATTR); - STAT_INC(LOAD_ATTR, hit); + // shared with LOAD_METHOD_MODULE + #define LOAD_MODULE_ATTR_OR_METHOD(attr_or_method) \ + PyObject *owner = TOP(); \ + PyObject *res; \ + SpecializedCacheEntry *caches = GET_CACHE(); \ + _PyAdaptiveEntry *cache0 = &caches[0].adaptive; \ + _PyAttrCache *cache1 = &caches[-1].attr; \ + DEOPT_IF(!PyModule_CheckExact(owner), ##attr_or_method); \ + PyDictObject *dict = (PyDictObject *)((PyModuleObject *)owner)->md_dict; \ + assert(dict != NULL); \ + DEOPT_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, ##attr_or_method); \ + assert(dict->ma_keys->dk_kind == DICT_KEYS_UNICODE); \ + assert(cache0->index < dict->ma_keys->dk_nentries); \ + PyDictKeyEntry *ep = DK_ENTRIES(dict->ma_keys) + cache0->index; \ + res = ep->me_value; \ + DEOPT_IF(res == NULL, ##attr_or_method); \ + STAT_INC(##attr_or_method, hit); \ record_cache_hit(cache0); + + LOAD_MODULE_ATTR_OR_METHOD(LOAD_ATTR); Py_INCREF(res); SET_TOP(res); Py_DECREF(owner); @@ -4273,7 +4277,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr } TARGET(LOAD_METHOD_CACHED): { - /* Designed to work in tandem with CALL_METHOD. */ + /* LOAD_METHOD, with cached method object */ assert(cframe.use_tracing == 0); PyObject *self = TOP(); PyTypeObject *self_cls = Py_TYPE(self); @@ -4309,6 +4313,41 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr DISPATCH(); } + TARGET(LOAD_METHOD_MODULE): { + assert(cframe.use_tracing == 0); + LOAD_MODULE_ATTR_OR_METHOD(LOAD_METHOD); + Py_INCREF(res); + SET_TOP(NULL); + Py_DECREF(owner); + PUSH(res); + DISPATCH(); + } + + TARGET(LOAD_METHOD_CLASS): { + /* LOAD_METHOD, for class methods */ + assert(cframe.use_tracing == 0); + SpecializedCacheEntry *caches = GET_CACHE(); + _PyAdaptiveEntry *cache0 = &caches[0].adaptive; + _PyAttrCache *cache1 = &caches[-1].attr; + _PyObjectCache *cache2 = &caches[-2].obj; + _PyObjectCache *cache3 = &caches[-3].obj; + + DEOPT_IF(TOP() != cache3->obj, LOAD_METHOD); + PyTypeObject *cls = (PyTypeObject *)TOP(); + DEOPT_IF(cls->tp_version_tag != cache1->tp_version, LOAD_METHOD); + + STAT_INC(LOAD_METHOD, hit); + record_cache_hit(cache0); + PyObject *res = cache2->obj; + assert(res != NULL); + assert(_PyType_HasFeature(Py_TYPE(res), Py_TPFLAGS_METHOD_DESCRIPTOR)); + Py_INCREF(res); + SET_TOP(NULL); + Py_DECREF(cls); + PUSH(res); + DISPATCH(); + } + TARGET(CALL_METHOD): { /* Designed to work in tamdem with LOAD_METHOD. */ PyObject **sp, *res; diff --git a/Python/opcode_targets.h b/Python/opcode_targets.h index d73e2d6bce543a..4eff328f345919 100644 --- a/Python/opcode_targets.h +++ b/Python/opcode_targets.h @@ -45,9 +45,9 @@ static void *opcode_targets[256] = { &&TARGET_LOAD_GLOBAL_BUILTIN, &&TARGET_LOAD_METHOD_ADAPTIVE, &&TARGET_LOAD_METHOD_CACHED, + &&TARGET_LOAD_METHOD_CLASS, + &&TARGET_LOAD_METHOD_MODULE, &&TARGET_STORE_ATTR_ADAPTIVE, - &&TARGET_STORE_ATTR_SPLIT_KEYS, - &&TARGET_STORE_ATTR_SLOT, &&TARGET_WITH_EXCEPT_START, &&TARGET_GET_AITER, &&TARGET_GET_ANEXT, @@ -57,7 +57,7 @@ static void *opcode_targets[256] = { &&TARGET_INPLACE_ADD, &&TARGET_INPLACE_SUBTRACT, &&TARGET_INPLACE_MULTIPLY, - &&TARGET_STORE_ATTR_WITH_HINT, + &&TARGET_STORE_ATTR_SPLIT_KEYS, &&TARGET_INPLACE_MODULO, &&TARGET_STORE_SUBSCR, &&TARGET_DELETE_SUBSCR, @@ -79,8 +79,8 @@ static void *opcode_targets[256] = { &&TARGET_INPLACE_AND, &&TARGET_INPLACE_XOR, &&TARGET_INPLACE_OR, - &&_unknown_opcode, - &&_unknown_opcode, + &&TARGET_STORE_ATTR_SLOT, + &&TARGET_STORE_ATTR_WITH_HINT, &&TARGET_LIST_TO_TUPLE, &&TARGET_RETURN_VALUE, &&TARGET_IMPORT_STAR, diff --git a/Python/specialize.c b/Python/specialize.c index 9ba4608d406476..39183bae433d9d 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -233,7 +233,7 @@ static uint8_t adaptive_opcodes[256] = { static uint8_t cache_requirements[256] = { [LOAD_ATTR] = 2, /* _PyAdaptiveEntry and _PyAttrCache */ [LOAD_GLOBAL] = 2, /* _PyAdaptiveEntry and _PyLoadGlobalCache */ - [LOAD_METHOD] = 3, /* _PyAdaptiveEntry, _PyAttrCache and _PyObjectCache */ + [LOAD_METHOD] = 4, /* _PyAdaptiveEntry, _PyAttrCache and 2 _PyObjectCache */ [BINARY_SUBSCR] = 0, [STORE_ATTR] = 2, /* _PyAdaptiveEntry and _PyAttrCache */ }; @@ -403,10 +403,10 @@ _Py_Quicken(PyCodeObject *code) { /* Methods */ -#define SPEC_FAIL_IS_ATTR 15 -#define SPEC_FAIL_DICT_SUBCLASS 16 -#define SPEC_FAIL_MODULE_METHOD 17 -#define SPEC_FAIL_CLASS_METHOD 18 +#define SPEC_FAIL_IS_ATTR 14 +#define SPEC_FAIL_DICT_SUBCLASS 15 +#define SPEC_FAIL_BUILTIN_CLASS_METHOD 17 +#define SPEC_FAIL_CLASS_METHOD_OBJ 18 #define SPEC_FAIL_NOT_METHOD 19 /* Binary subscr */ @@ -419,7 +419,8 @@ _Py_Quicken(PyCodeObject *code) { static int specialize_module_load_attr( PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, - _PyAdaptiveEntry *cache0, _PyAttrCache *cache1) + _PyAdaptiveEntry *cache0, _PyAttrCache *cache1, int opcode, + int opcode_module) { PyModuleObject *m = (PyModuleObject *)owner; PyObject *value = NULL; @@ -427,39 +428,39 @@ specialize_module_load_attr( _Py_IDENTIFIER(__getattr__); PyDictObject *dict = (PyDictObject *)m->md_dict; if (dict == NULL) { - SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_NO_DICT); + SPECIALIZATION_FAIL(opcode, SPEC_FAIL_NO_DICT); return -1; } if (dict->ma_keys->dk_kind != DICT_KEYS_UNICODE) { - SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_NON_STRING_OR_SPLIT); + SPECIALIZATION_FAIL(opcode, SPEC_FAIL_NON_STRING_OR_SPLIT); return -1; } getattr = _PyUnicode_FromId(&PyId___getattr__); /* borrowed */ if (getattr == NULL) { - SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OVERRIDDEN); + SPECIALIZATION_FAIL(opcode, SPEC_FAIL_OVERRIDDEN); PyErr_Clear(); return -1; } Py_ssize_t index = _PyDict_GetItemHint(dict, getattr, -1, &value); assert(index != DKIX_ERROR); if (index != DKIX_EMPTY) { - SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_MODULE_ATTR_NOT_FOUND); + SPECIALIZATION_FAIL(opcode, SPEC_FAIL_MODULE_ATTR_NOT_FOUND); return -1; } index = _PyDict_GetItemHint(dict, name, -1, &value); assert (index != DKIX_ERROR); if (index != (uint16_t)index) { - SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OUT_OF_RANGE); + SPECIALIZATION_FAIL(opcode, SPEC_FAIL_OUT_OF_RANGE); return -1; } uint32_t keys_version = _PyDictKeys_GetVersionForCurrentState(dict); if (keys_version == 0) { - SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OUT_OF_VERSIONS); + SPECIALIZATION_FAIL(opcode, SPEC_FAIL_OUT_OF_VERSIONS); return -1; } cache1->dk_version_or_hint = keys_version; cache0->index = (uint16_t)index; - *instr = _Py_MAKECODEUNIT(LOAD_ATTR_MODULE, _Py_OPARG(*instr)); + *instr = _Py_MAKECODEUNIT(opcode_module, _Py_OPARG(*instr)); return 0; } @@ -620,7 +621,8 @@ _Py_Specialize_LoadAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, Sp _PyAdaptiveEntry *cache0 = &cache->adaptive; _PyAttrCache *cache1 = &cache[-1].attr; if (PyModule_CheckExact(owner)) { - int err = specialize_module_load_attr(owner, instr, name, cache0, cache1); + int err = specialize_module_load_attr(owner, instr, name, cache0, cache1, + LOAD_ATTR, LOAD_ATTR_MODULE); if (err) { goto fail; } @@ -799,9 +801,12 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, PyTypeObject *owner_cls = Py_TYPE(owner); if (PyModule_CheckExact(owner)) { - // Mabe TODO: LOAD_METHOD_MODULE - SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_MODULE_METHOD); - goto fail; + int err = specialize_module_load_attr(owner, instr, name, cache0, cache1, + LOAD_METHOD, LOAD_METHOD_MODULE); + if (err) { + goto fail; + } + goto success; } if (owner_cls->tp_dict == NULL) { if (PyType_Ready(owner_cls) < 0) { @@ -814,15 +819,47 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, cache1->tp_version = owner_cls->tp_version_tag; if (kind != METHOD || descr == NULL) { -#if SPECIALIZATION_STATS if (kind == GETSET_OVERRIDDEN && PyObject_TypeCheck(owner, &PyBaseObject_Type)) { - // Maybe TODO: LOAD_METHOD_CLASS - SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_CLASS_METHOD); - goto fail; + PyTypeObject *cls = (PyTypeObject *)owner; + kind = analyze_descriptor(cls, name, &descr, 0); + // Store the version right away, in case it's modified halfway through. + cache1->tp_version = cls->tp_version_tag; + if (descr == NULL) { + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NOT_METHOD); + goto fail; + } + // Borrowed, only used for identity checks. Do not access the object. + _PyObjectCache *cache3 = &cache[-3].obj; + + // Python class method + if (kind == METHOD) { + // borrowed, see below for why this is safe + cache2->obj = descr; + cache3->obj = owner; + *instr = _Py_MAKECODEUNIT(LOAD_METHOD_CLASS, _Py_OPARG(*instr)); + goto success; + } +#if SPECIALIZATION_STATS + // Builtin class method -- ie wrapped PyCFunction + else if (kind == NON_OVERRIDING && Py_IS_TYPE(descr, &PyClassMethodDescr_Type)) { + // Unlike python classes, builtins don't hold a reference to the + // classmethod object (but instead the descriptor). Instead, a new + // object is created every time. So we can't use the borrowed + // object cache. + // TODO: make LOAD_METHOD_CLASS work with builtin classmethods + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_BUILTIN_CLASS_METHOD); + goto fail; + } + else if (Py_IS_TYPE(descr, &PyClassMethod_Type)) { + // Note: this is the actual @classmethod decorator object, not + // the X.y form. + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_CLASS_METHOD_OBJ); + goto fail; + } +#endif } SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NOT_METHOD); -#endif goto fail; } PyObject **cls_dictptr = _PyObject_GetDictPtr((PyObject *)owner_cls); From 94b6106b9813381f298bab5c651f308aa0428241 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Thu, 12 Aug 2021 18:02:03 +0800 Subject: [PATCH 26/40] fix macro --- Python/ceval.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index 11c4d515b51002..b4ca52d3bed433 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3449,19 +3449,20 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr SpecializedCacheEntry *caches = GET_CACHE(); \ _PyAdaptiveEntry *cache0 = &caches[0].adaptive; \ _PyAttrCache *cache1 = &caches[-1].attr; \ - DEOPT_IF(!PyModule_CheckExact(owner), ##attr_or_method); \ + DEOPT_IF(!PyModule_CheckExact(owner), LOAD_##attr_or_method); \ PyDictObject *dict = (PyDictObject *)((PyModuleObject *)owner)->md_dict; \ assert(dict != NULL); \ - DEOPT_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, ##attr_or_method); \ + DEOPT_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, \ + LOAD_##attr_or_method); \ assert(dict->ma_keys->dk_kind == DICT_KEYS_UNICODE); \ assert(cache0->index < dict->ma_keys->dk_nentries); \ PyDictKeyEntry *ep = DK_ENTRIES(dict->ma_keys) + cache0->index; \ res = ep->me_value; \ - DEOPT_IF(res == NULL, ##attr_or_method); \ - STAT_INC(##attr_or_method, hit); \ + DEOPT_IF(res == NULL, LOAD_##attr_or_method); \ + STAT_INC(LOAD_##attr_or_method, hit); \ record_cache_hit(cache0); - LOAD_MODULE_ATTR_OR_METHOD(LOAD_ATTR); + LOAD_MODULE_ATTR_OR_METHOD(ATTR); Py_INCREF(res); SET_TOP(res); Py_DECREF(owner); @@ -4315,7 +4316,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr TARGET(LOAD_METHOD_MODULE): { assert(cframe.use_tracing == 0); - LOAD_MODULE_ATTR_OR_METHOD(LOAD_METHOD); + LOAD_MODULE_ATTR_OR_METHOD(METHOD); Py_INCREF(res); SET_TOP(NULL); Py_DECREF(owner); From 1d87e53124b9fc473561b7a27d1f167dc64ce43e Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Thu, 12 Aug 2021 21:16:03 +0800 Subject: [PATCH 27/40] Partially address Mark's review --- Python/ceval.c | 60 +++++++++++++++++++++++++-------------------- Python/specialize.c | 25 ++++++++++++------- 2 files changed, 50 insertions(+), 35 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index b4ca52d3bed433..3b65c6316a5dd8 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1434,6 +1434,27 @@ eval_frame_handle_pending(PyThreadState *tstate) #define BUILTINS() frame->f_builtins #define LOCALS() frame->f_locals +/* Shared opcode macros */ + +// shared by LOAD_ATTR_MODULE and LOAD_METHOD_MODULE +#define LOAD_MODULE_ATTR_OR_METHOD(attr_or_method) \ + SpecializedCacheEntry *caches = GET_CACHE(); \ + _PyAdaptiveEntry *cache0 = &caches[0].adaptive; \ + _PyAttrCache *cache1 = &caches[-1].attr; \ + DEOPT_IF(!PyModule_CheckExact(owner), LOAD_##attr_or_method); \ + PyDictObject *dict = (PyDictObject *)((PyModuleObject *)owner)->md_dict; \ + assert(dict != NULL); \ + DEOPT_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, \ + LOAD_##attr_or_method); \ + assert(dict->ma_keys->dk_kind == DICT_KEYS_UNICODE); \ + assert(cache0->index < dict->ma_keys->dk_nentries); \ + PyDictKeyEntry *ep = DK_ENTRIES(dict->ma_keys) + cache0->index; \ + res = ep->me_value; \ + DEOPT_IF(res == NULL, LOAD_##attr_or_method); \ + STAT_INC(LOAD_##attr_or_method, hit); \ + record_cache_hit(cache0); \ + Py_INCREF(res); + static int trace_function_entry(PyThreadState *tstate, InterpreterFrame *frame) { @@ -3443,27 +3464,9 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr TARGET(LOAD_ATTR_MODULE): { assert(cframe.use_tracing == 0); // shared with LOAD_METHOD_MODULE - #define LOAD_MODULE_ATTR_OR_METHOD(attr_or_method) \ - PyObject *owner = TOP(); \ - PyObject *res; \ - SpecializedCacheEntry *caches = GET_CACHE(); \ - _PyAdaptiveEntry *cache0 = &caches[0].adaptive; \ - _PyAttrCache *cache1 = &caches[-1].attr; \ - DEOPT_IF(!PyModule_CheckExact(owner), LOAD_##attr_or_method); \ - PyDictObject *dict = (PyDictObject *)((PyModuleObject *)owner)->md_dict; \ - assert(dict != NULL); \ - DEOPT_IF(dict->ma_keys->dk_version != cache1->dk_version_or_hint, \ - LOAD_##attr_or_method); \ - assert(dict->ma_keys->dk_kind == DICT_KEYS_UNICODE); \ - assert(cache0->index < dict->ma_keys->dk_nentries); \ - PyDictKeyEntry *ep = DK_ENTRIES(dict->ma_keys) + cache0->index; \ - res = ep->me_value; \ - DEOPT_IF(res == NULL, LOAD_##attr_or_method); \ - STAT_INC(LOAD_##attr_or_method, hit); \ - record_cache_hit(cache0); - + PyObject *owner = TOP(); + PyObject *res; LOAD_MODULE_ATTR_OR_METHOD(ATTR); - Py_INCREF(res); SET_TOP(res); Py_DECREF(owner); DISPATCH(); @@ -4316,8 +4319,9 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr TARGET(LOAD_METHOD_MODULE): { assert(cframe.use_tracing == 0); + PyObject *owner = TOP(); + PyObject *res; LOAD_MODULE_ATTR_OR_METHOD(METHOD); - Py_INCREF(res); SET_TOP(NULL); Py_DECREF(owner); PUSH(res); @@ -4331,12 +4335,16 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr _PyAdaptiveEntry *cache0 = &caches[0].adaptive; _PyAttrCache *cache1 = &caches[-1].attr; _PyObjectCache *cache2 = &caches[-2].obj; - _PyObjectCache *cache3 = &caches[-3].obj; - - DEOPT_IF(TOP() != cache3->obj, LOAD_METHOD); - PyTypeObject *cls = (PyTypeObject *)TOP(); - DEOPT_IF(cls->tp_version_tag != cache1->tp_version, LOAD_METHOD); + PyObject *cls = TOP(); + PyTypeObject *cls_type = Py_TYPE(cls); + assert(cls_type->tp_dictoffset > 0); + PyObject *dict = *(PyObject **) ((char *)cls + cls_type->tp_dictoffset); + DEOPT_IF(((PyDictObject *)dict)->ma_keys->dk_version != + cache1->dk_version_or_hint, LOAD_METHOD); + DEOPT_IF(((PyTypeObject *)cls)->tp_version_tag != cache1->tp_version, + LOAD_METHOD); + STAT_INC(LOAD_METHOD, hit); record_cache_hit(cache0); PyObject *res = cache2->obj; diff --git a/Python/specialize.c b/Python/specialize.c index 39183bae433d9d..26e5c7a73b4950 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -233,7 +233,7 @@ static uint8_t adaptive_opcodes[256] = { static uint8_t cache_requirements[256] = { [LOAD_ATTR] = 2, /* _PyAdaptiveEntry and _PyAttrCache */ [LOAD_GLOBAL] = 2, /* _PyAdaptiveEntry and _PyLoadGlobalCache */ - [LOAD_METHOD] = 4, /* _PyAdaptiveEntry, _PyAttrCache and 2 _PyObjectCache */ + [LOAD_METHOD] = 3, /* _PyAdaptiveEntry, _PyAttrCache and _PyObjectCache */ [BINARY_SUBSCR] = 0, [STORE_ATTR] = 2, /* _PyAdaptiveEntry and _PyAttrCache */ }; @@ -818,6 +818,10 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, // Store the version right away, in case it's modified halfway through. cache1->tp_version = owner_cls->tp_version_tag; + PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); + int owner_has_dict = (owner_dictptr != NULL && *owner_dictptr != NULL); + PyDictObject *owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; + if (kind != METHOD || descr == NULL) { if (kind == GETSET_OVERRIDDEN && PyObject_TypeCheck(owner, &PyBaseObject_Type)) { @@ -829,14 +833,19 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NOT_METHOD); goto fail; } - // Borrowed, only used for identity checks. Do not access the object. - _PyObjectCache *cache3 = &cache[-3].obj; // Python class method if (kind == METHOD) { + assert(owner_has_dict); + uint32_t keys_version = _PyDictKeys_GetVersionForCurrentState( + owner_dict); + if (keys_version == 0) { + SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OUT_OF_VERSIONS); + goto fail; + } // borrowed, see below for why this is safe cache2->obj = descr; - cache3->obj = owner; + cache1->dk_version_or_hint = keys_version; *instr = _Py_MAKECODEUNIT(LOAD_METHOD_CLASS, _Py_OPARG(*instr)); goto success; } @@ -870,9 +879,6 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NO_DICT); goto fail; } - PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); - int owner_has_dict = (owner_dictptr != NULL && *owner_dictptr != NULL); - PyDictObject *owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; if (Py_TYPE(owner_cls)->tp_dictoffset >= 0 && cls_has_dict && owner_cls->tp_dictoffset >= 0) { @@ -895,6 +901,7 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, PyObject *value = NULL; Py_ssize_t ix = _Py_dict_lookup(owner_dict, name, hash, &value); assert(ix != DKIX_ERROR); + // Shouldn't be in __dict__. That makes it an attribute. if (ix != DKIX_EMPTY) { SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_IS_ATTR); goto fail; @@ -903,8 +910,8 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, if (keys_version == 0) { SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OUT_OF_VERSIONS); goto fail; - } - } // else owner is maybe a builtin with no dict, or __slots__ + } // Fall through. + } // Else owner is maybe a builtin with no dict, or __slots__ /* `descr` is borrowed. Just check tp_version_tag before accessing in case * it's deleted. This is safe for methods (even inherited ones from super From 3697d4f9ff52fab9cafd0de55e1de87e84cbed79 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Thu, 12 Aug 2021 22:02:32 +0800 Subject: [PATCH 28/40] refactor, fully address review --- Python/specialize.c | 93 +++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/Python/specialize.c b/Python/specialize.c index 26e5c7a73b4950..240b140cfb775c 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -403,8 +403,9 @@ _Py_Quicken(PyCodeObject *code) { /* Methods */ -#define SPEC_FAIL_IS_ATTR 14 -#define SPEC_FAIL_DICT_SUBCLASS 15 +#define SPEC_FAIL_NEGATIVE_DICTOFFSET 14 +#define SPEC_FAIL_IS_ATTR 15 +#define SPEC_FAIL_DICT_SUBCLASS 16 #define SPEC_FAIL_BUILTIN_CLASS_METHOD 17 #define SPEC_FAIL_CLASS_METHOD_OBJ 18 #define SPEC_FAIL_NOT_METHOD 19 @@ -792,6 +793,9 @@ _Py_Specialize_StoreAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, S } +// Note: Please collect stats carefully before modifying. A subtle change can cause +// a significant drop in cache hits. A possible test is python.exe -m test_typing +// test_re test_dis test_zlib. int _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SpecializedCacheEntry *cache) { @@ -813,43 +817,39 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, return -1; } } - PyObject *descr = NULL; - DesciptorClassification kind = analyze_descriptor(owner_cls, name, &descr, 0); - // Store the version right away, in case it's modified halfway through. - cache1->tp_version = owner_cls->tp_version_tag; - + // Technically this is fine for bound method calls, but slightly slower at runtime. + if (Py_TYPE(owner_cls)->tp_dictoffset < 0) { + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NEGATIVE_DICTOFFSET); + goto fail; + } PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); int owner_has_dict = (owner_dictptr != NULL && *owner_dictptr != NULL); PyDictObject *owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; + // Check for classmethods. + int owner_is_class = (owner_cls->tp_getattro != PyObject_GenericGetAttr) && + PyObject_TypeCheck(owner, &PyBaseObject_Type); + + PyObject *descr = NULL; + DesciptorClassification kind = 0; + // class method + if (owner_is_class) { + kind = analyze_descriptor((PyTypeObject *)owner, name, &descr, 0); + // Store the version right away, in case it's modified halfway through. + cache1->tp_version = ((PyTypeObject *)owner)->tp_version_tag; + } + else { + // instance method + kind = analyze_descriptor(owner_cls, name, &descr, 0); + cache1->tp_version = owner_cls->tp_version_tag; + } if (kind != METHOD || descr == NULL) { - if (kind == GETSET_OVERRIDDEN && - PyObject_TypeCheck(owner, &PyBaseObject_Type)) { - PyTypeObject *cls = (PyTypeObject *)owner; - kind = analyze_descriptor(cls, name, &descr, 0); - // Store the version right away, in case it's modified halfway through. - cache1->tp_version = cls->tp_version_tag; +#if SPECIALIZATION_STATS + if (owner_is_class) { if (descr == NULL) { SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NOT_METHOD); goto fail; } - - // Python class method - if (kind == METHOD) { - assert(owner_has_dict); - uint32_t keys_version = _PyDictKeys_GetVersionForCurrentState( - owner_dict); - if (keys_version == 0) { - SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OUT_OF_VERSIONS); - goto fail; - } - // borrowed, see below for why this is safe - cache2->obj = descr; - cache1->dk_version_or_hint = keys_version; - *instr = _Py_MAKECODEUNIT(LOAD_METHOD_CLASS, _Py_OPARG(*instr)); - goto success; - } -#if SPECIALIZATION_STATS // Builtin class method -- ie wrapped PyCFunction else if (kind == NON_OVERRIDING && Py_IS_TYPE(descr, &PyClassMethodDescr_Type)) { // Unlike python classes, builtins don't hold a reference to the @@ -866,8 +866,8 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_CLASS_METHOD_OBJ); goto fail; } -#endif } +#endif SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NOT_METHOD); goto fail; } @@ -879,10 +879,8 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NO_DICT); goto fail; } - if (Py_TYPE(owner_cls)->tp_dictoffset >= 0 && - cls_has_dict && - owner_cls->tp_dictoffset >= 0) { - + assert(kind == METHOD); + if (cls_has_dict && owner_cls->tp_dictoffset >= 0) { // If o.__dict__ changes, the method might be found in o.__dict__ // instead of old type lookup. So record o.__dict__'s keys. uint32_t keys_version = UINT32_MAX; @@ -899,19 +897,23 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, return -1; } PyObject *value = NULL; - Py_ssize_t ix = _Py_dict_lookup(owner_dict, name, hash, &value); - assert(ix != DKIX_ERROR); - // Shouldn't be in __dict__. That makes it an attribute. - if (ix != DKIX_EMPTY) { - SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_IS_ATTR); - goto fail; - } + if (!owner_is_class) { + // Normal methods shouldn't be in o.__dict__. + // That makes it an attribute. + Py_ssize_t ix = _Py_dict_lookup(owner_dict, name, hash, &value); + assert(ix != DKIX_ERROR); + if (ix != DKIX_EMPTY) { + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_IS_ATTR); + goto fail; + } + } keys_version = _PyDictKeys_GetVersionForCurrentState(owner_dict); if (keys_version == 0) { SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OUT_OF_VERSIONS); goto fail; - } // Fall through. - } // Else owner is maybe a builtin with no dict, or __slots__ + } + // Fall through. + } // Else owner is maybe a builtin with no dict, or __slots__. Doesn't matter. /* `descr` is borrowed. Just check tp_version_tag before accessing in case * it's deleted. This is safe for methods (even inherited ones from super @@ -930,7 +932,8 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, */ cache2->obj = descr; cache1->dk_version_or_hint = keys_version; - *instr = _Py_MAKECODEUNIT(LOAD_METHOD_CACHED, _Py_OPARG(*instr)); + *instr = _Py_MAKECODEUNIT(owner_is_class ? LOAD_METHOD_CLASS : + LOAD_METHOD_CACHED, _Py_OPARG(*instr)); goto success; } fail: From 1d83667756850aaae4317294ec72f1720078cea1 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Thu, 12 Aug 2021 22:19:24 +0800 Subject: [PATCH 29/40] fix check for classes --- Python/specialize.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/specialize.c b/Python/specialize.c index 240b140cfb775c..53b87e5bcebbf4 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -827,7 +827,7 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, PyDictObject *owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; // Check for classmethods. int owner_is_class = (owner_cls->tp_getattro != PyObject_GenericGetAttr) && - PyObject_TypeCheck(owner, &PyBaseObject_Type); + PyObject_TypeCheck(owner, &PyBaseObject_Type) && PyType_Check(owner); PyObject *descr = NULL; DesciptorClassification kind = 0; From 5db1d3b1340941ca6def32e61b72513eacfe0623 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 13 Aug 2021 00:09:53 +0800 Subject: [PATCH 30/40] update comments, LOAD_METHOD_BUILTIN isn't worth it --- Python/specialize.c | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/Python/specialize.c b/Python/specialize.c index 53b87e5bcebbf4..e714ff8f3f71b2 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -793,9 +793,9 @@ _Py_Specialize_StoreAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, S } -// Note: Please collect stats carefully before modifying. A subtle change can cause -// a significant drop in cache hits. A possible test is python.exe -m test_typing -// test_re test_dis test_zlib. +// Please collect stats carefully before and after modifying. A subtle change +// can cause a significant drop in cache hits. A possible test is +// python.exe -m test_typing test_re test_dis test_zlib. int _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SpecializedCacheEntry *cache) { @@ -831,8 +831,8 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, PyObject *descr = NULL; DesciptorClassification kind = 0; - // class method if (owner_is_class) { + // class method kind = analyze_descriptor((PyTypeObject *)owner, name, &descr, 0); // Store the version right away, in case it's modified halfway through. cache1->tp_version = ((PyTypeObject *)owner)->tp_version_tag; @@ -850,19 +850,15 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NOT_METHOD); goto fail; } - // Builtin class method -- ie wrapped PyCFunction - else if (kind == NON_OVERRIDING && Py_IS_TYPE(descr, &PyClassMethodDescr_Type)) { - // Unlike python classes, builtins don't hold a reference to the - // classmethod object (but instead the descriptor). Instead, a new - // object is created every time. So we can't use the borrowed - // object cache. - // TODO: make LOAD_METHOD_CLASS work with builtin classmethods + // Builtin METH_CLASS class method -- ie wrapped PyCFunction + else if (kind == NON_OVERRIDING && + Py_IS_TYPE(descr, &PyClassMethodDescr_Type)) { + // NOTE (KJ): Don't bother, rare and no speedup in microbenchmarks. SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_BUILTIN_CLASS_METHOD); goto fail; } else if (Py_IS_TYPE(descr, &PyClassMethod_Type)) { - // Note: this is the actual @classmethod decorator object, not - // the X.y form. + // Note: this is the actual Python classmethod(func) object. SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_CLASS_METHOD_OBJ); goto fail; } @@ -898,7 +894,7 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, } PyObject *value = NULL; if (!owner_is_class) { - // Normal methods shouldn't be in o.__dict__. + // Instance methods shouldn't be in o.__dict__. // That makes it an attribute. Py_ssize_t ix = _Py_dict_lookup(owner_dict, name, hash, &value); assert(ix != DKIX_ERROR); From d12205bde53c61cdd67b793350a35bfe4f099d0c Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 13 Aug 2021 17:24:24 +0800 Subject: [PATCH 31/40] apply mark's refactoring suggestions --- Python/specialize.c | 165 +++++++++++++++++++++----------------------- 1 file changed, 80 insertions(+), 85 deletions(-) diff --git a/Python/specialize.c b/Python/specialize.c index e714ff8f3f71b2..615bdff3806cd8 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -817,9 +817,8 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, return -1; } } - // Technically this is fine for bound method calls, but slightly slower at runtime. if (Py_TYPE(owner_cls)->tp_dictoffset < 0) { - SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NEGATIVE_DICTOFFSET); + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_OUT_OF_RANGE); goto fail; } PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); @@ -832,116 +831,112 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, PyObject *descr = NULL; DesciptorClassification kind = 0; if (owner_is_class) { - // class method + // Class method. kind = analyze_descriptor((PyTypeObject *)owner, name, &descr, 0); // Store the version right away, in case it's modified halfway through. cache1->tp_version = ((PyTypeObject *)owner)->tp_version_tag; } else { - // instance method + // Instance method, kind = analyze_descriptor(owner_cls, name, &descr, 0); cache1->tp_version = owner_cls->tp_version_tag; } - - if (kind != METHOD || descr == NULL) { + assert(descr != NULL || kind == ABSENT || kind == GETSET_OVERRIDDEN); + switch (kind) { + case METHOD: + break; #if SPECIALIZATION_STATS - if (owner_is_class) { - if (descr == NULL) { - SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NOT_METHOD); - goto fail; - } - // Builtin METH_CLASS class method -- ie wrapped PyCFunction - else if (kind == NON_OVERRIDING && - Py_IS_TYPE(descr, &PyClassMethodDescr_Type)) { - // NOTE (KJ): Don't bother, rare and no speedup in microbenchmarks. + case NON_OVERRIDING: + if (Py_IS_TYPE(descr, &PyClassMethodDescr_Type)) { + // Builtin METH_CLASS class method -- ie wrapped PyCFunction. + // (KJ): Don't bother, rare and no speedup in microbenchmarks. SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_BUILTIN_CLASS_METHOD); - goto fail; } else if (Py_IS_TYPE(descr, &PyClassMethod_Type)) { - // Note: this is the actual Python classmethod(func) object. + // This is the actual Python classmethod(func) object. SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_CLASS_METHOD_OBJ); - goto fail; } - } + goto fail; #endif + default: SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NOT_METHOD); goto fail; } - PyObject **cls_dictptr = _PyObject_GetDictPtr((PyObject *)owner_cls); - int cls_has_dict = (cls_dictptr != NULL && - *cls_dictptr != NULL && - PyDict_CheckExact(*cls_dictptr)); - if (!cls_has_dict) { - SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NO_DICT); - goto fail; - } + assert(kind == METHOD); - if (cls_has_dict && owner_cls->tp_dictoffset >= 0) { - // If o.__dict__ changes, the method might be found in o.__dict__ - // instead of old type lookup. So record o.__dict__'s keys. - uint32_t keys_version = UINT32_MAX; - if (owner_has_dict) { - // _PyDictKeys_GetVersionForCurrentState isn't accurate for - // custom dict subclasses at the moment. - if (!PyDict_CheckExact(owner_dict)) { - SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_DICT_SUBCLASS); - goto fail; - } - assert(PyUnicode_CheckExact(name)); - Py_hash_t hash = PyObject_Hash(name); - if (hash == -1) { - return -1; - } - PyObject *value = NULL; - if (!owner_is_class) { - // Instance methods shouldn't be in o.__dict__. - // That makes it an attribute. - Py_ssize_t ix = _Py_dict_lookup(owner_dict, name, hash, &value); - assert(ix != DKIX_ERROR); - if (ix != DKIX_EMPTY) { - SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_IS_ATTR); - goto fail; - } - } - keys_version = _PyDictKeys_GetVersionForCurrentState(owner_dict); - if (keys_version == 0) { - SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OUT_OF_VERSIONS); + // Technically this is fine for bound method calls, but slightly slower at + // runtime to get dict. TODO: profile pyperformance and see if it's worth it + // to slightly slow down the common case, so that we can specialize this + // uncommon one. + if (owner_cls->tp_dictoffset < 0) { + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NEGATIVE_DICTOFFSET); + goto fail; + } + // If o.__dict__ changes, the method might be found in o.__dict__ + // instead of old type lookup. So record o.__dict__'s keys. + uint32_t keys_version = UINT32_MAX; + if (owner_has_dict) { + // _PyDictKeys_GetVersionForCurrentState isn't accurate for + // custom dict subclasses at the moment. + if (!PyDict_CheckExact(owner_dict)) { + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_DICT_SUBCLASS); + goto fail; + } + assert(PyUnicode_CheckExact(name)); + Py_hash_t hash = PyObject_Hash(name); + if (hash == -1) { + return -1; + } + PyObject *value = NULL; + if (!owner_is_class) { + // Instance methods shouldn't be in o.__dict__. That makes + // it an attribute. + Py_ssize_t ix = _Py_dict_lookup(owner_dict, name, hash, &value); + assert(ix != DKIX_ERROR); + if (ix != DKIX_EMPTY) { + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_IS_ATTR); goto fail; } - // Fall through. - } // Else owner is maybe a builtin with no dict, or __slots__. Doesn't matter. + } + keys_version = _PyDictKeys_GetVersionForCurrentState(owner_dict); + if (keys_version == 0) { + SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OUT_OF_VERSIONS); + goto fail; + } + // Fall through. + } // Else owner is maybe a builtin with no dict, or __slots__. Doesn't matter. - /* `descr` is borrowed. Just check tp_version_tag before accessing in case - * it's deleted. This is safe for methods (even inherited ones from super - * classes!) as long as tp_version_tag is validated for two main reasons: - * - * 1. The class will always hold a reference to the method so it will - * usually not be GC-ed. Should it be deleted in Python, e.g. - * `del obj.meth`, tp_version_tag will be invalidated, because of reason 2. - * - * 2. The pre-existing type method cache (MCACHE) uses the same principles - * of caching a borrowed descriptor. It does all the heavy lifting for us. - * E.g. it invalidates on any MRO modification, on any type object - * change along said MRO, etc. (see PyType_Modified usages in typeobject.c). - * The type method cache has been working since Python 2.6 and it's - * battle-tested. - */ - cache2->obj = descr; - cache1->dk_version_or_hint = keys_version; - *instr = _Py_MAKECODEUNIT(owner_is_class ? LOAD_METHOD_CLASS : - LOAD_METHOD_CACHED, _Py_OPARG(*instr)); - goto success; - } -fail: - STAT_INC(LOAD_METHOD, specialization_failure); - assert(!PyErr_Occurred()); - cache_backoff(cache0); - return 0; + /* `descr` is borrowed. Just check tp_version_tag before accessing in case + * it's deleted. This is safe for methods (even inherited ones from super + * classes!) as long as tp_version_tag is validated for two main reasons: + * + * 1. The class will always hold a reference to the method so it will + * usually not be GC-ed. Should it be deleted in Python, e.g. + * `del obj.meth`, tp_version_tag will be invalidated, because of reason 2. + * + * 2. The pre-existing type method cache (MCACHE) uses the same principles + * of caching a borrowed descriptor. It does all the heavy lifting for us. + * E.g. it invalidates on any MRO modification, on any type object + * change along said MRO, etc. (see PyType_Modified usages in typeobject.c). + * The type method cache has been working since Python 2.6 and it's + * battle-tested. + */ + cache2->obj = descr; + cache1->dk_version_or_hint = keys_version; + *instr = _Py_MAKECODEUNIT(owner_is_class ? LOAD_METHOD_CLASS : + LOAD_METHOD_CACHED, _Py_OPARG(*instr)); + // Fall through. success: STAT_INC(LOAD_METHOD, specialization_success); assert(!PyErr_Occurred()); cache0->counter = saturating_start(); return 0; +fail: + STAT_INC(LOAD_METHOD, specialization_failure); + assert(!PyErr_Occurred()); + cache_backoff(cache0); + return 0; + } int _Py_Specialize_LoadGlobal( From 8af701d8211b1110650f7fe414ebbcf43f6d22c8 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 13 Aug 2021 17:46:25 +0800 Subject: [PATCH 32/40] more refactoring --- Python/specialize.c | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/Python/specialize.c b/Python/specialize.c index 615bdff3806cd8..d97ff69dcbb23c 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -821,26 +821,28 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_OUT_OF_RANGE); goto fail; } + // Technically this is fine for bound method calls, but slightly slower at + // runtime to get dict. TODO: profile pyperformance and see if it's worth it + // to slightly slow down the common case, so that we can specialize this + // uncommon one. + if (owner_cls->tp_dictoffset < 0) { + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NEGATIVE_DICTOFFSET); + goto fail; + } PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); int owner_has_dict = (owner_dictptr != NULL && *owner_dictptr != NULL); PyDictObject *owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; // Check for classmethods. int owner_is_class = (owner_cls->tp_getattro != PyObject_GenericGetAttr) && - PyObject_TypeCheck(owner, &PyBaseObject_Type) && PyType_Check(owner); + PyType_Check(owner) && PyObject_TypeCheck(owner, &PyBaseObject_Type); + owner_cls = owner_is_class ? (PyTypeObject *)owner : owner_cls; PyObject *descr = NULL; DesciptorClassification kind = 0; - if (owner_is_class) { - // Class method. - kind = analyze_descriptor((PyTypeObject *)owner, name, &descr, 0); - // Store the version right away, in case it's modified halfway through. - cache1->tp_version = ((PyTypeObject *)owner)->tp_version_tag; - } - else { - // Instance method, - kind = analyze_descriptor(owner_cls, name, &descr, 0); - cache1->tp_version = owner_cls->tp_version_tag; - } + kind = analyze_descriptor(owner_cls, name, &descr, 0); + // Store the version right away, in case it's modified halfway through. + cache1->tp_version = owner_cls->tp_version_tag; + assert(descr != NULL || kind == ABSENT || kind == GETSET_OVERRIDDEN); switch (kind) { case METHOD: @@ -864,14 +866,6 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, } assert(kind == METHOD); - // Technically this is fine for bound method calls, but slightly slower at - // runtime to get dict. TODO: profile pyperformance and see if it's worth it - // to slightly slow down the common case, so that we can specialize this - // uncommon one. - if (owner_cls->tp_dictoffset < 0) { - SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NEGATIVE_DICTOFFSET); - goto fail; - } // If o.__dict__ changes, the method might be found in o.__dict__ // instead of old type lookup. So record o.__dict__'s keys. uint32_t keys_version = UINT32_MAX; From ad4ff6e994ea141d89807b9d8d26f62d3e7c147f Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 13 Aug 2021 17:59:57 +0800 Subject: [PATCH 33/40] refactor and address review comments --- Python/specialize.c | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/Python/specialize.c b/Python/specialize.c index d97ff69dcbb23c..3caf4eb048179c 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -476,6 +476,8 @@ typedef enum { OBJECT_SLOT, /* Is an object slot descriptor */ OTHER_SLOT, /* Is a slot descriptor of another type */ NON_OVERRIDING, /* Is another non-overriding descriptor, and is an instance of an immutable class*/ + BUILTIN_CLASSMETHOD, /* Builtin methods with METH_CLASS */ + PYTHON_CLASSMETHOD, /* Python classmethod(func) object */ NON_DESCRIPTOR, /* Is not a descriptor, and is an instance of an immutable class */ MUTABLE, /* Instance of a mutable class; might, or might not, be a descriptor */ ABSENT, /* Attribute is not present on the class */ @@ -531,6 +533,13 @@ analyze_descriptor(PyTypeObject *type, PyObject *name, PyObject **descr, int sto if (desc_cls->tp_flags & Py_TPFLAGS_METHOD_DESCRIPTOR) { return METHOD; } + if (Py_IS_TYPE(descr, &PyClassMethodDescr_Type)) { + return BUILTIN_CLASSMETHOD; + } + if (Py_IS_TYPE(descr, &PyClassMethod_Type)) { + // Python classmethod(func) object. + return PYTHON_CLASSMETHOD; + } return NON_OVERRIDING; } return NON_DESCRIPTOR; @@ -602,6 +611,8 @@ specialize_dict_access( /* No attribute in instance dictionary */ switch(kind) { case NON_OVERRIDING: + case BUILTIN_CLASSMETHOD: + case PYTHON_CLASSMETHOD: SPECIALIZATION_FAIL(base_op, SPEC_FAIL_NON_OVERRIDING_DESCRIPTOR); return 0; case NON_DESCRIPTOR: @@ -834,7 +845,7 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, PyDictObject *owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; // Check for classmethods. int owner_is_class = (owner_cls->tp_getattro != PyObject_GenericGetAttr) && - PyType_Check(owner) && PyObject_TypeCheck(owner, &PyBaseObject_Type); + PyType_Check(owner); owner_cls = owner_is_class ? (PyTypeObject *)owner : owner_cls; PyObject *descr = NULL; @@ -847,22 +858,15 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, switch (kind) { case METHOD: break; -#if SPECIALIZATION_STATS - case NON_OVERRIDING: - if (Py_IS_TYPE(descr, &PyClassMethodDescr_Type)) { - // Builtin METH_CLASS class method -- ie wrapped PyCFunction. - // (KJ): Don't bother, rare and no speedup in microbenchmarks. - SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_BUILTIN_CLASS_METHOD); - } - else if (Py_IS_TYPE(descr, &PyClassMethod_Type)) { - // This is the actual Python classmethod(func) object. - SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_CLASS_METHOD_OBJ); - } - goto fail; -#endif - default: - SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NOT_METHOD); - goto fail; + case BUILTIN_CLASSMETHOD: + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_BUILTIN_CLASS_METHOD); + goto fail; + case PYTHON_CLASSMETHOD: + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_CLASS_METHOD_OBJ); + goto fail; + default: + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NOT_METHOD); + goto fail; } assert(kind == METHOD); From a3d1b50ab5918307edff06e2519e6c3bda00f307 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 13 Aug 2021 18:20:43 +0800 Subject: [PATCH 34/40] add cases, use pytype_check only --- Python/specialize.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Python/specialize.c b/Python/specialize.c index 3caf4eb048179c..20851f625366ea 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -552,7 +552,8 @@ specialize_dict_access( _PyAdaptiveEntry *cache0, _PyAttrCache *cache1, int base_op, int split_op, int hint_op) { - assert(kind == NON_OVERRIDING || kind == NON_DESCRIPTOR || kind == ABSENT); + assert(kind == NON_OVERRIDING || kind == NON_DESCRIPTOR || kind == ABSENT || + kind == BUILTIN_CLASSMETHOD || kind == PYTHON_CLASSMETHOD); // No desciptor, or non overriding. if (type->tp_dictoffset < 0) { SPECIALIZATION_FAIL(base_op, SPEC_FAIL_OUT_OF_RANGE); @@ -696,6 +697,8 @@ _Py_Specialize_LoadAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, Sp case GETSET_OVERRIDDEN: SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OVERRIDDEN); goto fail; + case BUILTIN_CLASSMETHOD: + case PYTHON_CLASSMETHOD: case NON_OVERRIDING: case NON_DESCRIPTOR: case ABSENT: @@ -775,6 +778,8 @@ _Py_Specialize_StoreAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, S case GETSET_OVERRIDDEN: SPECIALIZATION_FAIL(STORE_ATTR, SPEC_FAIL_OVERRIDDEN); goto fail; + case BUILTIN_CLASSMETHOD: + case PYTHON_CLASSMETHOD: case NON_OVERRIDING: case NON_DESCRIPTOR: case ABSENT: @@ -844,8 +849,7 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, int owner_has_dict = (owner_dictptr != NULL && *owner_dictptr != NULL); PyDictObject *owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; // Check for classmethods. - int owner_is_class = (owner_cls->tp_getattro != PyObject_GenericGetAttr) && - PyType_Check(owner); + int owner_is_class = PyType_Check(owner); owner_cls = owner_is_class ? (PyTypeObject *)owner : owner_cls; PyObject *descr = NULL; From d8384bc298bc39ba595a980312658ee41c01acb9 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 13 Aug 2021 18:59:41 +0800 Subject: [PATCH 35/40] address review number 100 --- Python/specialize.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Python/specialize.c b/Python/specialize.c index 20851f625366ea..50dcb93884120c 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -533,10 +533,10 @@ analyze_descriptor(PyTypeObject *type, PyObject *name, PyObject **descr, int sto if (desc_cls->tp_flags & Py_TPFLAGS_METHOD_DESCRIPTOR) { return METHOD; } - if (Py_IS_TYPE(descr, &PyClassMethodDescr_Type)) { + if (Py_IS_TYPE(descriptor, &PyClassMethodDescr_Type)) { return BUILTIN_CLASSMETHOD; } - if (Py_IS_TYPE(descr, &PyClassMethod_Type)) { + if (Py_IS_TYPE(descriptor, &PyClassMethod_Type)) { // Python classmethod(func) object. return PYTHON_CLASSMETHOD; } @@ -842,8 +842,8 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, // to slightly slow down the common case, so that we can specialize this // uncommon one. if (owner_cls->tp_dictoffset < 0) { - SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NEGATIVE_DICTOFFSET); - goto fail; + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NEGATIVE_DICTOFFSET); + goto fail; } PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); int owner_has_dict = (owner_dictptr != NULL && *owner_dictptr != NULL); From ae2a5209d2d2c810ad2b3c0a3e337c8c03695772 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 13 Aug 2021 19:03:55 +0800 Subject: [PATCH 36/40] remove usless comment --- Python/specialize.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Python/specialize.c b/Python/specialize.c index 50dcb93884120c..c1499636994513 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -537,7 +537,6 @@ analyze_descriptor(PyTypeObject *type, PyObject *name, PyObject **descr, int sto return BUILTIN_CLASSMETHOD; } if (Py_IS_TYPE(descriptor, &PyClassMethod_Type)) { - // Python classmethod(func) object. return PYTHON_CLASSMETHOD; } return NON_OVERRIDING; From a12406b34e512ee4cc2aa646c70005df4c402b77 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sat, 14 Aug 2021 00:35:45 +0800 Subject: [PATCH 37/40] Fix buildbot errors: cache the type/class and compare that too --- Python/ceval.c | 6 ++++-- Python/specialize.c | 11 +++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index 3b65c6316a5dd8..bc60b6c7ad2241 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4289,7 +4289,9 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr _PyAdaptiveEntry *cache0 = &caches[0].adaptive; _PyAttrCache *cache1 = &caches[-1].attr; _PyObjectCache *cache2 = &caches[-2].obj; + _PyObjectCache *cache3 = &caches[-3].obj; + DEOPT_IF((PyObject *)self_cls != cache3->obj, LOAD_METHOD); DEOPT_IF(self_cls->tp_version_tag != cache1->tp_version, LOAD_METHOD); assert(cache1->dk_version_or_hint != 0); assert(cache1->tp_version != 0); @@ -4335,13 +4337,13 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr _PyAdaptiveEntry *cache0 = &caches[0].adaptive; _PyAttrCache *cache1 = &caches[-1].attr; _PyObjectCache *cache2 = &caches[-2].obj; + _PyObjectCache *cache3 = &caches[-3].obj; PyObject *cls = TOP(); PyTypeObject *cls_type = Py_TYPE(cls); assert(cls_type->tp_dictoffset > 0); PyObject *dict = *(PyObject **) ((char *)cls + cls_type->tp_dictoffset); - DEOPT_IF(((PyDictObject *)dict)->ma_keys->dk_version != - cache1->dk_version_or_hint, LOAD_METHOD); + DEOPT_IF(cls != cache3->obj, LOAD_METHOD); DEOPT_IF(((PyTypeObject *)cls)->tp_version_tag != cache1->tp_version, LOAD_METHOD); diff --git a/Python/specialize.c b/Python/specialize.c index c1499636994513..39bb70f5d8b260 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -233,7 +233,7 @@ static uint8_t adaptive_opcodes[256] = { static uint8_t cache_requirements[256] = { [LOAD_ATTR] = 2, /* _PyAdaptiveEntry and _PyAttrCache */ [LOAD_GLOBAL] = 2, /* _PyAdaptiveEntry and _PyLoadGlobalCache */ - [LOAD_METHOD] = 3, /* _PyAdaptiveEntry, _PyAttrCache and _PyObjectCache */ + [LOAD_METHOD] = 4, /* _PyAdaptiveEntry, _PyAttrCache and 2 _PyObjectCache */ [BINARY_SUBSCR] = 0, [STORE_ATTR] = 2, /* _PyAdaptiveEntry and _PyAttrCache */ }; @@ -817,6 +817,7 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, _PyAdaptiveEntry *cache0 = &cache->adaptive; _PyAttrCache *cache1 = &cache[-1].attr; _PyObjectCache *cache2 = &cache[-2].obj; + _PyObjectCache *cache3 = &cache[-3].obj; PyTypeObject *owner_cls = Py_TYPE(owner); if (PyModule_CheckExact(owner)) { @@ -851,6 +852,10 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, int owner_is_class = PyType_Check(owner); owner_cls = owner_is_class ? (PyTypeObject *)owner : owner_cls; + if ((owner_cls->tp_flags & Py_TPFLAGS_VALID_VERSION_TAG) == 0) { + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_OUT_OF_RANGE); + goto fail; + } PyObject *descr = NULL; DesciptorClassification kind = 0; kind = analyze_descriptor(owner_cls, name, &descr, 0); @@ -875,8 +880,9 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, assert(kind == METHOD); // If o.__dict__ changes, the method might be found in o.__dict__ // instead of old type lookup. So record o.__dict__'s keys. + // Not required for class methods -- tp_version_tag is enough for those. uint32_t keys_version = UINT32_MAX; - if (owner_has_dict) { + if (owner_has_dict && !owner_is_class) { // _PyDictKeys_GetVersionForCurrentState isn't accurate for // custom dict subclasses at the moment. if (!PyDict_CheckExact(owner_dict)) { @@ -923,6 +929,7 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, * battle-tested. */ cache2->obj = descr; + cache3->obj = (PyObject *)owner_cls; // borrowed - cache the type cache1->dk_version_or_hint = keys_version; *instr = _Py_MAKECODEUNIT(owner_is_class ? LOAD_METHOD_CLASS : LOAD_METHOD_CACHED, _Py_OPARG(*instr)); From 33445d5c0f99c5e8b9d99f67727a2bf30256709a Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Mon, 16 Aug 2021 00:26:20 +0800 Subject: [PATCH 38/40] Revert "Fix buildbot errors: cache the type/class and compare that too" This reverts commit a12406b34e512ee4cc2aa646c70005df4c402b77. --- Python/ceval.c | 6 ++---- Python/specialize.c | 11 ++--------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index bc60b6c7ad2241..3b65c6316a5dd8 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4289,9 +4289,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr _PyAdaptiveEntry *cache0 = &caches[0].adaptive; _PyAttrCache *cache1 = &caches[-1].attr; _PyObjectCache *cache2 = &caches[-2].obj; - _PyObjectCache *cache3 = &caches[-3].obj; - DEOPT_IF((PyObject *)self_cls != cache3->obj, LOAD_METHOD); DEOPT_IF(self_cls->tp_version_tag != cache1->tp_version, LOAD_METHOD); assert(cache1->dk_version_or_hint != 0); assert(cache1->tp_version != 0); @@ -4337,13 +4335,13 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr _PyAdaptiveEntry *cache0 = &caches[0].adaptive; _PyAttrCache *cache1 = &caches[-1].attr; _PyObjectCache *cache2 = &caches[-2].obj; - _PyObjectCache *cache3 = &caches[-3].obj; PyObject *cls = TOP(); PyTypeObject *cls_type = Py_TYPE(cls); assert(cls_type->tp_dictoffset > 0); PyObject *dict = *(PyObject **) ((char *)cls + cls_type->tp_dictoffset); - DEOPT_IF(cls != cache3->obj, LOAD_METHOD); + DEOPT_IF(((PyDictObject *)dict)->ma_keys->dk_version != + cache1->dk_version_or_hint, LOAD_METHOD); DEOPT_IF(((PyTypeObject *)cls)->tp_version_tag != cache1->tp_version, LOAD_METHOD); diff --git a/Python/specialize.c b/Python/specialize.c index 39bb70f5d8b260..c1499636994513 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -233,7 +233,7 @@ static uint8_t adaptive_opcodes[256] = { static uint8_t cache_requirements[256] = { [LOAD_ATTR] = 2, /* _PyAdaptiveEntry and _PyAttrCache */ [LOAD_GLOBAL] = 2, /* _PyAdaptiveEntry and _PyLoadGlobalCache */ - [LOAD_METHOD] = 4, /* _PyAdaptiveEntry, _PyAttrCache and 2 _PyObjectCache */ + [LOAD_METHOD] = 3, /* _PyAdaptiveEntry, _PyAttrCache and _PyObjectCache */ [BINARY_SUBSCR] = 0, [STORE_ATTR] = 2, /* _PyAdaptiveEntry and _PyAttrCache */ }; @@ -817,7 +817,6 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, _PyAdaptiveEntry *cache0 = &cache->adaptive; _PyAttrCache *cache1 = &cache[-1].attr; _PyObjectCache *cache2 = &cache[-2].obj; - _PyObjectCache *cache3 = &cache[-3].obj; PyTypeObject *owner_cls = Py_TYPE(owner); if (PyModule_CheckExact(owner)) { @@ -852,10 +851,6 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, int owner_is_class = PyType_Check(owner); owner_cls = owner_is_class ? (PyTypeObject *)owner : owner_cls; - if ((owner_cls->tp_flags & Py_TPFLAGS_VALID_VERSION_TAG) == 0) { - SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_OUT_OF_RANGE); - goto fail; - } PyObject *descr = NULL; DesciptorClassification kind = 0; kind = analyze_descriptor(owner_cls, name, &descr, 0); @@ -880,9 +875,8 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, assert(kind == METHOD); // If o.__dict__ changes, the method might be found in o.__dict__ // instead of old type lookup. So record o.__dict__'s keys. - // Not required for class methods -- tp_version_tag is enough for those. uint32_t keys_version = UINT32_MAX; - if (owner_has_dict && !owner_is_class) { + if (owner_has_dict) { // _PyDictKeys_GetVersionForCurrentState isn't accurate for // custom dict subclasses at the moment. if (!PyDict_CheckExact(owner_dict)) { @@ -929,7 +923,6 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, * battle-tested. */ cache2->obj = descr; - cache3->obj = (PyObject *)owner_cls; // borrowed - cache the type cache1->dk_version_or_hint = keys_version; *instr = _Py_MAKECODEUNIT(owner_is_class ? LOAD_METHOD_CLASS : LOAD_METHOD_CACHED, _Py_OPARG(*instr)); From 6d806a29d15f5399320de2b1571a2e92c662f2ce Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Mon, 16 Aug 2021 20:49:27 +0800 Subject: [PATCH 39/40] add a few more checks to safeguard specializations --- Python/specialize.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Python/specialize.c b/Python/specialize.c index c1499636994513..d49b2b72aef848 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -819,6 +819,7 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, _PyObjectCache *cache2 = &cache[-2].obj; PyTypeObject *owner_cls = Py_TYPE(owner); + PyDictObject *owner_dict = NULL; if (PyModule_CheckExact(owner)) { int err = specialize_module_load_attr(owner, instr, name, cache0, cache1, LOAD_METHOD, LOAD_METHOD_MODULE); @@ -846,11 +847,18 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, } PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); int owner_has_dict = (owner_dictptr != NULL && *owner_dictptr != NULL); - PyDictObject *owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; + owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; + Py_XINCREF(owner_dict); // make sure dict doesn't disappear halfway // Check for classmethods. int owner_is_class = PyType_Check(owner); owner_cls = owner_is_class ? (PyTypeObject *)owner : owner_cls; + if ((owner_cls->tp_flags & Py_TPFLAGS_VALID_VERSION_TAG) == 0 || + owner_cls->tp_version_tag == 0) { + SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_OUT_OF_VERSIONS); + goto fail; + } + PyObject *descr = NULL; DesciptorClassification kind = 0; kind = analyze_descriptor(owner_cls, name, &descr, 0); @@ -928,11 +936,13 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, LOAD_METHOD_CACHED, _Py_OPARG(*instr)); // Fall through. success: + Py_XDECREF(owner_dict); STAT_INC(LOAD_METHOD, specialization_success); assert(!PyErr_Occurred()); cache0->counter = saturating_start(); return 0; fail: + Py_XDECREF(owner_dict); STAT_INC(LOAD_METHOD, specialization_failure); assert(!PyErr_Occurred()); cache_backoff(cache0); From d27e3888bded20d05157635fbc398185053c4e12 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Mon, 16 Aug 2021 22:27:42 +0800 Subject: [PATCH 40/40] Fix for types with no dict --- Python/ceval.c | 7 +++++-- Python/specialize.c | 9 ++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index 9ef2a8d4f71e5c..333c54f50e2e3a 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -4409,11 +4409,14 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr PyTypeObject *cls_type = Py_TYPE(cls); assert(cls_type->tp_dictoffset > 0); PyObject *dict = *(PyObject **) ((char *)cls + cls_type->tp_dictoffset); - DEOPT_IF(((PyDictObject *)dict)->ma_keys->dk_version != + // Don't care if no dict -- tp_version_tag should catch anything wrong. + DEOPT_IF(dict != NULL && ((PyDictObject *)dict)->ma_keys->dk_version != cache1->dk_version_or_hint, LOAD_METHOD); DEOPT_IF(((PyTypeObject *)cls)->tp_version_tag != cache1->tp_version, LOAD_METHOD); - + assert(cache1->dk_version_or_hint != 0); + assert(cache1->tp_version != 0); + STAT_INC(LOAD_METHOD, hit); record_cache_hit(cache0); PyObject *res = cache2->obj; diff --git a/Python/specialize.c b/Python/specialize.c index e11852ed0446ff..359bec5719363b 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -857,10 +857,8 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_OUT_OF_RANGE); goto fail; } - // Technically this is fine for bound method calls, but slightly slower at - // runtime to get dict. TODO: profile pyperformance and see if it's worth it - // to slightly slow down the common case, so that we can specialize this - // uncommon one. + // Technically this is fine for bound method calls, but it's uncommon and + // slightly slower at runtime to get dict. if (owner_cls->tp_dictoffset < 0) { SPECIALIZATION_FAIL(LOAD_METHOD, SPEC_FAIL_NEGATIVE_DICTOFFSET); goto fail; @@ -868,7 +866,8 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, PyObject **owner_dictptr = _PyObject_GetDictPtr(owner); int owner_has_dict = (owner_dictptr != NULL && *owner_dictptr != NULL); owner_dict = owner_has_dict ? (PyDictObject *)*owner_dictptr : NULL; - Py_XINCREF(owner_dict); // make sure dict doesn't disappear halfway + // Make sure dict doesn't get GC-ed halfway. + Py_XINCREF(owner_dict); // Check for classmethods. int owner_is_class = PyType_Check(owner); owner_cls = owner_is_class ? (PyTypeObject *)owner : owner_cls;