Skip to content

Commit 4ff3a46

Browse files
DinoVmeta-codesync[bot]
authored andcommitted
Directly embed deopt idx instead of return address
Summary: On ARM there's no way to reliably recover where the return address is stored by the callee. They are free to first store some additional data and then setup the frame - for example they may initially save callee saved registers to ensure that the values in their frame that they care about are closer. We could do something unsafe like search the stack for our frame pointer and then gather the IP address from there but if a callee preserved register collided with our frame pointer we'd return incorrect results. Instead this removes storing the return address for every call and instead stores the deopt index. This takes the same amount of instructions but we do it less as not every call is a deopt. We now typically generate: ``` mov x14, #1 str x14, [x19] ``` Instead of: ``` adr x13, #0xfffe3633032c str x13, [x29, #0x10] ``` But for example we generate none of these for decref where we previously generated 2. We could instead save an index which is the bytecode offset for each line number as that's what we really care about currently but the free-threaded work is going to drive us to actually care about the deopt index instead so we may as well just start there. Reviewed By: alexmalyshev Differential Revision: D97524722 fbshipit-source-id: 2fb51c6848451496f340ccee42548829705c26f7
1 parent 9d44ce1 commit 4ff3a46

13 files changed

Lines changed: 212 additions & 73 deletions

cinderx/Jit/code_runtime.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,18 @@ int CodeRuntime::traverse(visitproc visit, void* arg) {
153153
return 0;
154154
}
155155

156+
std::optional<UnitCallStack> CodeRuntime::getUnitCallStackFromDeoptIdx(
157+
std::size_t deopt_idx) const {
158+
if (deopt_idx >= deopt_metadatas_.size()) {
159+
return std::nullopt;
160+
}
161+
const DeoptMetadata& meta = deopt_metadatas_[deopt_idx];
162+
UnitCallStack stack;
163+
stack.reserve(meta.frame_meta.size());
164+
for (const auto& frame : meta.frame_meta) {
165+
stack.emplace_back(frame.code, frame.cause_instr_idx);
166+
}
167+
return stack;
168+
}
169+
156170
} // namespace jit

cinderx/Jit/code_runtime.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ class alignas(16) CodeRuntime {
136136
// True if the references have been cleared
137137
bool isCleared() const;
138138

139+
// Get the UnitCallStack from a deopt metadata index.
140+
std::optional<UnitCallStack> getUnitCallStackFromDeoptIdx(
141+
std::size_t deopt_idx) const;
142+
139143
#if PY_VERSION_HEX >= 0x030E0000 && defined(ENABLE_LIGHTWEIGHT_FRAMES)
140144
void setReifier(BorrowedRef<> reifier) {
141145
ThreadedCompileSerialize guard;

cinderx/Jit/codegen/arch.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ constexpr auto reg_stack_pointer_loc = SP;
8686
constexpr auto fp = asmjit::a64::x29;
8787
constexpr auto lr = asmjit::a64::x30;
8888

89+
// Size of the AArch64 frame record: saved FP + LR (two 64-bit registers).
90+
constexpr int kFrameRecordSize = 2 * sizeof(void*);
91+
8992
} // namespace jit::codegen::arch
9093

9194
#else

cinderx/Jit/codegen/autogen.cpp

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,7 +1333,7 @@ void translateEpilogueEnd(Environ* env, const Instruction* instr) {
13331333
restoreCalleeSavedRegsAarch64(as, saved_regs);
13341334
}
13351335
as->mov(a64::sp, arch::fp);
1336-
as->ldp(arch::fp, arch::lr, a64::ptr_post(a64::sp, 32));
1336+
as->ldp(arch::fp, arch::lr, a64::ptr_post(a64::sp, arch::kFrameRecordSize));
13371337
as->ret(arch::lr);
13381338
#else
13391339
CINDER_UNSUPPORTED
@@ -1935,32 +1935,21 @@ void translateCall(Environ* env, const Instruction* instr) {
19351935
auto output = instr->output();
19361936
auto input = instr->getInput(0);
19371937

1938-
// Load call target into a register, then save the return address at
1939-
// [fp + 16] before calling. This allows getIP() to find the return address
1940-
// at a fixed offset from the frame base, which is needed for cross-thread
1941-
// frame inspection (e.g. sys._current_frames()).
1942-
a64::Gp target;
19431938
if (input->isReg()) {
1944-
target = AT::getGp(input);
1939+
as->blr(AT::getGp(input));
19451940
} else if (input->isImm()) {
19461941
as->mov(arch::reg_scratch_br, input->getConstant());
1947-
target = arch::reg_scratch_br;
1942+
as->blr(arch::reg_scratch_br);
19481943
} else if (input->isStack()) {
19491944
auto loc = input->getStackSlot().loc;
19501945
as->ldr(
19511946
arch::reg_scratch_br,
19521947
arch::ptr_resolve(as, arch::fp, loc, arch::reg_scratch_0));
1953-
target = arch::reg_scratch_br;
1948+
as->blr(arch::reg_scratch_br);
19541949
} else {
19551950
JIT_ABORT("Unsupported operand type for Call: {}", input->type());
19561951
}
19571952

1958-
asmjit::Label after_call = as->newLabel();
1959-
as->adr(arch::reg_scratch_0, after_call);
1960-
as->str(arch::reg_scratch_0, a64::ptr(arch::fp, 16));
1961-
as->blr(target);
1962-
as->bind(after_call);
1963-
19641953
if (instr->origin()) {
19651954
asmjit::Label label = as->newLabel();
19661955
as->bind(label);

cinderx/Jit/codegen/frame_asm.cpp

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -681,13 +681,35 @@ void FrameAsm::linkLightWeightFunctionFrame(
681681
arch::sub_immediate(as_, arch::reg_scratch_0, arch::fp, frame_header_size);
682682
as_->str(a64::xzr, a64::ptr(arch::reg_scratch_0));
683683
env_.addAnnotation("Store rtfs state to 0", store_func_cursor);
684+
// Initialize deopt_idx to 0 in FrameHeader.
685+
// reg_scratch_0 already holds the frame header base address from above.
686+
{
687+
asmjit::BaseNode* store_deopt_idx_cursor_arm = as_->cursor();
688+
as_->str(
689+
a64::xzr,
690+
a64::ptr(
691+
arch::reg_scratch_0,
692+
static_cast<int>(offsetof(FrameHeader, deopt_idx))));
693+
env_.addAnnotation("Store deopt_idx to 0", store_deopt_idx_cursor_arm);
694+
}
684695
#else
685696
// Initialize the fields minus previous.
686697
// Store func before the header
687698
arch::sub_immediate(as_, arch::reg_scratch_0, arch::fp, frame_header_size);
688699
as_->str(func_reg, a64::ptr(arch::reg_scratch_0));
689700
incRef(func_reg, ref_cnt, ref_cnt_scratch, tstate_reg);
690701
env_.addAnnotation("Store func before frame header", store_func_cursor);
702+
// Initialize deopt_idx to 0 in FrameHeader (3.12 arm64).
703+
// reg_scratch_0 still holds the frame header base address.
704+
{
705+
asmjit::BaseNode* store_deopt_idx_cursor_312_arm = as_->cursor();
706+
as_->str(
707+
a64::xzr,
708+
a64::ptr(
709+
arch::reg_scratch_0,
710+
static_cast<int>(offsetof(FrameHeader, deopt_idx))));
711+
env_.addAnnotation("Store deopt_idx to 0", store_deopt_idx_cursor_312_arm);
712+
}
691713
#endif
692714

693715
asmjit::BaseNode* store_f_code_cursor = as_->cursor();
@@ -821,7 +843,7 @@ void FrameAsm::linkLightWeightFunctionFrame(
821843

822844
// Then finally link in our frame to thread state
823845
asmjit::BaseNode* update_linkage_cursor = as_->cursor();
824-
int size = -frame_header_size + sizeof(PyObject*);
846+
int size = -frame_header_size + sizeof(FrameHeader);
825847
arch::add_signed_immediate(as_, scratch, arch::fp, size);
826848

827849
#if PY_VERSION_HEX >= 0x030D0000

cinderx/Jit/codegen/gen_asm.cpp

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -850,7 +850,7 @@ void* generateDeoptTrampoline(bool generator_mode) {
850850
a.ldr(arch::reg_scratch_br, epilogue_addr);
851851
// Remove our frame from the stack
852852
a.mov(a64::sp, arch::fp);
853-
a.ldp(arch::fp, arch::lr, a64::ptr_post(a64::sp, 16));
853+
a.ldp(arch::fp, arch::lr, a64::ptr_post(a64::sp, arch::kFrameRecordSize));
854854
a.br(arch::reg_scratch_br);
855855
annot.add("Jump to real epilogue", &a, annot_cursor);
856856

@@ -910,7 +910,7 @@ void* generateFailedDeferredCompileTrampoline() {
910910
#elif defined(CINDER_AARCH64)
911911
auto annot_cursor = a.cursor();
912912

913-
a.stp(arch::fp, arch::lr, a64::ptr_pre(a64::sp, -16));
913+
a.stp(arch::fp, arch::lr, a64::ptr_pre(a64::sp, -arch::kFrameRecordSize));
914914
a.mov(arch::fp, a64::sp);
915915

916916
// save incoming arg registers
@@ -925,7 +925,7 @@ void* generateFailedDeferredCompileTrampoline() {
925925
a.mov(arch::reg_scratch_br, JITRT_FailedDeferredCompileShim);
926926
a.blr(arch::reg_scratch_br);
927927
a.mov(a64::sp, arch::fp);
928-
a.ldp(arch::fp, arch::lr, a64::ptr_post(a64::sp, 16));
928+
a.ldp(arch::fp, arch::lr, a64::ptr_post(a64::sp, arch::kFrameRecordSize));
929929
a.ret(arch::lr);
930930
#else
931931
CINDER_UNSUPPORTED
@@ -1276,7 +1276,7 @@ void NativeGenerator::generateFunctionEntry() {
12761276
as_->push(x86::rbp);
12771277
as_->mov(x86::rbp, x86::rsp);
12781278
#elif defined(CINDER_AARCH64)
1279-
as_->stp(arch::fp, arch::lr, a64::ptr_pre(a64::sp, -32));
1279+
as_->stp(arch::fp, arch::lr, a64::ptr_pre(a64::sp, -arch::kFrameRecordSize));
12801280
as_->mov(arch::fp, a64::sp);
12811281
#else
12821282
CINDER_UNSUPPORTED
@@ -1289,7 +1289,7 @@ void NativeGenerator::generateFunctionExit() {
12891289
as_->ret();
12901290
#elif defined(CINDER_AARCH64)
12911291
as_->mov(a64::sp, arch::fp);
1292-
as_->ldp(arch::fp, arch::lr, a64::ptr_post(a64::sp, 32));
1292+
as_->ldp(arch::fp, arch::lr, a64::ptr_post(a64::sp, arch::kFrameRecordSize));
12931293
as_->ret(arch::lr);
12941294
#else
12951295
CINDER_UNSUPPORTED
@@ -2496,9 +2496,7 @@ void NativeGenerator::generateStaticEntryPoint(
24962496
// into the generator object when we link the frame. We need to
24972497
// capture the incoming arguments first, which will mean we'll
24982498
// need to save and restore the register.
2499-
// Extra args are above the 32-byte frame (saved fp, lr, savedReturnIP,
2500-
// padding), so the offset is 32.
2501-
as_->add(a64::x10, arch::fp, 32);
2499+
as_->add(a64::x10, arch::fp, arch::kFrameRecordSize);
25022500
save_regs.emplace_back(a64::x10, a64::x10);
25032501
need_extra_args_load = false;
25042502
}
@@ -2514,9 +2512,7 @@ void NativeGenerator::generateStaticEntryPoint(
25142512
a64::x(INITIAL_FUNC_REG.loc), a64::x(INITIAL_TSTATE_REG.loc), save_regs);
25152513

25162514
if (need_extra_args_load) {
2517-
// Extra args are above the 32-byte frame (saved fp, lr, savedReturnIP,
2518-
// padding), so the offset is 32.
2519-
as_->add(a64::x10, arch::fp, 32);
2515+
as_->add(a64::x10, arch::fp, arch::kFrameRecordSize);
25202516
}
25212517
as_->b(finish_frame_setup);
25222518
env_.addAnnotation("StaticLinkFrame", static_link_cursor);
@@ -2633,7 +2629,8 @@ void NativeGenerator::generateCode(CodeHolder& codeholder) {
26332629

26342630
// leave + ret equivalent on aarch64
26352631
as_->mov(a64::sp, arch::fp);
2636-
as_->ldp(arch::fp, arch::lr, a64::ptr_post(a64::sp, 32));
2632+
as_->ldp(
2633+
arch::fp, arch::lr, a64::ptr_post(a64::sp, arch::kFrameRecordSize));
26372634
as_->ret(arch::lr);
26382635
#else
26392636
CINDER_UNSUPPORTED
@@ -2915,7 +2912,8 @@ NativeGenerator::generateBoxedReturnWrapper() {
29152912
as_->bind(error);
29162913
as_->mov(a64::x0, 0);
29172914
as_->mov(a64::sp, arch::fp);
2918-
as_->ldp(arch::fp, arch::lr, a64::ptr_post(a64::sp, 32));
2915+
as_->ldp(
2916+
arch::fp, arch::lr, a64::ptr_post(a64::sp, arch::kFrameRecordSize));
29192917
as_->ret(arch::lr);
29202918
}
29212919
#else
@@ -3022,7 +3020,8 @@ void NativeGenerator::generateArgcountCheckPrologue(Label correct_arg_count) {
30223020
}
30233021
as_->blr(arch::reg_scratch_br);
30243022
as_->mov(a64::sp, arch::fp);
3025-
as_->ldp(arch::fp, arch::lr, a64::ptr_post(a64::sp, 32));
3023+
as_->ldp(
3024+
arch::fp, arch::lr, a64::ptr_post(a64::sp, arch::kFrameRecordSize));
30263025
as_->ret(arch::lr);
30273026
env_.addAnnotation(
30283027
"Check if called with correct argcount", arg_check_cursor);

cinderx/Jit/codegen/gen_asm_utils.cpp

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,7 @@ void emitCall(Environ& env, uint64_t func, const jit::lir::Instruction* instr) {
4444
// https://github.com/asmjit/asmjit/issues/499, but as of writing is not yet
4545
// available.
4646
env.as->mov(arch::reg_scratch_br, func);
47-
// Save the return address at [fp + 16] so that getIP() can find it at a
48-
// fixed offset from the frame base for cross-thread frame inspection.
49-
asmjit::Label after_call = env.as->newLabel();
50-
env.as->adr(arch::reg_scratch_0, after_call);
51-
env.as->str(arch::reg_scratch_0, asmjit::a64::ptr(arch::fp, 16));
5247
env.as->blr(arch::reg_scratch_br);
53-
env.as->bind(after_call);
5448
#else
5549
CINDER_UNSUPPORTED
5650
#endif

cinderx/Jit/compiled_function.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ extern "C" {
1818
// jmp correct-bound-args
1919
#define JITRT_CALL_REENTRY_OFFSET (-6)
2020
#elif defined(__aarch64__)
21-
// stp fp, lr, [sp, #-32]!
21+
// stp fp, lr, [sp, #-16]!
2222
// mov fp, sp
2323
// b correct-bound-args
2424
#define JITRT_CALL_REENTRY_OFFSET (-12)

cinderx/Jit/frame.cpp

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
#include "cinderx/Common/util.h"
1212
#include "cinderx/Jit/frame_header.h"
1313
#include "cinderx/Jit/gen_data_footer.h"
14+
#if defined(CINDER_X86_64)
1415
#include "cinderx/Jit/symbolizer.h"
16+
#endif
1517
#include "cinderx/UpstreamBorrow/borrowed.h"
1618
#include "cinderx/module_state.h"
1719

@@ -83,6 +85,7 @@ bool isInlined(_PyInterpreterFrame* frame) {
8385
return hasRtfsFunction(frame);
8486
}
8587

88+
#if defined(CINDER_X86_64)
8689
// Return the base of the stack frame given its frame.
8790
uintptr_t getFrameBaseFromOnStackFrame(_PyInterpreterFrame* frame) {
8891
// The frame is embedded in the frame header at the beginning of the
@@ -100,17 +103,9 @@ uintptr_t getIP(_PyInterpreterFrame* frame, int frame_size) {
100103
auto footer = jitGenDataFooter(gen);
101104
if (footer->yieldPoint == nullptr) {
102105
// The generator is running.
103-
#if defined(__x86_64__)
104106
// On x86, we read the return address from a fixed offset on the real
105107
// stack relative to the resume function's RBP.
106108
frame_base = footer->originalFramePointer;
107-
#elif defined(__aarch64__)
108-
// On ARM64, the JIT stores the return address at [fp + 16] before
109-
// each call. During generator execution, FP is set to the
110-
// GenDataFooter pointer (gi_jit_data), so [footer + 16] is the
111-
// savedReturnIP field.
112-
frame_base = reinterpret_cast<uintptr_t>(footer);
113-
#endif
114109
} else {
115110
// The generator is suspended.
116111
return footer->yieldPoint->resumeTarget();
@@ -119,31 +114,18 @@ uintptr_t getIP(_PyInterpreterFrame* frame, int frame_size) {
119114
frame_base = getFrameBaseFromOnStackFrame(frame);
120115
}
121116
// Read the saved IP from the stack.
122-
#if defined(__x86_64__)
123117
// On x86, `call` pushes the return address on the stack at a fixed
124118
// location relative to the caller's frame pointer.
125119
uintptr_t ip;
126120
auto saved_ip =
127121
reinterpret_cast<uintptr_t*>(frame_base - frame_size - kPointerSize);
128122
memcpy(&ip, saved_ip, kPointerSize);
129123
return ip;
130-
#elif defined(__aarch64__)
131-
// On ARM64, `blr` stores the return address in lr (x30) rather than
132-
// pushing it on the stack. The JIT explicitly saves the return address
133-
// at [fp + 16] before each call, so we can read it from a fixed offset
134-
// from the frame base.
135-
uintptr_t ip;
136-
auto saved_ip = reinterpret_cast<uintptr_t*>(frame_base + 2 * kPointerSize);
137-
memcpy(&ip, saved_ip, kPointerSize);
138-
return ip;
139-
#else
140-
// Unsupported architecture.
141-
JIT_ABORT("getIP: unsupported architecture");
142-
#endif
143124
#else
144125
throw std::runtime_error{"getIP: Lightweight frames are not supported"};
145126
#endif
146127
}
128+
#endif
147129

148130
// Collect all the frames in the unit, with the frame for the
149131
// non-inlined function as the first element in the return vector.
@@ -200,9 +182,14 @@ UnitState getUnitState(_PyInterpreterFrame* frame) {
200182
_PyInterpreterFrame* non_inlined_sf = unit_frames[0];
201183
CodeRuntime* code_rt = getCodeRuntime(non_inlined_sf);
202184
JIT_CHECK(code_rt != nullptr, "failed to find code runtime");
203-
uintptr_t ip = getIP(non_inlined_sf, code_rt->frameSize());
185+
186+
#if defined(CINDER_AARCH64)
187+
// On ARM64, look up bytecode offsets using the deopt index stored in the
188+
// frame header. The JIT updates this index before each instruction that
189+
// can deopt, so it always reflects the current position in the bytecode.
190+
std::size_t deopt_idx = jitFrameGetHeader(non_inlined_sf)->deopt_idx;
204191
std::optional<UnitCallStack> locs =
205-
code_rt->debugInfo()->getUnitCallStack(ip);
192+
code_rt->getUnitCallStackFromDeoptIdx(deopt_idx);
206193
if (locs.has_value()) {
207194
// We may have a different number of unit_frames than locs, this happens
208195
// when we're updating the outer frame while we're in an inlined function,
@@ -216,11 +203,35 @@ UnitState getUnitState(_PyInterpreterFrame* frame) {
216203
unit_state.emplace_back(unit_frames[i], locs->at(i));
217204
}
218205
} else {
219-
// We might not have debug info for a number of reasons (e.g. we've read
220-
// the return address incorrectly or there's a bug with how we're
221-
// generating the information). The consequences of getting this wrong
222-
// (incorrect line numbers) don't warrant aborting in production, but it is
223-
// worth investigating. Leave some breadcrumbs to help with debugging.
206+
// We might not have debug info for a number of reasons.
207+
// The consequences of getting this wrong (incorrect line numbers) don't
208+
// warrant aborting in production, but it is worth investigating.
209+
JIT_LOG(
210+
"No debug info for deopt_idx {} in {}",
211+
deopt_idx,
212+
PyUnicode_AsUTF8(code_rt->frameState()->func()->func_qualname));
213+
logUnitFrames();
214+
JIT_DABORT("No debug info for deopt_idx {}", deopt_idx);
215+
for (_PyInterpreterFrame* unit_frame : unit_frames) {
216+
unit_state.emplace_back(
217+
unit_frame, CodeObjLoc{_PyFrame_GetCode(unit_frame), BCOffset{-1}});
218+
}
219+
}
220+
#elif defined(CINDER_X86_64)
221+
// On x86-64, look up bytecode offsets using the IP-based symbolizer.
222+
uintptr_t ip = getIP(non_inlined_sf, code_rt->frameSize());
223+
std::optional<UnitCallStack> locs =
224+
code_rt->debugInfo()->getUnitCallStack(ip);
225+
if (locs.has_value()) {
226+
for (std::size_t i = 0; i < unit_frames.size(); i++) {
227+
JIT_DCHECK(
228+
_PyFrame_GetCode(unit_frames[i]) == locs->at(i).code,
229+
"code mismatch {} vs {}",
230+
codeName(_PyFrame_GetCode(unit_frames[i])),
231+
codeName(locs->at(i).code));
232+
unit_state.emplace_back(unit_frames[i], locs->at(i));
233+
}
234+
} else {
224235
JIT_LOG(
225236
"No debug info for addr {:x} {}",
226237
ip,
@@ -232,6 +243,9 @@ UnitState getUnitState(_PyInterpreterFrame* frame) {
232243
unit_frame, CodeObjLoc{_PyFrame_GetCode(unit_frame), BCOffset{-1}});
233244
}
234245
}
246+
#else
247+
CINDER_UNSUPPORTED
248+
#endif
235249

236250
return unit_state;
237251
}
@@ -526,6 +540,9 @@ void jitFrameInitLightweight(
526540
setFrameCode(frame, reifier);
527541
setFrameFunction(frame, (PyObject*)Py_NewRef(func));
528542
jitFrameGetHeader(frame)->rtfs = 0;
543+
#if defined(CINDER_AARCH64)
544+
jitFrameGetHeader(frame)->deopt_idx = 0;
545+
#endif
529546
#else
530547
frame->stacktop = 0;
531548
setFrameInstruction(frame, _PyCode_CODE(code) - 1);

0 commit comments

Comments
 (0)