Skip to content

Commit 30558a3

Browse files
DinoVfacebook-github-bot
authored andcommitted
Re-enable load method elimination for 3.12
Summary: Re-enables the load method elimination optimization in multi-threaded compile that's currently disabled in 3.12. We maintain our own dictionary of core built-in types which we can apply this to. To make this thread safe in 3.12 we use an `unordered_map` for the type->dict mapping as we can't safely use a map that's keyed off non-unicode keys without potentially hitting the interpreter state. We then use a normal Python dict w/ only unicode keys for the members which is thread-safe w/o the GIL held. If a type isn't one of the built-in types but is immutable, has no meta-class and has no dictionary we also allow the optimization to be applied (although this hits very few types in the test cases - mostly just something in the lru cache). The `unordered_map` is our first usage of a C++ type in `ModuleState` which has initialization that's something other than zero-initialization. Therefore we now run the in place constructor on the memory that the Python runtime has allocated for us to store the ModuleState. Reviewed By: mpage Differential Revision: D78361677 fbshipit-source-id: ec3280d28dc434bb2b2e4f1a81156030e4aab3ac
1 parent b37a945 commit 30558a3

6 files changed

Lines changed: 209 additions & 30 deletions

File tree

cinderx/Jit/hir/optimization.cpp

Lines changed: 109 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "cinderx/Jit/hir/printer.h"
1919
#include "cinderx/Jit/threaded_compile.h"
2020
#include "cinderx/UpstreamBorrow/borrowed.h" // @donotremove
21+
#include "cinderx/module_state.h"
2122

2223
#include <fmt/format.h>
2324

@@ -824,20 +825,47 @@ struct MethodInvoke {
824825
CallMethod* call_method{nullptr};
825826
};
826827

827-
// Returns true if LoadMethod/CallMethod/GetSecondOutput were removed.
828-
// Returns false if they could not be removed.
829-
static bool tryEliminateLoadMethod(Function& irfunc, MethodInvoke& invoke) {
830-
// This isn't safe in the multi-threaded compilation on 3.12 because we
831-
// don't hold the GIL which is required for PyType_Lookup.
832-
RETURN_MULTITHREADED_COMPILE(false);
828+
#if PY_VERSION_HEX >= 0x030C0000
829+
BorrowedRef<> immutableMultithreadedTypeLookup(
830+
BorrowedRef<PyTypeObject> type,
831+
BorrowedRef<> name) {
832+
BorrowedRef<> mro = type->tp_mro;
833+
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(mro.get()); i++) {
834+
PyTypeObject* mro_type =
835+
reinterpret_cast<PyTypeObject*>(PyTuple_GET_ITEM(mro.get(), i));
836+
if (PyType_HasFeature(mro_type, _Py_TPFLAGS_STATIC_BUILTIN)) {
837+
auto& builtins = cinderx::getModuleState()->builtinMembers();
838+
839+
auto members = builtins.find(mro_type);
840+
if (members == builtins.end()) {
841+
// We don't know anything about this builtin type.
842+
return nullptr;
843+
}
844+
// We load all of the members from the MRO in the builtins
845+
// cache so it's completely authorative.
846+
return PyDict_GetItemWithError(members->second, name);
847+
} else if (
848+
!PyType_HasFeature(mro_type, Py_TPFLAGS_IMMUTABLETYPE) ||
849+
!PyType_CheckExact(mro_type)) {
850+
// We can't trust anything about this base type
851+
return nullptr;
852+
}
833853

834-
ThreadedCompileSerialize guard;
835-
PyCodeObject* code = invoke.load_method->frameState()->code;
836-
PyObject* names = code->co_names;
837-
PyObject* name = PyTuple_GetItem(names, invoke.load_method->name_idx());
838-
JIT_DCHECK(name != nullptr, "name must not be null");
839-
Register* receiver = invoke.load_method->receiver();
840-
Type receiver_type = receiver->type();
854+
BorrowedRef<> method_obj =
855+
PyDict_GetItemWithError(_PyType_GetDict(mro_type), name);
856+
if (method_obj != nullptr) {
857+
return method_obj;
858+
}
859+
}
860+
return nullptr;
861+
}
862+
#endif
863+
864+
// Gets a directly invokable method object from a JIT Type. This only succeeds
865+
// if we know the type can be directly invoked.
866+
static BorrowedRef<> getMethodObjectFromType(
867+
Type receiver_type,
868+
BorrowedRef<> name) {
841869
// This is a list of common builtin types whose methods cannot be overwritten
842870
// from managed code and for which looking up the methods is guaranteed to
843871
// not do anything "weird" that needs to happen at runtime, like make a
@@ -846,26 +874,85 @@ static bool tryEliminateLoadMethod(Function& irfunc, MethodInvoke& invoke) {
846874
// loading and invoking methods off an instance (e.g. {}.fromkeys(...)) is
847875
// resolved and called differently than from the type (e.g.
848876
// dict.fromkeys(...)). The code below handles the instance case only.
877+
#if PY_VERSION_HEX < 0x030C0000
878+
849879
if (!(receiver_type <= TArray || receiver_type <= TBool ||
850880
receiver_type <= TBytesExact || receiver_type <= TCode ||
851881
receiver_type <= TDictExact || receiver_type <= TFloatExact ||
852882
receiver_type <= TListExact || receiver_type <= TLongExact ||
853883
receiver_type <= TNoneType || receiver_type <= TSetExact ||
854884
receiver_type <= TTupleExact || receiver_type <= TUnicodeExact)) {
855-
return false;
885+
return nullptr;
856886
}
857887
PyTypeObject* type = receiver_type.runtimePyType();
858888
if (type == nullptr) {
859889
// This might happen for a variety of reasons, such as encountering a
860890
// method load on a maybe-defined value where the definition occurs in a
861-
// block of code that isn't seen by the compiler (e.g. in an except block).
891+
// block of code that isn't seen by the compiler (e.g. in an except
892+
// block).
862893
JIT_DCHECK(
863894
receiver_type == TBottom,
864895
"Type {} expected to have PyTypeObject*",
865896
receiver_type);
866-
return false;
897+
return nullptr;
898+
}
899+
return _PyType_Lookup(type, name);
900+
#else
901+
if (!receiver_type.hasTypeExactSpec()) {
902+
return nullptr;
903+
}
904+
PyTypeObject* type = receiver_type.runtimePyType();
905+
if (type == nullptr) {
906+
// This might happen for a variety of reasons, such as encountering a
907+
// method load on a maybe-defined value where the definition occurs in a
908+
// block of code that isn't seen by the compiler (e.g. in an except
909+
// block).
910+
JIT_DCHECK(
911+
receiver_type == TBottom,
912+
"Type {} expected to have PyTypeObject*",
913+
receiver_type);
914+
return nullptr;
867915
}
868-
auto method_obj = Ref<>::create(_PyType_Lookup(type, name));
916+
917+
BorrowedRef<> method_obj = nullptr;
918+
// In 3.12 we can't do PyType_Lookup because for built-in types it needs
919+
// access to the current runtime, and in multi-threaded compile we don't
920+
// have it. So we instead have a cache of all of the builtin types that we
921+
// support this for.
922+
auto& builtins = cinderx::getModuleState()->builtinMembers();
923+
924+
if (PyType_HasFeature(type, _Py_TPFLAGS_STATIC_BUILTIN)) {
925+
auto it = builtins.find(receiver_type.runtimePyType());
926+
if (it != builtins.end()) {
927+
method_obj = PyDict_GetItemWithError(it->second, name);
928+
}
929+
} else if (
930+
PyType_HasFeature(type, Py_TPFLAGS_IMMUTABLETYPE) &&
931+
PyType_CheckExact(type) && type->tp_dictoffset == 0) {
932+
method_obj = immutableMultithreadedTypeLookup(type, name);
933+
if (Py_TYPE(method_obj) != &PyClassMethodDescr_Type &&
934+
Py_TYPE(method_obj) != &PyMethodDescr_Type &&
935+
Py_TYPE(method_obj) != &PyWrapperDescr_Type &&
936+
Py_TYPE(method_obj) != &PyFunction_Type) {
937+
method_obj = nullptr;
938+
}
939+
}
940+
return method_obj;
941+
#endif
942+
}
943+
944+
// Returns true if LoadMethod/CallMethod/GetSecondOutput were removed.
945+
// Returns false if they could not be removed.
946+
static bool tryEliminateLoadMethod(Function& irfunc, MethodInvoke& invoke) {
947+
ThreadedCompileSerialize guard;
948+
PyCodeObject* code = invoke.load_method->frameState()->code;
949+
PyObject* names = code->co_names;
950+
PyObject* name = PyTuple_GetItem(names, invoke.load_method->name_idx());
951+
JIT_DCHECK(name != nullptr, "name must not be null");
952+
953+
Register* receiver = invoke.load_method->receiver();
954+
Type receiver_type = receiver->type();
955+
BorrowedRef<> method_obj = getMethodObjectFromType(receiver_type, name);
869956
if (method_obj == nullptr) {
870957
// No such method. Let the LoadMethod fail at runtime. _PyType_Lookup does
871958
// not raise an exception.
@@ -889,14 +976,17 @@ static bool tryEliminateLoadMethod(Function& irfunc, MethodInvoke& invoke) {
889976
// Pass the type as the first argument (e.g. dict.fromkeys).
890977
Register* type_reg = irfunc.env.AllocateRegister();
891978
auto load_type = LoadConst::create(
892-
type_reg, Type::fromObject(reinterpret_cast<PyObject*>(type)));
979+
type_reg,
980+
Type::fromObject(
981+
reinterpret_cast<PyObject*>(receiver_type.runtimePyType())));
893982
load_type->setBytecodeOffset(invoke.load_method->bytecodeOffset());
894983
load_type->InsertBefore(*invoke.call_method);
895984
call_static->SetOperand(1, type_reg);
896985
} else {
897986
JIT_DCHECK(
898987
Py_TYPE(method_obj) == &PyMethodDescr_Type ||
899-
Py_TYPE(method_obj) == &PyWrapperDescr_Type,
988+
Py_TYPE(method_obj) == &PyWrapperDescr_Type ||
989+
Py_TYPE(method_obj) == &PyFunction_Type,
900990
"unexpected type");
901991
// Pass the instance as the first argument (e.g. str.join, str.__mod__).
902992
call_static->SetOperand(1, receiver);

cinderx/RuntimeTests/hir_tests/all_passes_test.txt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,15 +496,26 @@ fun jittestmodule:test {
496496
Locals<1> v11
497497
}
498498
}
499+
v23:Object = LoadMethodCached<0; "append"> v15 {
500+
LiveValues<1> o:v15
501+
FrameState {
502+
CurInstrOffset 8
503+
Locals<1> v15
504+
}
505+
}
506+
v18:OptObject = GetSecondOutput<OptObject> v23
499507
v19:ImmortalLongExact[1] = LoadConst<ImmortalLongExact[1]>
500-
v24:CInt32 = ListAppend v15 v19 {
501-
LiveValues<2> o:v15 unc:v19
508+
v20:Object = CallMethod<3> v23 v18 v19 {
509+
LiveValues<4> o:v15 o:v18 unc:v19 o:v23
502510
FrameState {
503511
CurInstrOffset 30
504512
Locals<1> v15
505513
}
506514
}
507515
Decref v15
516+
XDecref v18
517+
Decref v20
518+
Decref v23
508519
v21:ImmortalNoneType = LoadConst<ImmortalNoneType>
509520
Return<ImmortalNoneType> v21
510521
}

cinderx/RuntimeTests/hir_tests/builtin_load_method_elimination_test.txt

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -226,11 +226,15 @@ fun jittestmodule:test {
226226
bb 3 (preds 1, 2) {
227227
v28:ImmortalUnicodeExact = Phi<1, 2> v22 v24
228228
Snapshot
229-
UseType<ImmortalUnicodeExact> v28
230-
v32:MortalObjectUser[method_descriptor:0xdeadbeef] = LoadConst<MortalObjectUser[method_descriptor:0xdeadbeef]>
231-
v30:ImmortalUnicodeExact = Assign v28
229+
v32:Object = LoadMethodCached<0; "join"> v28 {
230+
FrameState {
231+
CurInstrOffset 18
232+
Locals<3> v12 v13 v28
233+
}
234+
}
235+
v30:OptObject = GetSecondOutput<OptObject> v32
232236
Snapshot
233-
v31:UnicodeExact = VectorCall<2, static> v32 v28 v12 {
237+
v31:Object = CallMethod<3> v32 v30 v12 {
234238
FrameState {
235239
CurInstrOffset 40
236240
Locals<3> v12 v13 v28
@@ -402,11 +406,14 @@ fun jittestmodule:test {
402406
}
403407
}
404408
Snapshot
405-
UseType<UnicodeExact> v16
406-
v21:MortalObjectUser[method_descriptor:0xdeadbeef] = LoadConst<MortalObjectUser[method_descriptor:0xdeadbeef]>
407-
v18:UnicodeExact = Assign v16
409+
v21:Object = LoadMethodCached<1; "upper"> v16 {
410+
FrameState {
411+
CurInstrOffset 32
412+
}
413+
}
414+
v18:OptObject = GetSecondOutput<OptObject> v21
408415
Snapshot
409-
v19:UnicodeExact = VectorCall<1, static> v21 v16 {
416+
v19:Object = CallMethod<2> v21 v18 {
410417
FrameState {
411418
CurInstrOffset 52
412419
}

cinderx/_cinderx-lib.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1221,7 +1221,8 @@ PyMethodDef _cinderx_methods[] = {
12211221
{nullptr, nullptr, 0, nullptr}};
12221222

12231223
static int _cinderx_exec(PyObject* m) {
1224-
auto state = (cinderx::ModuleState*)PyModule_GetState(m);
1224+
void* state_mem = PyModule_GetState(m);
1225+
auto state = new (state_mem) cinderx::ModuleState();
12251226
auto cache_manager = new (std::nothrow) jit::GlobalCacheManager();
12261227
if (cache_manager == nullptr) {
12271228
return -1;
@@ -1343,6 +1344,12 @@ static int _cinderx_exec(PyObject* m) {
13431344
}
13441345
state->setSymbolizer(symbolizer);
13451346

1347+
if constexpr (PY_VERSION_HEX >= 0x030C0000) {
1348+
if (!state->initBuiltinMembers()) {
1349+
return -1;
1350+
}
1351+
}
1352+
13461353
cinderx::setModule(m);
13471354

13481355
CiExc_StaticTypeError =

cinderx/module_state.cpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
#include "cinderx/module_state.h"
44

5+
#include "internal/pycore_object.h"
6+
57
#include "cinderx/Common/log.h"
68

79
namespace cinderx {
@@ -43,6 +45,58 @@ ModuleState* getModuleState() {
4345
return s_cinderx_state;
4446
}
4547

48+
bool ModuleState::initBuiltinMembers() {
49+
#if PY_VERSION_HEX >= 0x030C0000
50+
constexpr PyTypeObject* types[] = {
51+
&PyBool_Type,
52+
&PyBytes_Type,
53+
&PyByteArray_Type,
54+
&PyComplex_Type,
55+
&PyCode_Type,
56+
&PyDict_Type,
57+
&PyFloat_Type,
58+
&PyFrozenSet_Type,
59+
&PyList_Type,
60+
&PyLong_Type,
61+
&_PyNone_Type,
62+
&PyProperty_Type,
63+
&PySet_Type,
64+
&PyTuple_Type,
65+
&PyUnicode_Type,
66+
};
67+
68+
for (auto type : types) {
69+
PyObject* mro = type->tp_mro;
70+
if (mro == nullptr) {
71+
continue;
72+
}
73+
74+
Ref<> type_members = Ref<>::steal(PyDict_New());
75+
if (type_members == nullptr) {
76+
return false;
77+
}
78+
for (Py_ssize_t i = 0; i < Py_SIZE(mro); i++) {
79+
PyTypeObject* base =
80+
reinterpret_cast<PyTypeObject*>(PyTuple_GetItem(mro, i));
81+
Py_ssize_t cur_mem = 0;
82+
PyObject *key, *value;
83+
Ref<> tp_dict = Ref<>::steal(PyType_GetDict(base));
84+
while (PyDict_Next(tp_dict, &cur_mem, &key, &value)) {
85+
if (PyDict_Contains(type_members, key)) {
86+
continue;
87+
}
88+
if (PyDict_SetItem(type_members, key, value) < 0) {
89+
return false;
90+
}
91+
}
92+
}
93+
94+
builtin_members_.emplace(type, std::move(type_members));
95+
}
96+
#endif
97+
return true;
98+
}
99+
46100
} // namespace cinderx
47101

48102
extern "C" {

cinderx/module_state.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "cinderx/async_lazy_value_iface.h"
1717

1818
#include <memory>
19+
#include <unordered_map>
1920

2021
namespace cinderx {
2122

@@ -166,6 +167,14 @@ class ModuleState {
166167
std::unique_ptr<jit::IJitGenFreeList>(jit_gen_free_list);
167168
}
168169

170+
// Returns a dictionary of type->dict[name, members] for standard builtin
171+
// types.
172+
std::unordered_map<PyTypeObject*, Ref<>>& builtinMembers() {
173+
return builtin_members_;
174+
}
175+
176+
bool initBuiltinMembers();
177+
169178
private:
170179
std::unique_ptr<jit::IGlobalCacheManager> cache_manager_;
171180
std::unique_ptr<jit::ICodeAllocator> code_allocator_;
@@ -175,6 +184,7 @@ class ModuleState {
175184
std::unique_ptr<IAsyncLazyValueState> async_lazy_value_;
176185
std::unique_ptr<jit::IJitGenFreeList> jit_gen_free_list_;
177186
Ref<PyTypeObject> coro_type_, gen_type_, anext_awaitable_type_;
187+
std::unordered_map<PyTypeObject*, Ref<>> builtin_members_;
178188
#ifdef ENABLE_LIGHTWEIGHT_FRAMES
179189
Ref<> frame_reifier_;
180190
#endif

0 commit comments

Comments
 (0)