Skip to content

Commit 870e469

Browse files
kddnewtonmeta-codesync[bot]
authored andcommitted
Eliminate redundant UpdatePrevInstr stores in InsertUpdatePrevInstr pass
Summary: The InsertUpdatePrevInstr pass inserts UpdatePrevInstr instructions before every hasArbitraryExecution instruction to keep the frame's prev_instr/instr_ptr field current for stack traces and instrumentation. The existing pass already avoids emitting duplicate stores when the source line hasn't changed, but it still emits redundant stores when consecutive arbitrary-execution instructions are on different lines with no observable instructions between them. Only the last store before each observation point matters. This change adds intra-block dead store elimination directly into the existing single-pass insertion loop. When a new UpdatePrevInstr is about to be inserted and the previously inserted one hasn't been observed by any hasArbitraryExecution instruction, the previous one is dead and gets removed. This is tracked with two variables per block: a pointer to the last emitted UpdatePrevInstr (`last_emitted`) and a bool indicating whether an arbitrary-execution instruction has been seen since the last emit (`saw_arbitrary`). State is conservatively reset at inline function boundaries (BeginInlinedFunction/EndInlinedFunction) since the UpdatePrevInstr stores target different frames across those boundaries. This is safe because deoptimization does not read the frame's prev_instr field — it uses FrameState::cur_instr_offs from the HIR metadata instead (see deopt.cpp reifyFrameImpl). The only consumers of prev_instr are instructions with hasArbitraryExecution (calls into Python/C that may inspect the frame for tracebacks, sys._getframe(), profilers, instrumentation), and our optimization guarantees the store is live before each such instruction. The optimization adds zero extra passes over the IR — it piggybacks on the existing insertion walk with only a pointer and bool of additional state per block. Reviewed By: alexmalyshev Differential Revision: D103112980 fbshipit-source-id: a14b3f6e97f4aac0302cbff0324b00bbfccaab73
1 parent c5fb4b8 commit 870e469

4 files changed

Lines changed: 134 additions & 3 deletions

File tree

cinderx/Jit/hir/hir.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3734,6 +3734,10 @@ class INSTR_CLASS(UpdatePrevInstr, (), Operands<0>) {
37343734
return line_no_;
37353735
}
37363736

3737+
void setLineNo(int line_no) {
3738+
line_no_ = line_no;
3739+
}
3740+
37373741
// The inlined function which this update belongs to or nullptr if not in an
37383742
// inlined function.
37393743
BeginInlinedFunction* parent() const {

cinderx/Jit/hir/insert_update_prev_instr.cpp

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,20 @@ void InsertUpdatePrevInstr::Run([[maybe_unused]] Function& func) {
8888
worklist.pop();
8989

9090
int prev_emitted_lno_or_bc = INT_MAX;
91+
Instr* last_emitted = nullptr;
9192
for (Instr& instr : *block) {
9293
auto update_one = [&]() {
9394
auto add_update_prev_instr = [&](int line_no) {
94-
Instr* update_instr = UpdatePrevInstr::create(line_no, parent);
95-
update_instr->copyBytecodeOffset(instr);
96-
update_instr->InsertBefore(instr);
95+
if (last_emitted != nullptr) {
96+
last_emitted->unlink();
97+
static_cast<UpdatePrevInstr*>(last_emitted)->setLineNo(line_no);
98+
last_emitted->copyBytecodeOffset(instr);
99+
last_emitted->InsertBefore(instr);
100+
} else {
101+
last_emitted = UpdatePrevInstr::create(line_no, parent);
102+
last_emitted->copyBytecodeOffset(instr);
103+
last_emitted->InsertBefore(instr);
104+
}
97105
};
98106
// If we don't have a valid line table to optimize with, update after
99107
// every bytecode.
@@ -135,12 +143,16 @@ void InsertUpdatePrevInstr::Run([[maybe_unused]] Function& func) {
135143
}
136144
parents[begin] = parent;
137145
parent = begin;
146+
last_emitted = nullptr;
147+
prev_emitted_lno_or_bc = INT_MAX;
138148
if (getConfig().frame_mode == FrameMode::kLightweight) {
139149
inited_once = false;
140150
}
141151
} else if (instr.IsEndInlinedFunction()) {
142152
parent =
143153
parents[static_cast<EndInlinedFunction&>(instr).matchingBegin()];
154+
last_emitted = nullptr;
155+
prev_emitted_lno_or_bc = INT_MAX;
144156
}
145157

146158
if (getConfig().frame_mode == FrameMode::kLightweight) {
@@ -157,11 +169,13 @@ void InsertUpdatePrevInstr::Run([[maybe_unused]] Function& func) {
157169
update_instr->setBytecodeOffset(
158170
BCIndex(target_code->_co_firsttraceable));
159171
update_instr->InsertBefore(instr);
172+
last_emitted = update_instr;
160173

161174
inited_once = true;
162175
}
163176
} else if (hasArbitraryExecution(instr)) {
164177
update_one();
178+
last_emitted = nullptr;
165179
}
166180
}
167181

cinderx/PythonLib/test_cinderx/test_jit_frame.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,29 @@ def f3():
110110
self.assert_code_and_lineno(gen1_frame, f1, 2)
111111
self.assert_code_and_lineno(gen2_frame, f2, 2)
112112

113+
def test_line_numbers_consecutive_calls(self) -> None:
114+
"""Verify line numbers are correct across consecutive calls on different
115+
lines. This exercises the UpdatePrevInstr dead store elimination: the
116+
JIT eliminates redundant prev_instr stores between consecutive calls,
117+
but each call must still see the correct line number."""
118+
stacks = []
119+
120+
def capture():
121+
stacks.append(traceback.extract_stack())
122+
123+
@cinder_support.failUnlessJITCompiled
124+
def f():
125+
capture()
126+
capture()
127+
capture()
128+
capture()
129+
130+
f()
131+
self.assertEqual(len(stacks), 4)
132+
base = firstlineno(f)
133+
for i, stack in enumerate(stacks):
134+
self.assertEqual(stack[-2].lineno, base + 2 + i)
135+
113136
def test_line_numbers_from_finalizers(self) -> None:
114137
"""Make sure we can get accurate line numbers from finalizers"""
115138
stack = []
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
3+
#include <gtest/gtest.h>
4+
5+
#include "cinderx/Jit/hir/hir.h"
6+
#include "cinderx/Jit/hir/insert_update_prev_instr.h"
7+
#include "cinderx/Jit/hir/instr_effects.h"
8+
#include "cinderx/Jit/hir/phi_elimination.h"
9+
#include "cinderx/Jit/hir/simplify.h"
10+
#include "cinderx/Jit/hir/ssa.h"
11+
#include "cinderx/RuntimeTests/fixtures.h"
12+
13+
using namespace jit::hir;
14+
15+
class InsertUpdatePrevInstrTest : public RuntimeTest {};
16+
17+
namespace {
18+
19+
int countIf(const Function& func, auto pred) {
20+
int count = 0;
21+
for (const auto& block : func.cfg.blocks) {
22+
for (const auto& instr : block) {
23+
if (pred(instr)) {
24+
count++;
25+
}
26+
}
27+
}
28+
return count;
29+
}
30+
31+
std::unique_ptr<Function> compileAndRunPass(
32+
RuntimeTest* test,
33+
const char* src) {
34+
std::unique_ptr<Function> irfunc;
35+
test->CompileToHIR(src, "test", irfunc);
36+
if (irfunc == nullptr) {
37+
return nullptr;
38+
}
39+
SSAify{}.Run(*irfunc);
40+
Simplify{}.Run(*irfunc);
41+
PhiElimination{}.Run(*irfunc);
42+
InsertUpdatePrevInstr{}.Run(*irfunc);
43+
return irfunc;
44+
}
45+
46+
} // namespace
47+
48+
TEST_F(InsertUpdatePrevInstrTest, RedundantStoresEliminated) {
49+
// Four len() calls on consecutive lines produce four arbitrary-execution
50+
// points on different source lines. The additions produce more. Without
51+
// dead store elimination, each line change emits its own UpdatePrevInstr.
52+
// With the optimization, consecutive UpdatePrevInstr stores separated only
53+
// by non-arbitrary-execution instructions are collapsed.
54+
const char* src = R"(
55+
def test(a):
56+
w = len(a)
57+
x = len(a)
58+
y = len(a)
59+
z = len(a)
60+
return w + x + y + z
61+
)";
62+
auto irfunc = compileAndRunPass(this, src);
63+
ASSERT_NE(irfunc, nullptr);
64+
65+
int update_count =
66+
countIf(*irfunc, [](const Instr& i) { return i.IsUpdatePrevInstr(); });
67+
int arbitrary_count = countIf(*irfunc, hasArbitraryExecution);
68+
69+
// There must be at least one UpdatePrevInstr.
70+
ASSERT_GT(update_count, 0);
71+
// There must be multiple arbitrary-execution points (len calls + additions).
72+
ASSERT_GE(arbitrary_count, 7);
73+
// The optimization must eliminate at least one redundant store: each
74+
// consecutive pair of arbitrary-execution instructions on different lines
75+
// with nothing observable between them has its first store removed.
76+
EXPECT_LT(update_count, arbitrary_count);
77+
}
78+
79+
TEST_F(InsertUpdatePrevInstrTest, SingleCallPreservesStore) {
80+
const char* src = R"(
81+
def test(a):
82+
return len(a)
83+
)";
84+
auto irfunc = compileAndRunPass(this, src);
85+
ASSERT_NE(irfunc, nullptr);
86+
87+
int update_count =
88+
countIf(*irfunc, [](const Instr& i) { return i.IsUpdatePrevInstr(); });
89+
EXPECT_GT(update_count, 0);
90+
}

0 commit comments

Comments
 (0)