8000 bpo-44889: Specialize LOAD_METHOD with PEP 659 adaptive interpreter by Fidget-Spinner · Pull Request #27722 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

bpo-44889: Specialize LOAD_METHOD with PEP 659 adaptive interpreter #27722

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 43 commits into from
Aug 17, 2021
Merged
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
60ab351
WIP: implement LOAD_METHOD_HINT
Fidget-Spinner Aug 6, 2021
e0f7ebe
fix most tests: validate object __dict__ didn't modify keys
Fidget-Spinner Aug 6, 2021
ffd7ff3
fix all tests and allow no owner.__dict__ to be specialized!
Fidget-Spinner Aug 7, 2021
2b014a2
specialize builtins too
Fidget-Spinner Aug 7, 2021
b200887
turn off specialization stats
Fidget-Spinner Aug 7, 2021
d977986
improve specialization
Fidget-Spinner Aug 7, 2021
187fddb
fix a comment
Fidget-Spinner Aug 7, 2021
c71f142
better attribute detection - fix all tests
Fidget-Spinner Aug 7, 2021
f3082f4
always check attr inside instance __dict__
Fidget-Spinner Aug 7, 2021
0f32d60
backoff immediately for LOAD_METHOD, fail on dict subclasses, optimiz…
Fidget-Spinner Aug 8, 2021
8a31db3
Improve comments and specialization stats
Fidget-Spinner Aug 9, 2021
06d606e
Merge remote-tracking branch 'upstream/main' into specialize_load_method
Fidget-Spinner Aug 10, 2021
608f6d0
regen opcode
Fidget-Spinner Aug 10, 2021
1f5e64a
simplify code, use new spec_fail stats style
Fidget-Spinner Aug 10, 2021
29daf44
LOAD_METHOD_WITH_HINT now supports inherited methods
Fidget-Spinner Aug 10, 2021
a3d6dd3
remove unneeded deopt
Fidget-Spinner Aug 10, 2021
d7ee4ac
remove experiments, rename to LOAD_METHOD_CACHED
Fidget-Spinner Aug 11, 2021
c944af0
move deopt earlier
Fidget-Spinner Aug 11, 2021
2f5cc63
Apply Mark's suggestions, fix up comments
Fidget-Spinner Aug 11, 2021
70ad1ea
Create 2021-08-11-20-45-02.bpo-44889.2T3nTn.rst
Fidget-Spinner Aug 11, 2021
f4487fc
remove unused variable
Fidget-Spinner Aug 11, 2021
afcf51e
round two: apply mark's suggestions
Fidget-Spinner Aug 11, 2021
e551d7a
Merge remote-tracking branch 'upstream/main' into specialize_load_method
Fidget-Spinner Aug 11, 2021
6f1f1f0
remove TARGET
Fidget-Spinner Aug 11, 2021
dc152ed
add to specialization stats dict
Fidget-Spinner Aug 11, 2021
dd77f75
improve borrowed ref safety explanation
Fidget-Spinner Aug 11, 2021
601e9b8
implement LOAD_METHOD_MODULE, and LOAD_METHOD_CLASS (for python classes)
Fidget-Spinner Aug 12, 2021
94b6106
fix macro
Fidget-Spinner Aug 12, 2021
1d87e53
Partially address Mark's review
Fidget-Spinner Aug 12, 2021
3697d4f
refactor, fully address review
Fidget-Spinner Aug 12, 2021
1d83667
fix check for classes
Fidget-Spinner Aug 12, 2021
5db1d3b
update comments, LOAD_METHOD_BUILTIN isn't worth it
Fidget-Spinner Aug 12, 2021
d12205b
apply mark's refactoring suggestions
Fidget-Spinner Aug 13, 2021
8af701d
more refactoring
Fidget-Spinner Aug 13, 2021
ad4ff6e
refactor and address review comments
Fidget-Spinner Aug 13, 2021
a3d1b50
add cases, use pytype_check only
Fidget-Spinner Aug 13, 2021
d8384bc
address review number 100
Fidget-Spinner Aug 13, 2021
ae2a520
remove usless comment
Fidget-Spinner Aug 13, 2021
a12406b
Fix buildbot errors: cache the type/class and compare that too
Fidget-Spinner Aug 13, 2021
33445d5
Revert "Fix buildbot errors: cache the type/class and compare that too"
Fidget-Spinner Aug 15, 2021
6d806a2
add a few more checks to safeguard specializations
Fidget-Spinner Aug 16, 2021
020a326
Merge remote-tracking branch 'upstream/main' into specialize_load_method
Fidget-Spinner Aug 16, 2021
d27e388
Fix for types with no dict
Fidget-Spinner Aug 16, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
apply mark's refactoring suggestions
  • Loading branch information
Fidget-Spinner committed Aug 13, 2021
commit d12205bde53c61cdd67b793350a35bfe4f099d0c
165 changes: 80 additions & 85 deletions Python/specialize.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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(
Expand Down
0