Skip to content

Commit 447471a

Browse files
alexmalyshevmeta-codesync[bot]
authored andcommitted
Lazily install the CinderX frame evaluator function
Summary: Doing this on module init is risky, we have features in CinderX that don't need the frame evaluator (e.g. Parallel GC) and so we don't want it to be unconditionally enabled. We know we have issues with PGO not being applied on the frame evaluator in all builds, so best to only turn it on when we know its going to be needed. The cases where this has to be turned on: * The JIT is enabled and is going to start compiling code. * The Static Python bytecode compiler is used. * Shadowcode is enabled (Python 3.10 only). Reviewed By: DinoV, jbower-fb Differential Revision: D92628744 fbshipit-source-id: 6acb7c4c9bc811b6ab42454d68fbadf6652d8d0b
1 parent f6cc400 commit 447471a

13 files changed

Lines changed: 319 additions & 59 deletions

File tree

CMakeLists.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,11 @@ target_link_libraries(cached-properties PRIVATE common borrowed)
254254
########################################
255255
# Interpreter/
256256

257-
set(INTERP_SOURCES ${PROJECT_SOURCE_DIR}/Interpreter/cinder_opcode.c ${PROJECT_SOURCE_DIR}/Interpreter/iter_helpers.c)
257+
set(INTERP_SOURCES
258+
${PROJECT_SOURCE_DIR}/Interpreter/cinder_opcode.c
259+
${PROJECT_SOURCE_DIR}/Interpreter/interpreter_base.cpp
260+
${PROJECT_SOURCE_DIR}/Interpreter/iter_helpers.c
261+
)
258262

259263
if (${PY_VERSION} EQUAL 3.12 OR ${PY_VERSION} EQUAL 3.14 OR ${PY_VERSION} EQUAL 3.15)
260264
set(SOURCE_CINDERX_OPCODE_TARGETS_H ${PROJECT_SOURCE_DIR}/Interpreter/${PY_VERSION}/cinderx_opcode_targets.h)

cinderx/Interpreter/interpreter.h

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@
44

55
#include "cinderx/python.h"
66

7-
#include "cinderx/Common/extra-py-flags.h"
8-
#include "cinderx/UpstreamBorrow/borrowed.h"
7+
// Exporting Ci_PyFunction_Vectorcall.
98
#include "cinderx/module_c_state.h"
10-
#if PY_VERSION_HEX >= 0x030D0000
11-
#include "internal/pycore_function.h"
12-
#endif
9+
10+
#include <stdbool.h>
11+
#include <stdint.h>
12+
1313
#ifdef __cplusplus
1414
extern "C" {
1515
#endif
1616

17+
/*
18+
* The CinderX frame evaluator function (interpreter loop).
19+
*/
1720
#if PY_VERSION_HEX < 0x030C0000
1821
PyObject* _Py_HOT_FUNCTION
1922
Ci_EvalFrame(PyThreadState* tstate, PyFrameObject* f, int throwflag);
@@ -51,17 +54,17 @@ PyObject* Ci_PyFunction_CallStatic(
5154
* This is a different function for Static Python functions versus "normal"
5255
* Python functions.
5356
*/
54-
static inline vectorcallfunc getInterpretedVectorcall(
55-
[[maybe_unused]] const PyFunctionObject* func) {
56-
#ifdef ENABLE_INTERPRETER_LOOP
57-
const PyCodeObject* code = (const PyCodeObject*)(func->func_code);
58-
return (code->co_flags & CI_CO_STATICALLY_COMPILED)
59-
? Ci_StaticFunction_Vectorcall
60-
: Ci_PyFunction_Vectorcall;
61-
#else
62-
return Ci_PyFunction_Vectorcall;
63-
#endif
64-
}
57+
vectorcallfunc getInterpretedVectorcall(const PyFunctionObject* func);
58+
59+
/*
60+
* Install the CinderX frame evaluator function into the runtime.
61+
*/
62+
int Ci_InitFrameEvalFunc();
63+
64+
/*
65+
* Remove the CinderX frame evaluator function from the runtime.
66+
*/
67+
void Ci_FiniFrameEvalFunc();
6568

6669
void Ci_InitOpcodes();
6770

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
3+
#include "cinderx/Common/extra-py-flags.h"
4+
#include "cinderx/Interpreter/interpreter.h"
5+
#include "cinderx/UpstreamBorrow/borrowed.h"
6+
7+
#if PY_VERSION_HEX >= 0x030D0000
8+
#include "internal/pycore_function.h"
9+
#endif
10+
11+
#if PY_VERSION_HEX < 0x030D0000 && defined(ENABLE_EVAL_HOOK)
12+
#include "cinder/hooks.h"
13+
#endif
14+
15+
extern "C" {
16+
17+
vectorcallfunc getInterpretedVectorcall(
18+
[[maybe_unused]] const PyFunctionObject* func) {
19+
#ifdef ENABLE_INTERPRETER_LOOP
20+
const PyCodeObject* code = (const PyCodeObject*)(func->func_code);
21+
return (code->co_flags & CI_CO_STATICALLY_COMPILED)
22+
? Ci_StaticFunction_Vectorcall
23+
: Ci_PyFunction_Vectorcall;
24+
#else
25+
return Ci_PyFunction_Vectorcall;
26+
#endif
27+
}
28+
29+
int Ci_InitFrameEvalFunc() {
30+
#ifdef ENABLE_INTERPRETER_LOOP
31+
#ifdef ENABLE_EVAL_HOOK
32+
Ci_hook_EvalFrame = Ci_EvalFrame;
33+
#elif defined(ENABLE_PEP523_HOOK)
34+
// Let borrowed.h know the eval frame pointer
35+
Ci_EvalFrameFunc = Ci_EvalFrame;
36+
37+
auto interp = _PyInterpreterState_GET();
38+
auto current_eval_frame = _PyInterpreterState_GetEvalFrameFunc(interp);
39+
if (current_eval_frame == Ci_EvalFrame) {
40+
return 0;
41+
}
42+
if (current_eval_frame != _PyEval_EvalFrameDefault) {
43+
PyErr_SetString(
44+
PyExc_RuntimeError,
45+
"CinderX tried to set a frame evaluator function but something else "
46+
"has done it first, this is not supported");
47+
return -1;
48+
}
49+
50+
_PyInterpreterState_SetEvalFrameFunc(interp, Ci_EvalFrame);
51+
#endif
52+
#endif
53+
54+
return 0;
55+
}
56+
57+
void Ci_FiniFrameEvalFunc() {
58+
#ifdef ENABLE_INTERPRETER_LOOP
59+
#ifdef ENABLE_EVAL_HOOK
60+
Ci_hook_EvalFrame = nullptr;
61+
#elif defined(ENABLE_PEP523_HOOK)
62+
_PyInterpreterState_SetEvalFrameFunc(_PyInterpreterState_GET(), nullptr);
63+
#endif
64+
#endif
65+
}
66+
67+
} // extern "C"

cinderx/Jit/pyjit.cpp

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,14 +1472,20 @@ PyObject* patched_sys_settrace(
14721472

14731473
#endif // PY_VERSION_HEX >= 0x030C0000
14741474

1475-
void compile_after_n_calls_impl(uint32_t calls) {
1475+
int compile_after_n_calls_impl(uint32_t calls) {
1476+
if (Ci_InitFrameEvalFunc() < 0) {
1477+
return -1;
1478+
}
1479+
14761480
getMutableConfig().compile_after_n_calls = calls;
14771481

14781482
// Schedule all pre-existing functions for compilation.
14791483
walkFunctionObjects(
14801484
[](BorrowedRef<PyFunctionObject> func) { scheduleJitCompile(func); });
14811485

14821486
JIT_DLOG("Configuring JIT to compile functions after {} calls", calls);
1487+
1488+
return 0;
14831489
}
14841490

14851491
PyObject* compile_after_n_calls(PyObject* /* self */, PyObject* arg) {
@@ -1495,14 +1501,18 @@ PyObject* compile_after_n_calls(PyObject* /* self */, PyObject* arg) {
14951501
return nullptr;
14961502
}
14971503

1498-
compile_after_n_calls_impl(calls);
1504+
if (compile_after_n_calls_impl(calls) < 0) {
1505+
return nullptr;
1506+
}
14991507

15001508
Py_RETURN_NONE;
15011509
}
15021510

15031511
PyObject* auto_jit(PyObject* /* self */, PyObject* /* arg */) {
15041512
// Default value that works well for most applications.
1505-
compile_after_n_calls_impl(1000);
1513+
if (compile_after_n_calls_impl(1000) < 0) {
1514+
return nullptr;
1515+
}
15061516

15071517
Py_RETURN_NONE;
15081518
}
@@ -1567,6 +1577,10 @@ PyObject* force_compile(PyObject* /* self */, PyObject* arg) {
15671577
Py_RETURN_FALSE;
15681578
}
15691579

1580+
if (Ci_InitFrameEvalFunc() < 0) {
1581+
return nullptr;
1582+
}
1583+
15701584
_PyJIT_Result result = compileFunction(func);
15711585
switch (result) {
15721586
case PYJIT_RESULT_OK:
@@ -1619,6 +1633,10 @@ PyObject* lazy_compile(PyObject* /* self */, PyObject* arg) {
16191633
Py_RETURN_FALSE;
16201634
}
16211635

1636+
if (Ci_InitFrameEvalFunc() < 0) {
1637+
return nullptr;
1638+
}
1639+
16221640
func->vectorcall = forcedJitVectorcall;
16231641
if (!registerFunction(func)) {
16241642
func->vectorcall = getInterpretedVectorcall(func);
@@ -1884,13 +1902,19 @@ void deleteJitList() {
18841902

18851903
// Reschedule all functions on the JIT list for compilation. Run when the JIT
18861904
// list is modified.
1887-
void rescheduleJitList() {
1905+
int rescheduleJitList() {
1906+
if (Ci_InitFrameEvalFunc() < 0) {
1907+
return -1;
1908+
}
1909+
18881910
walkFunctionObjects([](BorrowedRef<PyFunctionObject> func) {
18891911
auto jit_list = cinderx::getModuleState()->jitList();
18901912
if (jit_list->lookupFunc(func)) {
18911913
scheduleJitCompile(func);
18921914
}
18931915
});
1916+
1917+
return 0;
18941918
}
18951919

18961920
PyObject* append_jit_list(PyObject* /* self */, PyObject* arg) {
@@ -1924,7 +1948,9 @@ PyObject* append_jit_list(PyObject* /* self */, PyObject* arg) {
19241948
return nullptr;
19251949
}
19261950

1927-
rescheduleJitList();
1951+
if (rescheduleJitList() < 0) {
1952+
return nullptr;
1953+
}
19281954

19291955
Py_RETURN_NONE;
19301956
}
@@ -1960,7 +1986,9 @@ PyObject* read_jit_list(PyObject* /* self */, PyObject* arg) {
19601986
return nullptr;
19611987
}
19621988

1963-
rescheduleJitList();
1989+
if (rescheduleJitList() < 0) {
1990+
return nullptr;
1991+
}
19641992

19651993
Py_RETURN_NONE;
19661994
}
@@ -3364,9 +3392,13 @@ int initialize() {
33643392
// startup, start scheduling functions for compilation now.
33653393
if (auto compile_n = getConfig().compile_after_n_calls;
33663394
compile_n.has_value()) {
3367-
compile_after_n_calls_impl(*compile_n);
3395+
if (compile_after_n_calls_impl(*compile_n) < 0) {
3396+
return -1;
3397+
}
33683398
} else if (mod_state->jitList() != nullptr) {
3369-
rescheduleJitList();
3399+
if (rescheduleJitList() < 0) {
3400+
return -1;
3401+
}
33703402
}
33713403

33723404
return 0;

cinderx/PythonLib/cinderx/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ def is_supported_runtime() -> bool:
8686
get_parallel_gc_settings,
8787
has_parallel_gc,
8888
immortalize_heap,
89+
install_frame_evaluator,
90+
is_frame_evaluator_installed,
8991
is_immortal,
92+
remove_frame_evaluator,
9093
strict_module_patch,
9194
strict_module_patch_delete,
9295
strict_module_patch_enabled,
@@ -501,11 +504,20 @@ def has_parallel_gc() -> bool:
501504
def immortalize_heap() -> None:
502505
pass
503506

507+
def install_frame_evaluator() -> None:
508+
pass
509+
510+
def is_frame_evaluator_installed() -> bool:
511+
return False
512+
504513
def is_immortal(obj: object) -> bool:
505514
raise RuntimeError(
506515
"Can't answer whether an object is mortal or immortal from Python code"
507516
)
508517

518+
def remove_frame_evaluator() -> None:
519+
pass
520+
509521
def strict_module_patch(mod: object, name: str, value: object) -> None:
510522
pass
511523

cinderx/PythonLib/cinderx/compiler/strict/loader.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
from types import CodeType, ModuleType
4747
from typing import Callable, cast, Collection, final, Iterable, Mapping
4848

49-
from _cinderx import StrictModule, watch_sys_modules
49+
from _cinderx import install_frame_evaluator, StrictModule, watch_sys_modules
5050
from cinderx.static import install_sp_audit_hook
5151

5252
from ..consts import CI_CO_STATICALLY_COMPILED
@@ -795,6 +795,7 @@ def init_static_python() -> None:
795795
796796
Should be called at least once if any Static modules/functions exist.
797797
"""
798+
install_frame_evaluator()
798799
watch_sys_modules()
799800
install_sp_audit_hook()
800801

cinderx/PythonLib/test_cinderx/test_compiler/test_static/compile.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
import cinderx.jit
2626
import xxclassloader
27-
from cinderx import StrictModule
27+
from cinderx import install_frame_evaluator, StrictModule
2828
from cinderx.compiler.consts import CI_CO_STATICALLY_COMPILED
2929
from cinderx.compiler.pycodegen import PythonCodeGenerator
3030
from cinderx.compiler.static import StaticCodeGenerator
@@ -107,6 +107,8 @@ def __class_getitem__(cls, elem_type):
107107
code = comp.compile("xxclassloader", "", tree, codestr, optimize=0)
108108
d = {"<builtins>": builtins.__dict__}
109109
add_fixed_module(d)
110+
111+
install_frame_evaluator()
110112
exec(code, d, d)
111113

112114
xxclassloader.XXGeneric = d["XXGeneric"]

0 commit comments

Comments
 (0)