Skip to content

Commit 96d2866

Browse files
kddnewtonmeta-codesync[bot]
authored andcommitted
Skip RunPeriodicTasks for leaf functions in JIT
Summary: Trivial JIT-compiled functions like `typing.cast` — which just returns its second argument — generate ~70 instructions of prologue (frame setup, callee-save spills, RunPeriodicTasks check) and ~20 of epilogue, while the actual work is only 2 instructions (Incref + mov). This diff reduces that overhead for "leaf" functions. A function is a leaf if its bytecode contains only opcodes from a conservative allowlist that cannot invoke user Python code (LOAD_FAST, STORE_FAST, LOAD_CONST, RETURN_VALUE, POP_TOP, COPY, SWAP, etc.) and has no backward jumps (loops). The allowlist approach is safe against future bytecode additions since new opcodes are excluded by default. For leaf functions, two optimizations apply: 1. Skip the RunPeriodicTasks check at RESUME. This eliminates a LoadEvalBreaker, CondBranch, snapshot, and cold-path call to `_Py_HandlePending` from the function entry. This is safe because leaf functions execute a bounded, small number of instructions and return quickly — the caller will check periodic tasks at its next check point. The no-backward-jumps constraint ensures the function cannot loop, bounding its execution time. 2. Use a specialized `JITRT_UnlinkLeafFrame` at function exit. Since the function has no DeoptBase instructions, its interpreter frame can never be materialized at runtime, so we skip the materialization check (`header->rtfs & JIT_FRAME_INITIALIZED`) that `JITRT_UnlinkLightweightFrameFast` performs. A shared `cleanupLightweightFrameExecutable` helper is extracted to deduplicate the reifier cleanup logic between the two unlink functions. The `can_deopt` flag on Environ (set from `Function::canDeopt()` after all HIR passes) propagates this information from the HIR layer to the LIR exit block generator, which selects between `JITRT_UnlinkLeafFrame`, `JITRT_UnlinkLightweightFrameFast`, and `JITRT_UnlinkFrame` at JIT compile time. Before (def f(x): return x): 3 basic blocks, 11 HIR instructions including LoadEvalBreaker + CondBranch + RunPeriodicTasks After: 1 basic block, 5 HIR instructions (LoadArg, LoadCurrentFunc, LoadFrame, Snapshot, Return) Reviewed By: alexmalyshev Differential Revision: D103112548 fbshipit-source-id: 92eb90ee6935619c1498a0f02f1e21bca95c74fb
1 parent 982e4cb commit 96d2866

15 files changed

Lines changed: 214 additions & 346 deletions

cinderx/Jit/bytecode.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ bool BytecodeInstruction::isBranch() const {
129129
}
130130
}
131131

132+
bool BytecodeInstruction::isBackwardBranch() const {
133+
return isBranch() && getJumpTarget() <= baseOffset();
134+
}
135+
132136
bool BytecodeInstruction::isReturn() const {
133137
switch (opcode()) {
134138
case RETURN_CONST:

cinderx/Jit/bytecode.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class BytecodeInstruction {
4646
// Check if this instruction is a branch, a return, or a general basic block
4747
// terminator.
4848
bool isBranch() const;
49+
bool isBackwardBranch() const;
4950
bool isReturn() const;
5051
bool isTerminator() const;
5152

cinderx/Jit/codegen/environ.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ struct Environ {
170170
int win_struct_ret_offset{0};
171171
#endif
172172

173+
// True if the function has any DeoptBase instructions in its final HIR.
174+
// When false, the interpreter frame can never be materialized, enabling a
175+
// cheaper inline frame unlink at exit.
176+
bool can_deopt{true};
177+
173178
#if defined(CINDER_AARCH64)
174179
// Constant pool for large immediate values. translateMovConstPool populates
175180
// these; gen_asm.cpp emits the pool data after deopt exits.

cinderx/Jit/codegen/gen_asm.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,8 @@ void* NativeGenerator::getVectorcallEntry() {
727727
env_.changed_regs = lsalloc.getChangedRegs();
728728
env_.exit_label = as_->newLabel();
729729
env_.frame_mode = GetFunction()->frameMode;
730+
env_.can_deopt = GetFunction()->canDeopt();
731+
730732
JIT_LOGIF(
731733
getConfig().log.dump_lir,
732734
"LIR for {} after register allocation:\n{}",

cinderx/Jit/hir/builder.cpp

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,42 @@ BasicBlock* HIRBuilder::getBlockAtOff(BCOffset off) {
515515
return it->second;
516516
}
517517

518+
bool HIRBuilder::isSimpleLeafFunction(BorrowedRef<PyCodeObject> code) {
519+
if (code->co_flags & kCoFlagsAnyGenerator) {
520+
return false;
521+
}
522+
for (auto& instr : BytecodeInstructionBlock{code}) {
523+
switch (instr.opcode()) {
524+
case COPY:
525+
case LOAD_CONST:
526+
case LOAD_FAST:
527+
case LOAD_FAST_AND_CLEAR:
528+
case LOAD_FAST_BORROW:
529+
case LOAD_FAST_BORROW_LOAD_FAST_BORROW:
530+
case LOAD_FAST_CHECK:
531+
case LOAD_FAST_LOAD_FAST:
532+
case NOP:
533+
case NOT_TAKEN:
534+
case POP_TOP:
535+
case PUSH_NULL:
536+
case RESUME:
537+
case RETURN_CONST:
538+
case RETURN_VALUE:
539+
case STORE_FAST:
540+
case STORE_FAST_LOAD_FAST:
541+
case STORE_FAST_STORE_FAST:
542+
case SWAP:
543+
break;
544+
default:
545+
return false;
546+
}
547+
if (instr.isBackwardBranch()) {
548+
return false;
549+
}
550+
}
551+
return true;
552+
}
553+
518554
std::unique_ptr<Function> buildHIR(const Preloader& preloader) {
519555
return HIRBuilder{preloader}.buildHIR();
520556
}
@@ -534,6 +570,8 @@ std::unique_ptr<Function> buildHIR(const Preloader& preloader) {
534570
std::unique_ptr<Function> HIRBuilder::buildHIR() {
535571
checkTranslate();
536572

573+
is_simple_leaf_function_ = isSimpleLeafFunction(code_);
574+
537575
std::unique_ptr<Function> irfunc = preloader_.makeFunction();
538576
buildHIRImpl(irfunc.get(), /*frame_state=*/nullptr);
539577
// Use removeTrampolineBlocks and removeUnreachableBlocks directly instead of
@@ -1090,19 +1128,17 @@ void HIRBuilder::translate(
10901128
}
10911129
case POP_JUMP_IF_FALSE:
10921130
case POP_JUMP_IF_TRUE: {
1093-
BCOffset target_off = bc_instr.getJumpTarget();
1094-
BasicBlock* target = getBlockAtOff(target_off);
1095-
if (target_off <= bc_instr.baseOffset()) {
1131+
BasicBlock* target = getBlockAtOff(bc_instr.getJumpTarget());
1132+
if (bc_instr.isBackwardBranch()) {
10961133
loop_headers.emplace(target);
10971134
}
10981135
emitPopJumpIf(tc, bc_instr);
10991136
break;
11001137
}
11011138
case POP_JUMP_IF_NONE:
11021139
case POP_JUMP_IF_NOT_NONE: {
1103-
BCOffset target_off = bc_instr.getJumpTarget();
1104-
BasicBlock* target = getBlockAtOff(target_off);
1105-
if (target_off <= bc_instr.baseOffset()) {
1140+
BasicBlock* target = getBlockAtOff(bc_instr.getJumpTarget());
1141+
if (bc_instr.isBackwardBranch()) {
11061142
loop_headers.emplace(target);
11071143
}
11081144
emitPopJumpIfNone(tc, bc_instr);
@@ -1936,6 +1972,9 @@ void HIRBuilder::emitResume(
19361972
if (bc_instr.oparg() >= 2) {
19371973
return;
19381974
}
1975+
if (is_simple_leaf_function_) {
1976+
return;
1977+
}
19391978
TranslationContext succ(cfg.AllocateBlock(), tc.frame);
19401979
succ.emitSnapshot();
19411980
insertRunPeriodicActivites(cfg, tc.block, succ.block, tc.frame);

cinderx/Jit/hir/builder.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,15 @@ class HIRBuilder {
554554
Register* kwnames_{nullptr};
555555

556556
OperandStack static_method_stack_;
557+
558+
// True if the function's bytecode contains only opcodes that cannot invoke
559+
// user Python code and has no backward jumps (loops). Stricter than the
560+
// common "leaf function" definition (no calls) — this also requires no
561+
// complex opcodes. Used to skip RunPeriodicTasks at RESUME since the
562+
// function returns quickly and the caller will check periodic tasks.
563+
bool is_simple_leaf_function_{false};
564+
565+
static bool isSimpleLeafFunction(BorrowedRef<PyCodeObject> code);
557566
};
558567

559568
} // namespace jit::hir

cinderx/Jit/jit_rt.cpp

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,30 @@ void JITRT_UnlinkFrame(PyThreadState* tstate) {
854854
// JIT frames are stack allocated so there's nothing to pop.
855855
}
856856

857+
// Clean up the reifier and decref the executable for a lightweight frame.
858+
// Shared by JITRT_UnlinkLightweightFrameFast and JITRT_UnlinkLeafFrame.
859+
static void cleanupLightweightFrameExecutable(
860+
_PyInterpreterFrame* frame,
861+
[[maybe_unused]] jit::FrameHeader* header) {
862+
#if PY_VERSION_HEX >= 0x030E0000
863+
PyStackRef_CLOSE(frame->f_executable);
864+
#else
865+
// Replace the reifier in f_funcobj with the actual function so that any
866+
// escaped references to the frame see a valid function pointer, not a
867+
// dangling reifier callback.
868+
if (jit::hasRtfsFunction(frame)) {
869+
frame->f_funcobj = jit::jitFrameGetRtfs(frame)->func();
870+
} else {
871+
PyObject* func = jit::jitFrameGetFunction(frame);
872+
frame->f_funcobj = func;
873+
Py_XDECREF(func);
874+
header->rtfs = JIT_FRAME_INITIALIZED;
875+
}
876+
877+
Py_DECREF(frameExecutable(frame));
878+
#endif
879+
}
880+
857881
void JITRT_UnlinkLightweightFrameFast(PyThreadState* tstate) {
858882
_PyInterpreterFrame* frame = currentFrame(tstate);
859883
setCurrentFrame(tstate, frame->previous);
@@ -881,22 +905,19 @@ void JITRT_UnlinkLightweightFrameFast(PyThreadState* tstate) {
881905
Ci_STACK_CLOSE(frame->f_funcobj);
882906
}
883907

884-
#if PY_VERSION_HEX >= 0x030E0000
885-
PyStackRef_CLOSE(frame->f_executable);
886-
#else
887-
// We can't leave our reifier dangling here otherwise we may
888-
// continue to get callbacks, instead leave the function dangling.
889-
if (jit::hasRtfsFunction(frame)) {
890-
frame->f_funcobj = jit::jitFrameGetRtfs(frame)->func();
891-
} else {
892-
PyObject* func = jit::jitFrameGetFunction(frame);
893-
frame->f_funcobj = func;
894-
Py_XDECREF(func);
895-
header->rtfs = JIT_FRAME_INITIALIZED;
896-
}
908+
cleanupLightweightFrameExecutable(frame, header);
909+
}
897910

898-
Py_DECREF(frameExecutable(frame));
899-
#endif
911+
void JITRT_UnlinkLeafFrame(PyThreadState* tstate) {
912+
_PyInterpreterFrame* frame = currentFrame(tstate);
913+
setCurrentFrame(tstate, frame->previous);
914+
915+
// No deopts means the frame was never materialized — skip the
916+
// materialization check and just close funcobj + executable directly.
917+
Ci_STACK_CLOSE(frame->f_funcobj);
918+
919+
auto* header = reinterpret_cast<jit::FrameHeader*>(frame) - 1;
920+
cleanupLightweightFrameExecutable(frame, header);
900921
}
901922

902923
PyObject*

cinderx/Jit/jit_rt.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ void JITRT_UnlinkFrame(PyThreadState* tstate);
7171
// jitFrameClearExceptCode path.
7272
void JITRT_UnlinkLightweightFrameFast(PyThreadState* tstate);
7373

74+
// Specialized version for non-deopting leaf functions with lightweight frames.
75+
// Since no deopts can occur, the frame is guaranteed to never be materialized,
76+
// so we skip the materialization check entirely.
77+
void JITRT_UnlinkLeafFrame(PyThreadState* tstate);
78+
7479
/*
7580
* Handles a call that includes kw arguments where the target function has
7681
* *args, **kwargs, or keyword only args.

cinderx/Jit/lir/generator.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -935,11 +935,18 @@ void LIRGenerator::GenerateExitBlocks() {
935935
Instruction::kPhi, nullptr, OutVReg{ret_data_type});
936936

937937
// Unlink frame before epilogue. Non-generators always unlink.
938+
bool has_freevars = func_->code != nullptr && func_->code->co_nfreevars > 0;
939+
bool uses_lw_frames = getConfig().frame_mode == FrameMode::kLightweight;
940+
uint64_t helper;
941+
if (!env_->can_deopt && uses_lw_frames && !has_freevars) {
942+
helper = reinterpret_cast<uint64_t>(JITRT_UnlinkLeafFrame);
943+
} else if (!has_freevars && uses_lw_frames) {
944+
helper = reinterpret_cast<uint64_t>(JITRT_UnlinkLightweightFrameFast);
945+
} else {
946+
helper = reinterpret_cast<uint64_t>(JITRT_UnlinkFrame);
947+
}
938948
block->allocateInstr(
939-
Instruction::kCall,
940-
nullptr,
941-
Imm{reinterpret_cast<uint64_t>(JITRT_UnlinkFrame)},
942-
VReg{env_->asm_tstate});
949+
Instruction::kCall, nullptr, Imm{helper}, VReg{env_->asm_tstate});
943950

944951
block->allocateInstr(Instruction::kEpilogueEnd, nullptr, VReg{exit_phi_});
945952
return;

cinderx/RuntimeTests/hir_tests/all_passes_test.txt

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,8 @@ def test():
1111
fun jittestmodule:test {
1212
bb 0 {
1313
LoadFrame
14-
UpdatePrevInstr<idx:0 line_no:1: no parent>
15-
v5:CInt32 = LoadEvalBreaker
16-
CondBranch<2, 1> v5
17-
}
18-
19-
bb 2 (preds 0) {
20-
v6:CInt32 = RunPeriodicTasks {
21-
FrameState {
22-
CurInstrOffset 0
23-
}
24-
}
25-
Branch<1>
26-
}
27-
28-
bb 1 (preds 0, 2) {
29-
v7:ImmortalLongExact[1] = LoadConst<ImmortalLongExact[1]>
30-
Return<ImmortalLongExact[1]> v7
14+
v3:ImmortalLongExact[1] = LoadConst<ImmortalLongExact[1]>
15+
Return<ImmortalLongExact[1]> v3
3116
}
3217
}
3318
--- Expected 3.14 ---

0 commit comments

Comments
 (0)