Skip to content

Commit e76ac5c

Browse files
jbower-fbmeta-codesync[bot]
authored andcommitted
Visit instruction arguments in reverse in register allocator when calculating last-use data
Summary: (This is another attempt at fixing the issue papered over in D104499077.) optimizeMoveSequence() caches register-to-spill moves and rewrites later stack operands to use the cached register. The bug is that a rewritten operand can be marked `lastUse` even when the same spill slot is read again later in the block. D104499077 makes this easy to hit by reusing one stripped value twice for the same runtime call, but the optimizer bug exists independently of that change. [spill] = Move RDI RSI = Move [spill] ; rewritten to RSI = Move RDI RDX = Move [spill] ; still reads the spill later Call ... If the first reload carries `lastUse`, `optimizeMoveSequence()` deletes `[spill] = Move RDI` as dead immediately after rewriting `RSI = Move [spill]`. The later reload still reads `[spill]`, so the call runs with stale stack data. The cause involves the same value being read from the same spill slot by multiple instructions in the same basic block. This is aa situation created by D102409172's tag stripping: * D102409172 adds a strip_cache — when the same tagged value needs stripping for two call arguments, one `And` instruction is emitted and both call arguments become `LinkedOperand`s pointing to the same vreg output. * After regalloc, if that vreg is spilled, both operands become stack-slot reads from the same physical location. * After `rewriteCallInstrs` extracts them into separate `Move`s, you get two `Move`s reading the same spill slot. * However, before the fix, the allocator iterates inputs in forward order while processing instructions in reverse. For `Call func, [spill], [spill]`: the first input is encountered first, def is not in the live set, so it gets `lastUse`. The second input finds def already live — no `lastUse`. So, after call rewriting, `lastUse` is on the chronologically first `Move` — exactly the wrong one for spill deletion. The bug specifically triggers in the self-reload variant: when the spilled register happens to match one of the argument registers, producing `Move RDI, [spill]` (self-reload) followed by `Move RSI, [spill]`. To deal with this we change the register allocator to visit instruction inputs in reverse (apparently) consistently with visiting instructions in reverse. Then `rewriteCallInstrs()` can correctly apply `lastUse` to the chronologically last `Move`. Reviewed By: yoney Differential Revision: D105856708 fbshipit-source-id: 60af6cbc087576f2c18796104c390ca538884ae0
1 parent 4cfd2b1 commit e76ac5c

3 files changed

Lines changed: 63 additions & 58 deletions

File tree

cinderx/Jit/lir/postalloc.cpp

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1480,10 +1480,6 @@ RewriteResult optimizeMoveSequence(BasicBlock* basicblock) {
14801480
auto out_reg = instr->output()->isReg()
14811481
? instr->output()->getPhyRegister()
14821482
: PhyLocation::REG_INVALID;
1483-
// for moves only we can generate A = Move A, which will get optimized out
1484-
if (instr->isMove()) {
1485-
out_reg = PhyLocation::REG_INVALID;
1486-
}
14871483
instr->foreachInputOperand([&](OperandBase* operand) {
14881484
if (!operand->isStack()) {
14891485
return;
@@ -1498,9 +1494,6 @@ RewriteResult optimizeMoveSequence(BasicBlock* basicblock) {
14981494
auto opnd = static_cast<Operand*>(operand);
14991495
auto data_type = opnd->dataType();
15001496
auto old_opnd = fmt::to_string(*opnd);
1501-
bool replacement_is_self_move = instr->isMove() &&
1502-
instr->output()->isReg() &&
1503-
instr->output()->getPhyRegister() == reg;
15041497
opnd->setPhyRegister(reg);
15051498
JIT_CHECK(
15061499
bitSize(data_type) == bitSize(opnd->dataType()),
@@ -1511,13 +1504,8 @@ RewriteResult optimizeMoveSequence(BasicBlock* basicblock) {
15111504
*instr);
15121505
changed = kChanged;
15131506

1514-
// if the stack location operand can be replaced by the register it came
1515-
// from and this is the last use of the operand, we can remove the move
1516-
// instruction moving from the register to the stack location.
1517-
// Keep the spill alive when the replacement would turn this consumer
1518-
// into a self-move: later loads from the same stack slot may still
1519-
// need the spilled value after this move is optimized away.
1520-
if (opnd->isLastUse() && !replacement_is_self_move) {
1507+
// If this is the last use of the stack operand, remove the spill.
1508+
if (opnd->isLastUse()) {
15211509
auto opt_iter = registerMemoryMoves.getInstrFromMemory(stack_slot);
15221510
JIT_CHECK(opt_iter.has_value(), "There must be a def instruction.");
15231511
basicblock->instructions().erase(*opt_iter);

cinderx/Jit/lir/regalloc.cpp

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -468,9 +468,15 @@ void LinearScanAllocator::calculateLiveIntervals() {
468468
visit_indirect(output_opnd);
469469
}
470470

471-
// inputs
472-
for (size_t i = 0; i < instr->getNumInputs(); i++) {
473-
const OperandBase* opnd = instr->getInput(i);
471+
// Inputs are iterated in reverse so that when two operands reference
472+
// the same vreg, lastUse lands on the last operand by index. After
473+
// call rewriting splits operands into separate Move instructions, the
474+
// last operand becomes the chronologically last Move, which is the
475+
// correct one to carry lastUse for spill-deletion decisions.
476+
size_t num_inputs = instr->getNumInputs();
477+
for (size_t i = 0; i < num_inputs; ++i) {
478+
size_t idx = num_inputs - i - 1;
479+
const OperandBase* opnd = instr->getInput(idx);
474480
if (!opnd->isVreg() && !opnd->isInd()) {
475481
continue;
476482
}
@@ -480,7 +486,7 @@ void LinearScanAllocator::calculateLiveIntervals() {
480486
continue;
481487
}
482488

483-
register_input(opnd, instr->getInputPhyRegUse(i));
489+
register_input(opnd, instr->getInputPhyRegUse(idx));
484490
}
485491

486492
if (instr_opcode == Instruction::kCall ||

cinderx/RuntimeTests/backend_test.cpp

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -768,19 +768,34 @@ TEST_F(BackendTest, MoveSequenceOptTest) {
768768
[RBP - 16]:Object = Move RAX:Object
769769
[RBP - 24]:Object = Move RSI:Object
770770
RDI:Object = Move RAX:Object
771+
RSI:Object = Move [RBP - 24]:Object
771772
RDX:Object = Move RCX:Object
772773
Call Object
774+
775+
[RBP - 32] is deleted: lastUse with no later stack reads.
776+
RSI = Move [RBP - 24] is a self-reload (RSI spilled and loaded back to RSI).
777+
It is left intact because reg == out_reg skips the rewrite.
773778
*/
774-
ASSERT_EQ(bb->getNumInstrs(), 5);
779+
ASSERT_EQ(bb->getNumInstrs(), 6);
775780
auto& instrs = bb->instructions();
776781

777782
auto iter = instrs.begin();
778783

779-
ASSERT_EQ((*(iter++))->opcode(), Instruction::kMove);
780-
ASSERT_EQ((*(iter++))->opcode(), Instruction::kMove);
781-
ASSERT_EQ((*(iter++))->opcode(), Instruction::kMove);
782-
ASSERT_EQ((*(iter++))->opcode(), Instruction::kMove);
783-
ASSERT_EQ((*(iter++))->opcode(), Instruction::kCall);
784+
auto* spill0 = (*(iter++)).get();
785+
auto* spill1 = (*(iter++)).get();
786+
auto* arg0 = (*(iter++)).get();
787+
auto* arg1 = (*(iter++)).get();
788+
auto* arg2 = (*(iter++)).get();
789+
auto* call_instr = (*(iter++)).get();
790+
791+
ASSERT_EQ(spill0->opcode(), Instruction::kMove);
792+
ASSERT_EQ(spill1->opcode(), Instruction::kMove);
793+
ASSERT_EQ(arg0->opcode(), Instruction::kMove);
794+
ASSERT_EQ(arg0->getInput(0)->type(), OperandBase::kReg);
795+
ASSERT_EQ(arg1->opcode(), Instruction::kMove);
796+
ASSERT_EQ(arg1->getInput(0)->type(), OperandBase::kStack);
797+
ASSERT_EQ(arg2->opcode(), Instruction::kMove);
798+
ASSERT_EQ(call_instr->opcode(), Instruction::kCall);
784799
}
785800

786801
TEST_F(BackendTest, MoveSequenceOpt2Test) {
@@ -820,47 +835,47 @@ TEST_F(BackendTest, MoveSequenceOpt2Test) {
820835
ASSERT_EQ((*iter)->getInput(1)->type(), OperandBase::kStack);
821836
}
822837

823-
TEST_F(BackendTest, MoveSequenceOptKeepsSharedSpillAliveAcrossSelfMove) {
838+
TEST_F(BackendTest, MoveSequenceOptLeavesSelfReloadsIntact) {
824839
auto lirfunc = std::make_unique<Function>();
825840
auto bb = lirfunc->allocateBasicBlock();
826841
auto epilogue = lirfunc->allocateBasicBlock();
827842

828843
const PhyLocation kSharedSlot{-16, 64};
829-
const PhyLocation kSelfMoveReg = ARGUMENT_REGS[0];
844+
const PhyLocation kReloadReg = ARGUMENT_REGS[0];
830845
constexpr uint64_t kExpected = 4;
831846

832-
// Seed the spill slot with a known stale value. If the optimizer later
833-
// deletes the real spill by mistake, the final reload reads this zero and the
834-
// test fails deterministically instead of depending on uninitialized stack
835-
// contents.
847+
// Set up the previously failing case:
848+
//
849+
// [RBP - 16] = Move RSI
850+
// RSI = Move [RBP - 16] ; writes RSI, does not consume cached RSI
851+
// RAX = Move [RBP - 16] ; later stack read still needs the spill
852+
//
853+
// A bad rewrite would turn the middle instruction into `RSI = Move RSI` and
854+
// then conclude the spill is dead. This test checks that we keep both the
855+
// spill store and the explicit self-reload in the block.
856+
// Make a deleted spill observable instead of reading arbitrary stack data.
836857
bb->allocateInstr(
837858
Instruction::kMove,
838859
nullptr,
839860
OutStk{kSharedSlot, OperandBase::k64bit},
840-
Imm{0, OperandBase::k64bit});
861+
Imm{kExpected - kExpected, OperandBase::k64bit});
841862
bb->allocateInstr(
842863
Instruction::kMove,
843864
nullptr,
844-
OutPhyReg{kSelfMoveReg, OperandBase::k64bit},
865+
OutPhyReg{kReloadReg, OperandBase::k64bit},
845866
Imm{kExpected, OperandBase::k64bit});
846867
bb->allocateInstr(
847868
Instruction::kMove,
848869
nullptr,
849870
OutStk{kSharedSlot, OperandBase::k64bit},
850-
PhyReg{kSelfMoveReg, OperandBase::k64bit});
851-
// Reload the same spilled value back into the same physical register. Once
852-
// the stack input is rewritten to use the remembered register directly, this
853-
// instruction becomes `Move reg, reg` and is dropped later as a no-op.
854-
auto self_move = bb->allocateInstr(
871+
PhyReg{kReloadReg, OperandBase::k64bit});
872+
873+
auto self_reload = bb->allocateInstr(
855874
Instruction::kMove,
856875
nullptr,
857-
OutPhyReg{kSelfMoveReg, OperandBase::k64bit},
876+
OutPhyReg{kReloadReg, OperandBase::k64bit},
858877
Stk{kSharedSlot, OperandBase::k64bit});
859-
// Mark the stack operand as a last use to model the exact case that used to
860-
// trigger spill deletion inside optimizeMoveSequence().
861-
self_move->getInput(0)->setLastUse();
862-
// A second reload still needs the shared spill slot after the self-move has
863-
// been simplified away. That is what the old optimization missed.
878+
self_reload->getInput(0)->setLastUse();
864879
bb->allocateInstr(
865880
Instruction::kMove,
866881
nullptr,
@@ -871,30 +886,26 @@ TEST_F(BackendTest, MoveSequenceOptKeepsSharedSpillAliveAcrossSelfMove) {
871886

872887
auto func = reinterpret_cast<uint64_t (*)()>(SimpleCompile(lirfunc.get()));
873888

874-
bool saw_preserved_spill = false;
875-
for (auto iter = bb->instructions().begin(); iter != bb->instructions().end();
876-
++iter) {
877-
auto* instr = iter->get();
889+
bool saw_spill = false;
890+
bool saw_self_reload = false;
891+
for (auto& instr : bb->instructions()) {
878892
if (!instr->isMove()) {
879893
continue;
880894
}
881895
auto* out = instr->output();
882896
auto* in = instr->getInput(0);
883897
if (out->isStack() && out->getStackSlot().loc == kSharedSlot.loc &&
884-
in->isReg() && in->getPhyRegister() == kSelfMoveReg) {
885-
saw_preserved_spill = true;
886-
break;
898+
in->isReg() && in->getPhyRegister() == kReloadReg) {
899+
saw_spill = true;
900+
}
901+
if (out->isReg() && out->getPhyRegister() == kReloadReg && in->isStack() &&
902+
in->getStackSlot().loc == kSharedSlot.loc) {
903+
saw_self_reload = true;
887904
}
888905
}
889906

890-
// Check both the internal LIR shape and the final machine-code behavior.
891-
// Before the fix, optimizeMoveSequence() treated the self-move's stack input
892-
// as the last consumer of the spill, erased the defining store, and relied on
893-
// the register value staying live. That was wrong because the later reload
894-
// still legitimately reads the stack slot. Keeping the spill is safe: it only
895-
// skips the deletion in the self-move case, where the move itself is about to
896-
// disappear and therefore cannot serve as the final use of the spilled value.
897-
EXPECT_TRUE(saw_preserved_spill);
907+
EXPECT_TRUE(saw_spill);
908+
EXPECT_TRUE(saw_self_reload);
898909
EXPECT_EQ(func(), kExpected);
899910
}
900911

0 commit comments

Comments
 (0)