@@ -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+
127272int 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
0 commit comments