Skip to content

Commit d97cf1f

Browse files
author
Sjoerd Meijer
committed
[ARM][LowOverheadLoops] Remove dead loop update instructions.
After creating a low-overhead loop, the loop update instruction was still lingering around hurting performance. This removes dead loop update instructions, which in our case are mostly SUBS instructions. To support this, some helper functions were added to MachineLoopUtils and ReachingDefAnalysis to analyse live-ins of loop exit blocks and find uses before a particular loop instruction, respectively. This is a first version that removes a SUBS instruction when there are no other uses inside and outside the loop block, but there are some more interesting cases in test/CodeGen/Thumb2/LowOverheadLoops/mve-tail-data-types.ll which shows that there is room for improvement. For example, we can't handle this case yet: .. dlstp.32 lr, r2 .LBB0_1: mov r3, r2 subs r2, #4 vldrh.u32 q2, [r1], #8 vmov q1, q0 vmla.u32 q0, q2, r0 letp lr, .LBB0_1 @ %bb.2: vctp.32 r3 .. which is a lot more tricky because r2 is not only used by the subs, but also by the mov to r3, which is used outside the low-overhead loop by the vctp instruction, and that requires a bit of a different approach, and I will follow up on this. Differential Revision: https://reviews.llvm.org/D71007
1 parent bd0f271 commit d97cf1f

11 files changed

+638
-9
lines changed

llvm/include/llvm/CodeGen/MachineLoopUtils.h

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#define LLVM_LIB_CODEGEN_MACHINELOOPUTILS_H
1111

1212
namespace llvm {
13+
class MachineLoop;
1314
class MachineBasicBlock;
1415
class MachineRegisterInfo;
1516
class TargetInstrInfo;
@@ -36,6 +37,10 @@ MachineBasicBlock *PeelSingleBlockLoop(LoopPeelDirection Direction,
3637
MachineRegisterInfo &MRI,
3738
const TargetInstrInfo *TII);
3839

40+
/// Return true if PhysReg is live outside the loop, i.e. determine if it
41+
/// is live in the loop exit blocks, and false otherwise.
42+
bool isRegLiveInExitBlocks(MachineLoop *Loop, int PhysReg);
43+
3944
} // namespace llvm
4045

4146
#endif // LLVM_LIB_CODEGEN_MACHINELOOPUTILS_H

llvm/include/llvm/CodeGen/ReachingDefAnalysis.h

+7
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ class ReachingDefAnalysis : public MachineFunctionPass {
110110
/// use or a live out.
111111
bool isRegUsedAfter(MachineInstr *MI, int PhysReg);
112112

113+
/// Provides the first instruction before MI that uses PhysReg
114+
MachineInstr *getInstWithUseBefore(MachineInstr *MI, int PhysReg);
115+
116+
/// Provides all instructions before MI that uses PhysReg
117+
void getAllInstWithUseBefore(MachineInstr *MI, int PhysReg,
118+
SmallVectorImpl<MachineInstr*> &Uses);
119+
113120
/// Provides the clearance - the number of instructions since the closest
114121
/// reaching def instuction of PhysReg that reaches MI.
115122
int getClearance(MachineInstr *MI, MCPhysReg PhysReg);

llvm/lib/CodeGen/MachineLoopUtils.cpp

+12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77
//===----------------------------------------------------------------------===//
88

9+
#include "llvm/CodeGen/MachineLoopInfo.h"
910
#include "llvm/CodeGen/MachineLoopUtils.h"
1011
#include "llvm/CodeGen/MachineBasicBlock.h"
1112
#include "llvm/CodeGen/MachineRegisterInfo.h"
@@ -130,3 +131,14 @@ MachineBasicBlock *llvm::PeelSingleBlockLoop(LoopPeelDirection Direction,
130131

131132
return NewBB;
132133
}
134+
135+
bool llvm::isRegLiveInExitBlocks(MachineLoop *Loop, int PhysReg) {
136+
SmallVector<MachineBasicBlock *, 4> ExitBlocks;
137+
Loop->getExitBlocks(ExitBlocks);
138+
139+
for (auto *MBB : ExitBlocks)
140+
if (MBB->isLiveIn(PhysReg))
141+
return true;
142+
143+
return false;
144+
}

llvm/lib/CodeGen/ReachingDefAnalysis.cpp

+25-1
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ int ReachingDefAnalysis::getClearance(MachineInstr *MI, MCPhysReg PhysReg) {
227227
}
228228

229229
void ReachingDefAnalysis::getReachingLocalUses(MachineInstr *Def, int PhysReg,
230-
SmallVectorImpl<MachineInstr*> &Uses) {
230+
SmallVectorImpl<MachineInstr*> &Uses) {
231231
MachineBasicBlock *MBB = Def->getParent();
232232
MachineBasicBlock::iterator MI = MachineBasicBlock::iterator(Def);
233233
while (++MI != MBB->end()) {
@@ -272,3 +272,27 @@ bool ReachingDefAnalysis::isRegUsedAfter(MachineInstr *MI, int PhysReg) {
272272
return false;
273273
}
274274

275+
MachineInstr *ReachingDefAnalysis::getInstWithUseBefore(MachineInstr *MI,
276+
int PhysReg) {
277+
auto I = MachineBasicBlock::reverse_iterator(MI);
278+
auto E = MI->getParent()->rend();
279+
I++;
280+
281+
for ( ; I != E; I++)
282+
for (auto &MO : I->operands())
283+
if (MO.isReg() && MO.isUse() && MO.getReg() == PhysReg)
284+
return &*I;
285+
286+
return nullptr;
287+
}
288+
289+
void ReachingDefAnalysis::getAllInstWithUseBefore(MachineInstr *MI,
290+
int PhysReg, SmallVectorImpl<MachineInstr*> &Uses) {
291+
MachineInstr *Use = nullptr;
292+
MachineInstr *Pos = MI;
293+
294+
while ((Use = getInstWithUseBefore(Pos, PhysReg))) {
295+
Uses.push_back(Use);
296+
Pos = Use;
297+
}
298+
}

llvm/lib/Target/ARM/ARMLowOverheadLoops.cpp

+73-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "ARMSubtarget.h"
2525
#include "llvm/CodeGen/MachineFunctionPass.h"
2626
#include "llvm/CodeGen/MachineLoopInfo.h"
27+
#include "llvm/CodeGen/MachineLoopUtils.h"
2728
#include "llvm/CodeGen/MachineRegisterInfo.h"
2829
#include "llvm/CodeGen/Passes.h"
2930
#include "llvm/CodeGen/ReachingDefAnalysis.h"
@@ -163,6 +164,7 @@ namespace {
163164
ReachingDefAnalysis *RDA = nullptr;
164165
const ARMBaseInstrInfo *TII = nullptr;
165166
MachineRegisterInfo *MRI = nullptr;
167+
const TargetRegisterInfo *TRI = nullptr;
166168
std::unique_ptr<ARMBasicBlockUtils> BBUtils = nullptr;
167169

168170
public:
@@ -200,6 +202,8 @@ namespace {
200202

201203
void RevertLoopEnd(MachineInstr *MI, bool SkipCmp = false) const;
202204

205+
void RemoveLoopUpdate(LowOverheadLoop &LoLoop);
206+
203207
void RemoveVPTBlocks(LowOverheadLoop &LoLoop);
204208

205209
MachineInstr *ExpandLoopStart(LowOverheadLoop &LoLoop);
@@ -383,6 +387,7 @@ bool ARMLowOverheadLoops::runOnMachineFunction(MachineFunction &mf) {
383387
MF->getProperties().set(MachineFunctionProperties::Property::TracksLiveness);
384388
MRI = &MF->getRegInfo();
385389
TII = static_cast<const ARMBaseInstrInfo*>(ST.getInstrInfo());
390+
TRI = ST.getRegisterInfo();
386391
BBUtils = std::unique_ptr<ARMBasicBlockUtils>(new ARMBasicBlockUtils(*MF));
387392
BBUtils->computeAllBlockSizes();
388393
BBUtils->adjustBBOffsetsAfter(&MF->front());
@@ -511,7 +516,7 @@ void ARMLowOverheadLoops::RevertWhile(MachineInstr *MI) const {
511516
MIB.addImm(0);
512517
MIB.addImm(ARMCC::AL);
513518
MIB.addReg(ARM::NoRegister);
514-
519+
515520
MachineBasicBlock *DestBB = MI->getOperand(1).getMBB();
516521
unsigned BrOpc = BBUtils->isBBInRange(MI, DestBB, 254) ?
517522
ARM::tBcc : ARM::t2Bcc;
@@ -631,6 +636,70 @@ MachineInstr* ARMLowOverheadLoops::ExpandLoopStart(LowOverheadLoop &LoLoop) {
631636
return &*MIB;
632637
}
633638

639+
// Goal is to optimise and clean-up these loops:
640+
//
641+
// vector.body:
642+
// renamable $vpr = MVE_VCTP32 renamable $r3, 0, $noreg
643+
// renamable $r3, dead $cpsr = tSUBi8 killed renamable $r3(tied-def 0), 4
644+
// ..
645+
// $lr = MVE_DLSTP_32 renamable $r3
646+
//
647+
// The SUB is the old update of the loop iteration count expression, which
648+
// is no longer needed. This sub is removed when the element count, which is in
649+
// r3 in this example, is defined by an instruction in the loop, and it has
650+
// no uses.
651+
//
652+
void ARMLowOverheadLoops::RemoveLoopUpdate(LowOverheadLoop &LoLoop) {
653+
Register ElemCount = LoLoop.VCTP->getOperand(1).getReg();
654+
MachineInstr *LastInstrInBlock = &LoLoop.VCTP->getParent()->back();
655+
656+
LLVM_DEBUG(dbgs() << "ARM Loops: Trying to remove loop update stmt\n");
657+
658+
if (LoLoop.ML->getNumBlocks() != 1) {
659+
LLVM_DEBUG(dbgs() << "ARM Loops: single block loop expected\n");
660+
return;
661+
}
662+
663+
LLVM_DEBUG(dbgs() << "ARM Loops: Analyzing MO: ";
664+
LoLoop.VCTP->getOperand(1).dump());
665+
666+
// Find the definition we are interested in removing, if there is one.
667+
MachineInstr *Def = RDA->getReachingMIDef(LastInstrInBlock, ElemCount);
668+
if (!Def)
669+
return;
670+
671+
// Bail if we define CPSR and it is not dead
672+
if (!Def->registerDefIsDead(ARM::CPSR, TRI)) {
673+
LLVM_DEBUG(dbgs() << "ARM Loops: CPSR is not dead\n");
674+
return;
675+
}
676+
677+
// Bail if elemcount is used in exit blocks, i.e. if it is live-in.
678+
if (isRegLiveInExitBlocks(LoLoop.ML, ElemCount)) {
679+
LLVM_DEBUG(dbgs() << "ARM Loops: Elemcount is live-out, can't remove stmt\n");
680+
return;
681+
}
682+
683+
// Bail if there are uses after this Def in the block.
684+
SmallVector<MachineInstr*, 4> Uses;
685+
RDA->getReachingLocalUses(Def, ElemCount, Uses);
686+
if (Uses.size()) {
687+
LLVM_DEBUG(dbgs() << "ARM Loops: Local uses in block, can't remove stmt\n");
688+
return;
689+
}
690+
691+
Uses.clear();
692+
RDA->getAllInstWithUseBefore(Def, ElemCount, Uses);
693+
694+
// Remove Def if there are no uses, or if the only use is the VCTP
695+
// instruction.
696+
if (!Uses.size() || (Uses.size() == 1 && Uses[0] == LoLoop.VCTP)) {
697+
LLVM_DEBUG(dbgs() << "ARM Loops: Removing loop update instruction: ";
698+
Def->dump());
699+
Def->eraseFromParent();
700+
}
701+
}
702+
634703
void ARMLowOverheadLoops::RemoveVPTBlocks(LowOverheadLoop &LoLoop) {
635704
LLVM_DEBUG(dbgs() << "ARM Loops: Removing VCTP: " << *LoLoop.VCTP);
636705
LoLoop.VCTP->eraseFromParent();
@@ -703,8 +772,10 @@ void ARMLowOverheadLoops::Expand(LowOverheadLoop &LoLoop) {
703772
RemoveDeadBranch(LoLoop.Start);
704773
LoLoop.End = ExpandLoopEnd(LoLoop);
705774
RemoveDeadBranch(LoLoop.End);
706-
if (LoLoop.IsTailPredicationLegal())
775+
if (LoLoop.IsTailPredicationLegal()) {
776+
RemoveLoopUpdate(LoLoop);
707777
RemoveVPTBlocks(LoLoop);
778+
}
708779
}
709780
}
710781

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# RUN: llc -mtriple=thumbv8.1m.main -mattr=+lob -run-pass=arm-low-overhead-loops --verify-machineinstrs %s -o - | FileCheck %s
2+
3+
# There are 2 SUBS, and the 2nd one is identified as the def.
4+
# Thus, the 1st is a use, and we shouldn't optimise away the SUBS.
5+
6+
# CHECK: bb.1.vector.body:
7+
# CHECK: renamable $r3, dead $cpsr = tSUBi8 killed renamable $r3, 4, 14, $noreg
8+
# CHECK: renamable $r3, dead $cpsr = tSUBi8 killed renamable $r3, 4, 14, $noreg
9+
# CHECK: $lr = MVE_LETP renamable $lr, %bb.1
10+
11+
--- |
12+
target datalayout = "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64"
13+
target triple = "thumbv8.1m.main-arm-unknown-eabi"
14+
15+
define dso_local void @use_before_def(i32* noalias nocapture %A, i32* noalias nocapture readonly %B, i32* noalias nocapture readonly %C, i32 %N) local_unnamed_addr #0 {
16+
entry:
17+
%cmp8 = icmp sgt i32 %N, 0
18+
%0 = add i32 %N, 3
19+
%1 = lshr i32 %0, 2
20+
%2 = shl nuw i32 %1, 2
21+
%3 = add i32 %2, -4
22+
%4 = lshr i32 %3, 2
23+
%5 = add nuw nsw i32 %4, 1
24+
br i1 %cmp8, label %vector.ph, label %for.cond.cleanup
25+
26+
vector.ph: ; preds = %entry
27+
call void @llvm.set.loop.iterations.i32(i32 %5)
28+
br label %vector.body
29+
30+
vector.body: ; preds = %vector.body, %vector.ph
31+
%lsr.iv17 = phi i32* [ %scevgep18, %vector.body ], [ %A, %vector.ph ]
32+
%lsr.iv14 = phi i32* [ %scevgep15, %vector.body ], [ %C, %vector.ph ]
33+
%lsr.iv = phi i32* [ %scevgep, %vector.body ], [ %B, %vector.ph ]
34+
%6 = phi i32 [ %5, %vector.ph ], [ %11, %vector.body ]
35+
%7 = phi i32 [ %N, %vector.ph ], [ %9, %vector.body ]
36+
%lsr.iv13 = bitcast i32* %lsr.iv to <4 x i32>*
37+
%lsr.iv1416 = bitcast i32* %lsr.iv14 to <4 x i32>*
38+
%lsr.iv1719 = bitcast i32* %lsr.iv17 to <4 x i32>*
39+
%8 = call <4 x i1> @llvm.arm.vctp32(i32 %7)
40+
%9 = sub i32 %7, 4
41+
%wide.masked.load = call <4 x i32> @llvm.masked.load.v4i32.p0v4i32(<4 x i32>* %lsr.iv13, i32 4, <4 x i1> %8, <4 x i32> undef), !tbaa !3
42+
%wide.masked.load12 = call <4 x i32> @llvm.masked.load.v4i32.p0v4i32(<4 x i32>* %lsr.iv1416, i32 4, <4 x i1> %8, <4 x i32> undef), !tbaa !3
43+
%10 = add nsw <4 x i32> %wide.masked.load12, %wide.masked.load
44+
call void @llvm.masked.store.v4i32.p0v4i32(<4 x i32> %10, <4 x i32>* %lsr.iv1719, i32 4, <4 x i1> %8), !tbaa !3
45+
%scevgep = getelementptr i32, i32* %lsr.iv, i32 4
46+
%scevgep15 = getelementptr i32, i32* %lsr.iv14, i32 4
47+
%scevgep18 = getelementptr i32, i32* %lsr.iv17, i32 4
48+
%11 = call i32 @llvm.loop.decrement.reg.i32.i32.i32(i32 %6, i32 1)
49+
%12 = icmp ne i32 %11, 0
50+
br i1 %12, label %vector.body, label %for.cond.cleanup, !llvm.loop !7
51+
52+
for.cond.cleanup: ; preds = %vector.body, %entry
53+
ret void
54+
}
55+
declare void @llvm.set.loop.iterations.i32(i32) #1
56+
declare <4 x i1> @llvm.arm.vctp32(i32) #2
57+
declare i32 @llvm.loop.decrement.reg.i32.i32.i32(i32, i32) #1
58+
declare void @llvm.lifetime.start.p0i8(i64 immarg, i8* nocapture) #3
59+
declare <4 x i32> @llvm.masked.load.v4i32.p0v4i32(<4 x i32>*, i32 immarg, <4 x i1>, <4 x i32>) #4
60+
declare void @llvm.masked.store.v4i32.p0v4i32(<4 x i32>, <4 x i32>*, i32 immarg, <4 x i1>) #3
61+
declare void @llvm.stackprotector(i8*, i8**) #5
62+
63+
attributes #0 = { nofree norecurse nounwind "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="true" "no-jump-tables"="false" "no-nans-fp-math"="true" "no-signed-zeros-fp-math"="true" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="generic" "target-features"="+armv8.1-m.main,+fp-armv8d16sp,+fp16,+fpregs,+fullfp16,+hwdiv,+lob,+mve.fp,+ras,+strict-align,+thumb-mode,+vfp2sp,+vfp3d16sp,+vfp4d16sp" "unsafe-fp-math"="true" "use-soft-float"="false" }
64+
attributes #1 = { noduplicate nounwind }
65+
attributes #2 = { nounwind readnone }
66+
attributes #3 = { argmemonly nounwind willreturn }
67+
attributes #4 = { argmemonly nounwind readonly willreturn }
68+
attributes #5 = { nounwind }
69+
70+
!llvm.module.flags = !{!0, !1}
71+
!llvm.ident = !{!2}
72+
73+
!0 = !{i32 1, !"wchar_size", i32 4}
74+
!1 = !{i32 1, !"min_enum_size", i32 4}
75+
!2 = !{!"clang version 10.0.0 (http://github.com/llvm/llvm-project 2589b6d9edda73280fe1dc1d944ee34e22fe9a6f)"}
76+
!3 = !{!4, !4, i64 0}
77+
!4 = !{!"int", !5, i64 0}
78+
!5 = !{!"omnipotent char", !6, i64 0}
79+
!6 = !{!"Simple C++ TBAA"}
80+
!7 = distinct !{!7, !8}
81+
!8 = !{!"llvm.loop.isvectorized", i32 1}
82+
83+
...
84+
---
85+
name: use_before_def
86+
alignment: 2
87+
exposesReturnsTwice: false
88+
legalized: false
89+
regBankSelected: false
90+
selected: false
91+
failedISel: false
92+
tracksRegLiveness: true
93+
hasWinCFI: false
94+
registers: []
95+
liveins:
96+
- { reg: '$r0', virtual-reg: '' }
97+
- { reg: '$r1', virtual-reg: '' }
98+
- { reg: '$r2', virtual-reg: '' }
99+
- { reg: '$r3', virtual-reg: '' }
100+
frameInfo:
101+
isFrameAddressTaken: false
102+
isReturnAddressTaken: false
103+
hasStackMap: false
104+
hasPatchPoint: false
105+
stackSize: 8
106+
offsetAdjustment: 0
107+
maxAlignment: 4
108+
adjustsStack: false
109+
hasCalls: false
110+
stackProtector: ''
111+
maxCallFrameSize: 0
112+
cvBytesOfCalleeSavedRegisters: 0
113+
hasOpaqueSPAdjustment: false
114+
hasVAStart: false
115+
hasMustTailInVarArgFunc: false
116+
localFrameSize: 0
117+
savePoint: ''
118+
restorePoint: ''
119+
fixedStack: []
120+
stack:
121+
- { id: 0, name: '', type: spill-slot, offset: -4, size: 4, alignment: 4,
122+
stack-id: default, callee-saved-register: '$lr', callee-saved-restored: false,
123+
debug-info-variable: '', debug-info-expression: '', debug-info-location: '' }
124+
- { id: 1, name: '', type: spill-slot, offset: -8, size: 4, alignment: 4,
125+
stack-id: default, callee-saved-register: '$r7', callee-saved-restored: true,
126+
debug-info-variable: '', debug-info-expression: '', debug-info-location: '' }
127+
callSites: []
128+
constants: []
129+
machineFunctionInfo: {}
130+
body: |
131+
bb.0.entry:
132+
successors: %bb.1(0x80000000)
133+
liveins: $r0, $r1, $r2, $r3, $lr
134+
135+
frame-setup tPUSH 14, $noreg, $r7, killed $lr, implicit-def $sp, implicit $sp
136+
frame-setup CFI_INSTRUCTION def_cfa_offset 8
137+
frame-setup CFI_INSTRUCTION offset $lr, -4
138+
frame-setup CFI_INSTRUCTION offset $r7, -8
139+
$r7 = frame-setup tMOVr $sp, 14, $noreg
140+
frame-setup CFI_INSTRUCTION def_cfa_register $r7
141+
tCMPi8 renamable $r3, 1, 14, $noreg, implicit-def $cpsr
142+
t2IT 11, 8, implicit-def $itstate
143+
tPOP_RET 11, killed $cpsr, def $r7, def $pc, implicit killed $itstate
144+
renamable $r12 = t2ADDri renamable $r3, 3, 14, $noreg, $noreg
145+
renamable $lr = t2MOVi 1, 14, $noreg, $noreg
146+
renamable $r12 = t2BICri killed renamable $r12, 3, 14, $noreg, $noreg
147+
renamable $r12 = t2SUBri killed renamable $r12, 4, 14, $noreg, $noreg
148+
renamable $lr = nuw nsw t2ADDrs killed renamable $lr, killed renamable $r12, 19, 14, $noreg, $noreg
149+
t2DoLoopStart renamable $lr
150+
151+
bb.1.vector.body:
152+
successors: %bb.1(0x7c000000), %bb.2(0x04000000)
153+
liveins: $lr, $r0, $r1, $r2, $r3
154+
155+
renamable $vpr = MVE_VCTP32 renamable $r3, 0, $noreg
156+
MVE_VPST 4, implicit $vpr
157+
renamable $r1, renamable $q0 = MVE_VLDRWU32_post killed renamable $r1, 16, 1, renamable $vpr :: (load 16 from %ir.lsr.iv13, align 4, !tbaa !3)
158+
renamable $r2, renamable $q1 = MVE_VLDRWU32_post killed renamable $r2, 16, 1, renamable $vpr :: (load 16 from %ir.lsr.iv1416, align 4, !tbaa !3)
159+
renamable $r3, dead $cpsr = tSUBi8 killed renamable $r3, 4, 14, $noreg
160+
renamable $q0 = nsw MVE_VADDi32 killed renamable $q1, killed renamable $q0, 0, $noreg, undef renamable $q0
161+
MVE_VPST 8, implicit $vpr
162+
renamable $r0 = MVE_VSTRWU32_post killed renamable $q0, killed renamable $r0, 16, 1, killed renamable $vpr :: (store 16 into %ir.lsr.iv1719, align 4, !tbaa !3)
163+
renamable $lr = t2LoopDec killed renamable $lr, 1
164+
renamable $r3, dead $cpsr = tSUBi8 killed renamable $r3, 4, 14, $noreg
165+
t2LoopEnd renamable $lr, %bb.1, implicit-def dead $cpsr
166+
tB %bb.2, 14, $noreg
167+
168+
bb.2.for.cond.cleanup:
169+
tPOP_RET 14, $noreg, def $r7, def $pc
170+
171+
...

0 commit comments

Comments
 (0)