@@ -97,6 +97,13 @@ UnitDeletedCallback handle_unit_deleted_during_preload = nullptr;
9797std::atomic<int > g_compile_workers_attempted;
9898std::atomic<int > g_compile_workers_retries;
9999
100+ // Don't care flags: CO_NOFREE, CO_FUTURE_* (the only still-relevant future is
101+ // "annotations" which doesn't impact bytecode execution.)
102+ constexpr int required_code_flags = CO_OPTIMIZED | CO_NEWLOCALS ;
103+ bool hasRequiredFlags (BorrowedRef<PyCodeObject> code) {
104+ return (code->co_flags & required_code_flags) == required_code_flags;
105+ }
106+
100107uint64_t countCalls (PyCodeObject* code) {
101108#if SHADOWCODE_SUPPORTED
102109 return code->co_mutable ->ncalls ;
@@ -1087,6 +1094,89 @@ bool compile_all(size_t workers = 0) {
10871094 return true ;
10881095}
10891096
1097+ // Gets the eligibility for code or a function to be compiled. A function
1098+ // can be ineligible, eligible due to the JIT list, or if there's no
1099+ // jit list then just eligible. This is used to support handling nested
1100+ // functions in the cases of multi-threaded compile / JIT list and without.
1101+ //
1102+ // In multi-threaded compile w/ a JIT list: We need to track the nested code
1103+ // objects in jit_reg_units for when the multi-threaded compile kicks in and we
1104+ // may not have created any functions yet. But we don't need that if we're not
1105+ // doing multi-threaded compile, we'll only compile nested functions when a
1106+ // function gets called. So that's why we track this as an extra state.
1107+ //
1108+ // In both cases we always need to track the outer function so that we don't
1109+ // repeatedly re-compile nested functions - which is the big change here. That's
1110+ // the processing that we were previously only doing when we had a JIT list so
1111+ // now we're just skipping the jit_reg_units case when we're doing this for the
1112+ // non-JIT list/multi-threaded compile case.
1113+ enum class JitEligibility { Ineligible, JitListEligible, Eligible };
1114+
1115+ /*
1116+ * Check for a functions eligibility to be compiled.
1117+ *
1118+ * This is the most broad definition of eligibility - that is it will only
1119+ * return Ineligible for functions which are specifically not allowed to
1120+ * be compiled for one reason or another.
1121+ *
1122+ * This doesn't guarantee that the function can or will be compiled, it just
1123+ * checks if the JIT has been configured in such a way that compilation is
1124+ * possible.
1125+ */
1126+ JitEligibility getCompilationEligibility (BorrowedRef<PyFunctionObject> func) {
1127+ // Can be called after the module has been finalized, due to function events.
1128+ if (jitCtx () == nullptr || isCinderModule (func->func_module )) {
1129+ return JitEligibility::Ineligible;
1130+ }
1131+
1132+ BorrowedRef<PyCodeObject> code{func->func_code };
1133+ if (!hasRequiredFlags (code)) {
1134+ return JitEligibility::Ineligible;
1135+ }
1136+
1137+ // Note: This is not the same as fetching the function's code object and
1138+ // checking its module and qualname, as functions can be renamed after they
1139+ // are created. Code objects cannot.
1140+ if (auto jit_list = cinderx::getModuleState ()->jitList ()) {
1141+ if (jit_list->lookupFunc (func) == 1 ) {
1142+ return JitEligibility::JitListEligible;
1143+ }
1144+ return JitEligibility::Ineligible;
1145+ }
1146+
1147+ return JitEligibility::Eligible;
1148+ }
1149+
1150+ /*
1151+ * Variant of getCompilationEligibility() for nested code objects.
1152+ */
1153+ JitEligibility getCompilationEligibility (
1154+ BorrowedRef<> module_name,
1155+ BorrowedRef<PyCodeObject> code) {
1156+ // Can be called after the module has been finalized, due to function events.
1157+ if (jitCtx () == nullptr ) {
1158+ return JitEligibility::Ineligible;
1159+ }
1160+
1161+ if (isCinderModule (module_name)) {
1162+ return JitEligibility::Ineligible;
1163+ }
1164+
1165+ if (!hasRequiredFlags (code)) {
1166+ return JitEligibility::Ineligible;
1167+ }
1168+
1169+ if (auto jit_list = cinderx::getModuleState ()->jitList ()) {
1170+ if (jit_list->lookupCode (code) == 1 ||
1171+ jit_list->lookupName (module_name, code->co_qualname ) == 1 ) {
1172+ return JitEligibility::JitListEligible;
1173+ }
1174+ return JitEligibility::Ineligible;
1175+ }
1176+
1177+ return JitEligibility::Eligible;
1178+ }
1179+
10901180// Recursively search the given co_consts tuple for any code objects that are
10911181// on the current jit-list, using the given module name to form a
10921182// fully-qualified function name.
@@ -1106,7 +1196,8 @@ std::vector<BorrowedRef<PyCodeObject>> findNestedCodes(
11061196 BorrowedRef<PyCodeObject> code = PyTuple_GET_ITEM (consts, i);
11071197 if (!PyCode_Check (code) || !visited.insert (code).second ||
11081198 code->co_qualname == nullptr ||
1109- !shouldScheduleCompile (module , code)) {
1199+ getCompilationEligibility (module , code) ==
1200+ JitEligibility::Ineligible) {
11101201 continue ;
11111202 }
11121203
@@ -1151,23 +1242,6 @@ bool registerFunction(BorrowedRef<PyFunctionObject> func) {
11511242 auto & jit_reg_units = cinderx::getModuleState ()->registeredCompilationUnits ();
11521243 jit_reg_units.emplace (func.getObj ());
11531244
1154- // Map this function's code object to itself.
1155- auto & jit_code_outer_funcs = jitCtx ()->codeOuterFunctions ();
1156- BorrowedRef<PyCodeObject> func_code{func->func_code };
1157- jit_code_outer_funcs.emplace (func_code, func);
1158-
1159- // If we have an active jit-list, scan this function's code object for any
1160- // nested functions that might be on the jit-list, and register them as well.
1161- if (cinderx::getModuleState ()->jitList () != nullptr ) {
1162- PyObject* module = func->func_module ;
1163- BorrowedRef<PyCodeObject> top_code{func->func_code };
1164- BorrowedRef<> top_consts{top_code->co_consts };
1165- for (BorrowedRef<PyCodeObject> code : findNestedCodes (module , top_consts)) {
1166- jit_reg_units.emplace (code.getObj ());
1167- jit_code_outer_funcs.emplace (code, func);
1168- }
1169- }
1170-
11711245 return true ;
11721246}
11731247
@@ -2952,6 +3026,39 @@ PyModuleDef jit_module = {
29523026 nullptr , /* m_free */
29533027};
29543028
3029+ void trackEligibleCodeObjects (
3030+ BorrowedRef<PyFunctionObject> func,
3031+ BorrowedRef<PyCodeObject> func_code,
3032+ JitEligibility eligibility = JitEligibility::Eligible) {
3033+ // We need to maintain a mapping for all functions which are
3034+ // eligible for compilation at some point - we track the code
3035+ // object and their parent function.
3036+ // Map this function's code object to itself.
3037+ auto & jit_code_outer_funcs = jitCtx ()->codeOuterFunctions ();
3038+ if (jit_code_outer_funcs.contains (func_code)) {
3039+ // already registered this code
3040+ return ;
3041+ }
3042+
3043+ auto & jit_reg_units = cinderx::getModuleState ()->registeredCompilationUnits ();
3044+
3045+ jit_code_outer_funcs.try_emplace (func_code, func);
3046+
3047+ // Scan this function's code object for any nested functions that
3048+ // might be compiled
3049+ PyObject* module = func->func_module ;
3050+ BorrowedRef<> top_consts{func_code->co_consts };
3051+ for (BorrowedRef<PyCodeObject> code : findNestedCodes (module , top_consts)) {
3052+ if (jit_code_outer_funcs.contains (code)) {
3053+ continue ;
3054+ }
3055+ jit_code_outer_funcs.emplace (code, func);
3056+ if (eligibility == JitEligibility::JitListEligible) {
3057+ jit_reg_units.emplace (code.getObj ());
3058+ }
3059+ }
3060+ }
3061+
29553062// Preload a function and its dependencies, then compile them all.
29563063//
29573064// Failing to compile a dependent function is a soft failure, and is ignored.
@@ -2960,10 +3067,13 @@ _PyJIT_Result compile_func(BorrowedRef<PyFunctionObject> func) {
29603067 // jitable function, resulting in a single-function compile
29613068 hir::IsolatedPreloaders ip;
29623069
2963- // Ensure the function's code object is mapped to itself.
2964- auto & jit_code_outer_funcs = jitCtx ()->codeOuterFunctions ();
2965- BorrowedRef<PyCodeObject> func_code{func->func_code };
2966- jit_code_outer_funcs.emplace (func_code, func);
3070+ // We generally track function objects when they are created. But we may need
3071+ // to re-track here. A function can have nested functions and those nested
3072+ // functions can out-live the function that created them. When the outer
3073+ // function is destroyed we need to remove the dangling registrations in
3074+ // codeOuterFunctions. We will treat whatever remains as new top-level
3075+ // functions.
3076+ trackEligibleCodeObjects (func, func->func_code );
29673077
29683078 // Collect a list of functions to compile. If it's empty then there must have
29693079 // been a Python error during preloading.
@@ -3128,6 +3238,46 @@ constexpr std::string_view getCpuArchName() {
31283238#endif
31293239}
31303240
3241+ // Unregister a function and its nested code objects from jit_reg_units and
3242+ // jit_code_outer_funcs. Called when a function is destroyed or its code object
3243+ // is being replaced.
3244+ void unregisterFunctionCodes (BorrowedRef<PyFunctionObject> func) {
3245+ if (!jitCtx ()) {
3246+ return ;
3247+ }
3248+ auto mod_state = cinderx::getModuleState ();
3249+ if (!mod_state) {
3250+ return ;
3251+ }
3252+
3253+ auto & jit_reg_units = mod_state->registeredCompilationUnits ();
3254+ auto & jit_code_outer_funcs = jitCtx ()->codeOuterFunctions ();
3255+
3256+ BorrowedRef<PyCodeObject> top_code{func->func_code };
3257+ auto it = jit_code_outer_funcs.find (top_code);
3258+ if (it != jit_code_outer_funcs.end () && it->second == func) {
3259+ jit_code_outer_funcs.erase (it);
3260+ PyObject* module = func->func_module ;
3261+ BorrowedRef<> top_consts{top_code->co_consts };
3262+ for (BorrowedRef<PyCodeObject> code : findNestedCodes (module , top_consts)) {
3263+ jit_reg_units.erase (code);
3264+ auto existing = jit_code_outer_funcs.find (code);
3265+ if (existing != jit_code_outer_funcs.end () && existing->second == func) {
3266+ jit_code_outer_funcs.erase (code);
3267+ }
3268+ if (handle_unit_deleted_during_preload != nullptr ) {
3269+ handle_unit_deleted_during_preload (code.getObj ());
3270+ }
3271+ }
3272+ }
3273+
3274+ jit_reg_units.erase (func);
3275+ jit_reg_units.erase (top_code);
3276+ if (handle_unit_deleted_during_preload != nullptr ) {
3277+ handle_unit_deleted_during_preload (func.getObj ());
3278+ }
3279+ }
3280+
31313281} // namespace
31323282
31333283#if PY_VERSION_HEX < 0x030C0000
@@ -3435,44 +3585,25 @@ void finalize() {
34353585}
34363586
34373587bool shouldScheduleCompile (BorrowedRef<PyFunctionObject> func) {
3438- // Can be called after the module has been finalized, due to function events.
3439- if (jitCtx () == nullptr ) {
3440- return false ;
3441- }
3442-
3443- if (isCinderModule (func->func_module )) {
3444- return false ;
3445- }
3446-
3447- // Note: This is not the same as fetching the function's code object and
3448- // checking its module and qualname, as functions can be renamed after they
3449- // are created. Code objects cannot.
3450- if (auto jit_list = cinderx::getModuleState ()->jitList ()) {
3451- return jit_list->lookupFunc (func) == 1 ;
3452- }
3453-
34543588 BorrowedRef<PyCodeObject> code{func->func_code };
34553589 return shouldAlwaysScheduleCompile (code) ||
34563590 getConfig ().compile_after_n_calls .has_value ();
34573591}
34583592
3459- bool shouldScheduleCompile (
3460- BorrowedRef<> module_name,
3461- BorrowedRef<PyCodeObject> code) {
3462- if (isCinderModule (module_name)) {
3593+ bool scheduleJitCompile (BorrowedRef<PyFunctionObject> func) {
3594+ auto eligible = getCompilationEligibility (func);
3595+ if (eligible == JitEligibility::Ineligible) {
34633596 return false ;
34643597 }
3598+ trackEligibleCodeObjects (func, func->func_code , eligible);
34653599
3466- if (auto jit_list = cinderx::getModuleState ()->jitList ()) {
3467- return jit_list->lookupCode (code) == 1 ||
3468- jit_list->lookupName (module_name, code->co_qualname ) == 1 ;
3600+ // If we're not eligible due to the JIT list check if we have config (e.g.
3601+ // auto jit, jit all, or jit all static methods) that makes compilation happen
3602+ // automatically.
3603+ if (eligible == JitEligibility::Eligible && !shouldScheduleCompile (func)) {
3604+ return false ;
34693605 }
34703606
3471- return shouldAlwaysScheduleCompile (code) ||
3472- getConfig ().compile_after_n_calls .has_value ();
3473- }
3474-
3475- bool scheduleJitCompile (BorrowedRef<PyFunctionObject> func) {
34763607 // Could be creating an inner function with an already-compiled code object.
34773608 if (isJitCompiled (func)) {
34783609 return true ;
@@ -3533,7 +3664,9 @@ std::vector<BorrowedRef<PyFunctionObject>> preloadFuncAndDeps(
35333664 worklist.push_back (func);
35343665
35353666 auto shouldPreload = [&](BorrowedRef<PyFunctionObject> f) {
3536- return !isPreloaded (f) && (shouldScheduleCompile (f) || forcePreload);
3667+ return !isPreloaded (f) &&
3668+ (forcePreload ||
3669+ getCompilationEligibility (f) != JitEligibility::Ineligible);
35373670 };
35383671
35393672 while (worklist.size () > 0 && result.size () < limit) {
@@ -3608,32 +3741,13 @@ void codeDestroyed(BorrowedRef<PyCodeObject> code) {
36083741}
36093742
36103743void funcDestroyed (BorrowedRef<PyFunctionObject> func) {
3611- if (isJitUsable ()) {
3612- auto mod_state = cinderx::getModuleState ();
3613-
3614- auto & jit_reg_units = mod_state->registeredCompilationUnits ();
3615- jit_reg_units.erase (func.getObj ());
3616- if (handle_unit_deleted_during_preload != nullptr ) {
3617- handle_unit_deleted_during_preload (func.getObj ());
3618- }
3619-
3620- // erase any child code objects we registered too
3621- if (mod_state->jitList () != nullptr ) {
3622- auto & jit_code_outer_funcs = jitCtx ()->codeOuterFunctions ();
3623- PyObject* module = func->func_module ;
3624- BorrowedRef<PyCodeObject> top_code{func->func_code };
3625- BorrowedRef<> top_consts{top_code->co_consts };
3626- for (BorrowedRef<PyCodeObject> code :
3627- findNestedCodes (module , top_consts)) {
3628- jit_reg_units.erase (code);
3629- jit_code_outer_funcs.erase (code);
3630- if (handle_unit_deleted_during_preload != nullptr ) {
3631- handle_unit_deleted_during_preload (code.getObj ());
3632- }
3633- }
3634- }
3744+ auto mod_state = cinderx::getModuleState ();
3745+ if (!mod_state) {
3746+ return ;
36353747 }
36363748
3749+ unregisterFunctionCodes (func);
3750+
36373751 // Have to check if context exists as this can fire after jit::finalize().
36383752 if (jitCtx ()) {
36393753 jitCtx ()->funcDestroyed (func);
@@ -3646,6 +3760,11 @@ void funcDestroyed(BorrowedRef<PyFunctionObject> func) {
36463760
36473761void funcModified (BorrowedRef<PyFunctionObject> func) {
36483762 deoptFunc (func);
3763+ // Clean up registrations for the old code object. At this point
3764+ // func->func_code still refers to the old code. The caller will update
3765+ // func->func_code and call scheduleCompile() to re-register with the new
3766+ // code.
3767+ unregisterFunctionCodes (func);
36493768}
36503769
36513770void typeDestroyed (BorrowedRef<PyTypeObject> type) {
@@ -3691,10 +3810,7 @@ _PyJIT_Result compilePreloaderImpl(
36913810 BorrowedRef<PyDictObject> builtins = preloader.builtins ();
36923811 BorrowedRef<PyDictObject> globals = preloader.globals ();
36933812
3694- // Don't care flags: CO_NOFREE, CO_FUTURE_* (the only still-relevant future is
3695- // "annotations" which doesn't impact bytecode execution.)
3696- int required_flags = CO_OPTIMIZED | CO_NEWLOCALS ;
3697- if ((code->co_flags & required_flags) != required_flags) {
3813+ if (!hasRequiredFlags (code)) {
36983814 JIT_DLOG (
36993815 " Can't compile {} due to missing required code flags" ,
37003816 preloader.fullname ());
0 commit comments