Skip to content

Commit 5557217

Browse files
DinoVmeta-codesync[bot]
authored andcommitted
Optimize load pairs
Summary: Adds a post-alloc pass that re-writes two adjacent loads into a single load pair instruction. This is not part of the main post alloc pass because we only need to run it once everything else is done and we're not going to benefit from running it multiple times to hit a fix point. Reviewed By: alexmalyshev Differential Revision: D114958913 fbshipit-source-id: 72f896af39153480078a6605ba4a29641e555a00
1 parent 182114e commit 5557217

4 files changed

Lines changed: 258 additions & 0 deletions

File tree

cinderx/Jit/codegen/gen_asm.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,15 @@ void* NativeGenerator::getVectorcallEntry() {
882882
"Post Reg Alloc Rewrite",
883883
post_rewrite.run())
884884

885+
#if defined(CINDER_AARCH64)
886+
// Peepholes go last, once nothing else will add to or remove from the
887+
// instruction stream.
888+
COMPILE_TIMER(
889+
getFunction()->compilation_phase_timer,
890+
"Post Reg Alloc Peephole",
891+
runPostRegAllocPeephole(lir_func.get()))
892+
#endif
893+
885894
JIT_LOGIF(
886895
getConfig().log.dump_lir,
887896
"LIR for {} after postalloc rewrites:\n{}",

cinderx/Jit/lir/postalloc.cpp

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,151 @@ void insertStorePairToMemoryLocation(
124124
}
125125
#endif
126126

127+
#if defined(CINDER_AARCH64)
128+
// A 64-bit general-purpose move between a register and memory.
129+
struct PairCandidate {
130+
bool is_load;
131+
// Frame slots report the frame pointer; their offset is the slot location.
132+
PhyLocation base;
133+
int32_t offset;
134+
PhyLocation reg;
135+
136+
bool isAdjacentLoad(const PairCandidate& other) const {
137+
return is_load == other.is_load && base == other.base &&
138+
std::abs(
139+
static_cast<int64_t>(offset) -
140+
static_cast<int64_t>(other.offset)) == kPointerSize;
141+
}
142+
};
143+
144+
std::optional<std::pair<PhyLocation, int32_t>> pairMemoryLocation(
145+
const Operand* operand) {
146+
if (operand->isStack()) {
147+
return std::make_pair(
148+
codegen::arch::reg_frame_pointer_loc, operand->getStackSlot().loc);
149+
}
150+
if (operand->isInd()) {
151+
MemoryIndirect* ind = operand->getMemoryIndirect();
152+
// An index register would need scaling that ldp/stp can't express.
153+
if (ind->getIndexRegOperand() != nullptr) {
154+
return std::nullopt;
155+
}
156+
const Operand* base = ind->getBaseRegOperand();
157+
if (base == nullptr || !base->isReg()) {
158+
return std::nullopt;
159+
}
160+
return std::make_pair(base->getPhyRegister(), ind->getOffset());
161+
}
162+
return std::nullopt;
163+
}
164+
165+
std::optional<PairCandidate> describePairCandidate(const Instruction* instr) {
166+
if (!instr->isMove() || instr->getNumInputs() != 1 ||
167+
instr->getNumOutputs() != 1) {
168+
return std::nullopt;
169+
}
170+
171+
const Operand* out = instr->output();
172+
const Operand* in = instr->getInput(0);
173+
174+
// ldp/stp only come in general-purpose and floating-point flavours that
175+
// can't be mixed, and only the 64-bit width is worth matching here. The
176+
// access width is decided by the memory operand for a store and by the
177+
// destination register for a load; requiring both to be 64-bit covers it.
178+
if (out->isFp() || in->isFp() || out->sizeInBits() != 64 ||
179+
in->sizeInBits() != 64) {
180+
return std::nullopt;
181+
}
182+
183+
if (out->isReg()) {
184+
auto loc = pairMemoryLocation(in);
185+
if (!loc.has_value()) {
186+
return std::nullopt;
187+
}
188+
return PairCandidate{true, loc->first, loc->second, out->getPhyRegister()};
189+
}
190+
191+
if (in->isReg()) {
192+
auto loc = pairMemoryLocation(out);
193+
if (!loc.has_value()) {
194+
return std::nullopt;
195+
}
196+
return PairCandidate{false, loc->first, loc->second, in->getPhyRegister()};
197+
}
198+
199+
return std::nullopt;
200+
}
201+
202+
// Merge adjacent 64-bit loads or stores of neighbouring memory into ldp/stp.
203+
//
204+
// Spill traffic is the main source of these: the register allocator emits one
205+
// move per slot, and consecutive slots are a pointer apart. Argument loading
206+
// off the vectorcall array has the same shape.
207+
//
208+
// Only directly adjacent instructions are considered, which keeps this honest
209+
// about ordering without needing an aliasing check — nothing runs in between,
210+
// so the only reordering is between the two accesses themselves, and they
211+
// cover disjoint memory.
212+
void pairAdjacentMemoryOps(BasicBlock* block) {
213+
auto& instrs = block->instructions();
214+
215+
for (auto it = instrs.begin(); it != instrs.end();) {
216+
auto second = std::next(it);
217+
if (second == instrs.end()) {
218+
break;
219+
}
220+
221+
auto first_desc = describePairCandidate(it->get());
222+
auto second_desc = describePairCandidate(second->get());
223+
if (!first_desc.has_value() || !second_desc.has_value() ||
224+
!first_desc->isAdjacentLoad(*second_desc)) {
225+
++it;
226+
continue;
227+
}
228+
229+
// ldp/stp always take the lower address first.
230+
bool in_order = first_desc->offset < second_desc->offset;
231+
const PairCandidate& low = in_order ? *first_desc : *second_desc;
232+
const PairCandidate& high = in_order ? *second_desc : *first_desc;
233+
234+
if (low.is_load) {
235+
// ldp with a repeated destination, or with a destination that is also
236+
// the base, is architecturally unpredictable. The unmerged pair would
237+
// also have fed the first load's result into the second's address.
238+
if (low.reg == high.reg || low.reg == low.base || high.reg == low.base) {
239+
++it;
240+
continue;
241+
}
242+
}
243+
244+
auto offset = static_cast<uint64_t>(static_cast<int64_t>(low.offset));
245+
// describePairCandidate filtered out floating point and non-64 bit regs
246+
if (low.is_load) {
247+
block->allocateInstrBefore(
248+
it,
249+
Instruction::kLoadPair,
250+
OutPhyReg{low.reg, DataType::k64bit},
251+
Imm{offset},
252+
PhyReg{low.base, DataType::k64bit},
253+
PhyReg{high.reg, DataType::k64bit});
254+
} else {
255+
block->allocateInstrBefore(
256+
it,
257+
Instruction::kStorePair,
258+
Imm{offset},
259+
PhyReg{low.base, DataType::k64bit},
260+
PhyReg{low.reg, DataType::k64bit},
261+
PhyReg{high.reg, DataType::k64bit});
262+
}
263+
264+
auto next = std::next(second);
265+
block->removeInstr(it);
266+
block->removeInstr(second);
267+
it = next;
268+
}
269+
}
270+
#endif
271+
127272
int rewriteRegularFunction(instr_iter_t instr_iter, int base_offset) {
128273
auto instr = instr_iter->get();
129274
auto block = instr->basicBlock();
@@ -1628,4 +1773,12 @@ void PostRegAllocRewrite::registerRewrites() {
16281773
#endif
16291774
}
16301775

1776+
#if defined(CINDER_AARCH64)
1777+
void runPostRegAllocPeephole(Function* func) {
1778+
for (auto& block : func->basicBlocks()) {
1779+
pairAdjacentMemoryOps(block);
1780+
}
1781+
}
1782+
#endif
1783+
16311784
} // namespace cinderx::jit::lir

cinderx/Jit/lir/postalloc.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,11 @@ class PostRegAllocRewrite : public Rewrite {
1818
void registerRewrites();
1919
};
2020

21+
#if defined(CINDER_AARCH64)
22+
23+
// Peephole rewrites run once the instruction stream is otherwise final.
24+
void runPostRegAllocPeephole(Function* func);
25+
26+
#endif
27+
2128
} // namespace cinderx::jit::lir

cinderx/RuntimeTests/lir_postalloc_test.cpp

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,95 @@ BB %0
445445
store_pair->getInput(1)->getPhyRegister(), arch::reg_stack_pointer_loc);
446446
ASSERT_TRUE(verifyPostRegAllocInvariants(parsed_func.get(), std::cout));
447447
}
448+
TEST_F(LIRPostAllocRewriteTest, AdjacentFrameSlotStoresBecomeStorePair) {
449+
Function func;
450+
auto* bb = func.allocateBasicBlock();
451+
452+
bb->allocateInstr(
453+
Instruction::kMove,
454+
nullptr,
455+
OutStk{PhyLocation(-24, 64), DataType::k64bit},
456+
PhyReg{X0, DataType::k64bit});
457+
bb->allocateInstr(
458+
Instruction::kMove,
459+
nullptr,
460+
OutStk{PhyLocation(-16, 64), DataType::k64bit},
461+
PhyReg{X1, DataType::k64bit});
462+
463+
jit::codegen::Environ env;
464+
PostRegAllocRewrite rewrite(&func, &env);
465+
rewrite.run();
466+
runPostRegAllocPeephole(&func);
467+
468+
auto instrs = collectInstrs(*bb);
469+
ASSERT_EQ(instrs.size(), 1);
470+
ASSERT_TRUE(instrs[0]->isStorePair());
471+
// The lower address comes first, so the slot at -24 supplies the offset.
472+
EXPECT_EQ(static_cast<int32_t>(instrs[0]->getInput(0)->getConstant()), -24);
473+
EXPECT_EQ(
474+
instrs[0]->getInput(1)->getPhyRegister(), arch::reg_frame_pointer_loc);
475+
EXPECT_EQ(instrs[0]->getInput(2)->getPhyRegister(), X0);
476+
EXPECT_EQ(instrs[0]->getInput(3)->getPhyRegister(), X1);
477+
}
478+
479+
TEST_F(LIRPostAllocRewriteTest, DescendingFrameSlotLoadsBecomeLoadPair) {
480+
Function func;
481+
auto* bb = func.allocateBasicBlock();
482+
483+
// Written high address first, so the pair has to swap the register order.
484+
bb->allocateInstr(
485+
Instruction::kMove,
486+
nullptr,
487+
OutPhyReg{X0, DataType::k64bit},
488+
Stk{PhyLocation(-16, 64), DataType::k64bit});
489+
bb->allocateInstr(
490+
Instruction::kMove,
491+
nullptr,
492+
OutPhyReg{X1, DataType::k64bit},
493+
Stk{PhyLocation(-24, 64), DataType::k64bit});
494+
495+
jit::codegen::Environ env;
496+
PostRegAllocRewrite rewrite(&func, &env);
497+
rewrite.run();
498+
runPostRegAllocPeephole(&func);
499+
500+
auto instrs = collectInstrs(*bb);
501+
ASSERT_EQ(instrs.size(), 1);
502+
ASSERT_TRUE(instrs[0]->isLoadPair());
503+
EXPECT_EQ(static_cast<int32_t>(instrs[0]->getInput(0)->getConstant()), -24);
504+
EXPECT_EQ(instrs[0]->output()->getPhyRegister(), X1);
505+
EXPECT_EQ(instrs[0]->getInput(2)->getPhyRegister(), X0);
506+
}
507+
508+
// ldp with a destination that is also its base is unpredictable, and the
509+
// unmerged form would have fed the first load's result into the second's
510+
// address, so this pair has to be left alone.
511+
TEST_F(LIRPostAllocRewriteTest, LoadPairSkippedWhenDestinationIsBase) {
512+
Function func;
513+
auto* bb = func.allocateBasicBlock();
514+
515+
bb->allocateInstr(
516+
Instruction::kMove,
517+
nullptr,
518+
OutPhyReg{X2, DataType::k64bit},
519+
Ind(X2, static_cast<int32_t>(0)));
520+
bb->allocateInstr(
521+
Instruction::kMove,
522+
nullptr,
523+
OutPhyReg{X3, DataType::k64bit},
524+
Ind(X2, static_cast<int32_t>(8)));
525+
526+
jit::codegen::Environ env;
527+
PostRegAllocRewrite rewrite(&func, &env);
528+
rewrite.run();
529+
runPostRegAllocPeephole(&func);
530+
531+
auto instrs = collectInstrs(*bb);
532+
ASSERT_EQ(instrs.size(), 2);
533+
EXPECT_FALSE(instrs[0]->isLoadPair());
534+
EXPECT_FALSE(instrs[1]->isLoadPair());
535+
}
536+
448537
#endif // CINDER_AARCH64
449538

450539
} // namespace cinderx::jit::lir

0 commit comments

Comments
 (0)