Skip to content

Commit d4a606b

Browse files
committed
[IR][GlobalDCE] Extend !vcall_visibility to be able to specify an offset range that it applies to
This PR adds support to GlobalDCE for vtables containing references that are not virtual functions and should not participate in VFE. This is achieved by extending !vcall_visibility to be able to specify an offset range that it applies to.
1 parent ec3ee55 commit d4a606b

File tree

8 files changed

+264
-19
lines changed

8 files changed

+264
-19
lines changed

llvm/docs/TypeMetadata.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,11 @@ In addition, all function pointer loads from a vtable marked with the
288288
calls sites can be correlated with the vtables which they might load from.
289289
Other parts of the vtable (RTTI, offset-to-top, ...) can still be accessed with
290290
normal loads.
291+
292+
Alternatively, the ``!vcall_visibility`` metadata attachment can have an
293+
extended format of a tuple with two additional integer values representing the
294+
begin and end offset (a half-open range, the begin offset is included, the end
295+
offset is excluded) within the vtable that the visibility applies to. When the
296+
range is missing, the meaning is the same as a range covering the entire vtable.
297+
Any part of the vtable that is not covered by the specified range is not
298+
eligible for elimination of virtual functions.

llvm/include/llvm/IR/GlobalObject.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ class GlobalObject : public GlobalValue {
138138
void addTypeMetadata(unsigned Offset, Metadata *TypeID);
139139
void setVCallVisibilityMetadata(VCallVisibility Visibility);
140140
VCallVisibility getVCallVisibility() const;
141+
std::pair<uint64_t, uint64_t> getVTableOffsetRange() const;
141142

142143
/// Returns true if the alignment of the value can be unilaterally
143144
/// increased.

llvm/include/llvm/Transforms/IPO/GlobalDCE.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ class GlobalDCEPass : public PassInfoMixin<GlobalDCEPass> {
4747
DenseMap<Metadata *, SmallSet<std::pair<GlobalVariable *, uint64_t>, 4>>
4848
TypeIdMap;
4949

50-
// Global variables which are vtables, and which we have enough information
51-
// about to safely do dead virtual function elimination.
52-
SmallPtrSet<GlobalValue *, 32> VFESafeVTables;
50+
/// VTable -> set of vfuncs. This only contains vtables for which we have
51+
/// enough information to safely do dead virtual function elimination, and
52+
/// only contains vfuncs that are within the range specified in
53+
/// !vcall_visibility).
54+
DenseMap<GlobalValue *, SmallPtrSet<GlobalValue *, 8>> VFESafeVTablesAndFns;
5355

5456
void UpdateGVDependencies(GlobalValue &GV);
5557
void MarkLive(GlobalValue &GV,

llvm/lib/IR/Metadata.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1525,6 +1525,23 @@ GlobalObject::VCallVisibility GlobalObject::getVCallVisibility() const {
15251525
return VCallVisibility::VCallVisibilityPublic;
15261526
}
15271527

1528+
std::pair<uint64_t, uint64_t> GlobalObject::getVTableOffsetRange() const {
1529+
if (MDNode *MD = getMetadata(LLVMContext::MD_vcall_visibility)) {
1530+
if (MD->getNumOperands() >= 3) {
1531+
uint64_t RangeStart =
1532+
cast<ConstantInt>(
1533+
cast<ConstantAsMetadata>(MD->getOperand(1))->getValue())
1534+
->getZExtValue();
1535+
uint64_t RangeEnd =
1536+
cast<ConstantInt>(
1537+
cast<ConstantAsMetadata>(MD->getOperand(2))->getValue())
1538+
->getZExtValue();
1539+
return std::make_pair(RangeStart, RangeEnd);
1540+
}
1541+
}
1542+
return std::make_pair(0, std::numeric_limits<uint64_t>::max());
1543+
}
1544+
15281545
void Function::setSubprogram(DISubprogram *SP) {
15291546
setMetadata(LLVMContext::MD_dbg, SP);
15301547
}

llvm/lib/IR/Verifier.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,40 @@ void Verifier::visitGlobalVariable(const GlobalVariable &GV) {
749749
"DIGlobalVariableExpression");
750750
}
751751

752+
MDs.clear();
753+
GV.getMetadata(LLVMContext::MD_vcall_visibility, MDs);
754+
for (auto *MD : MDs) {
755+
Assert(MD->getNumOperands() >= 1, "bad !vcall_visibility attachment");
756+
Assert(isa<ConstantAsMetadata>(MD->getOperand(0)),
757+
"bad !vcall_visibility attachment");
758+
auto *Op0Val = cast<ConstantAsMetadata>(MD->getOperand(0))->getValue();
759+
Assert(isa<ConstantInt>(Op0Val), "bad !vcall_visibility attachment");
760+
auto Op0Int = cast<ConstantInt>(Op0Val)->getValue();
761+
Assert(Op0Int.uge(0) && Op0Int.ult(std::numeric_limits<uint64_t>::max()),
762+
"bad !vcall_visibility attachment");
763+
if (MD->getNumOperands() == 3) {
764+
Assert(isa<ConstantAsMetadata>(MD->getOperand(1)),
765+
"bad !vcall_visibility attachment");
766+
auto *Op1Val = cast<ConstantAsMetadata>(MD->getOperand(1))->getValue();
767+
Assert(isa<ConstantInt>(Op1Val), "bad !vcall_visibility attachment");
768+
auto Op1Int = cast<ConstantInt>(Op1Val)->getValue();
769+
Assert(Op1Int.uge(0) && Op1Int.ult(std::numeric_limits<uint64_t>::max()),
770+
"bad !vcall_visibility attachment");
771+
772+
Assert(isa<ConstantAsMetadata>(MD->getOperand(2)),
773+
"bad !vcall_visibility attachment");
774+
auto *Op2Val = cast<ConstantAsMetadata>(MD->getOperand(2))->getValue();
775+
Assert(isa<ConstantInt>(Op2Val), "bad !vcall_visibility attachment");
776+
auto Op2Int = cast<ConstantInt>(Op2Val)->getValue();
777+
Assert(Op2Int.uge(0) && Op2Int.ult(std::numeric_limits<uint64_t>::max()),
778+
"bad !vcall_visibility attachment");
779+
780+
Assert(Op1Int.ule(Op2Int), "bad !vcall_visibility attachment");
781+
} else {
782+
Assert(MD->getNumOperands() == 1, "bad !vcall_visibility attachment");
783+
}
784+
}
785+
752786
// Scalable vectors cannot be global variables, since we don't know
753787
// the runtime size. If the global is an array containing scalable vectors,
754788
// that will be caught by the isValidElementType methods in StructType or

llvm/lib/Transforms/IPO/GlobalDCE.cpp

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,12 @@ void GlobalDCEPass::UpdateGVDependencies(GlobalValue &GV) {
127127
ComputeDependencies(User, Deps);
128128
Deps.erase(&GV); // Remove self-reference.
129129
for (GlobalValue *GVU : Deps) {
130-
// If this is a dep from a vtable to a virtual function, and we have
131-
// complete information about all virtual call sites which could call
132-
// though this vtable, then skip it, because the call site information will
133-
// be more precise.
134-
if (VFESafeVTables.count(GVU) && isa<Function>(&GV)) {
130+
// If this is a dep from a vtable to a virtual function, and it's within the
131+
// range specified in !vcall_visibility, and we have complete information
132+
// about all virtual call sites which could call though this vtable, then
133+
// skip it, because the call site information will be more precise.
134+
if (isa<Function>(&GV) && VFESafeVTablesAndFns.count(GVU) &&
135+
VFESafeVTablesAndFns[GVU].contains(&GV)) {
135136
LLVM_DEBUG(dbgs() << "Ignoring dep " << GVU->getName() << " -> "
136137
<< GV.getName() << "\n");
137138
continue;
@@ -157,6 +158,47 @@ void GlobalDCEPass::MarkLive(GlobalValue &GV,
157158
}
158159
}
159160

161+
/// Recursively iterate over the (sub-)constants in the vtable and look for
162+
/// vptrs, if their offset is within [RangeStart..RangeEnd), add them to VFuncs.
163+
static void FindVirtualFunctionsInVTable(Module &M, Constant *C,
164+
uint64_t RangeStart, uint64_t RangeEnd,
165+
SmallPtrSet<GlobalValue *, 8> *VFuncs,
166+
uint64_t BaseOffset = 0) {
167+
if (auto *GV = dyn_cast<GlobalValue>(C)) {
168+
if (auto *F = dyn_cast<Function>(GV))
169+
if (RangeStart <= BaseOffset && BaseOffset < RangeEnd)
170+
VFuncs->insert(F);
171+
172+
// Do not recurse outside of the current global.
173+
return;
174+
}
175+
176+
if (auto *S = dyn_cast<ConstantStruct>(C)) {
177+
StructType *STy = dyn_cast<StructType>(S->getType());
178+
const StructLayout *SL = M.getDataLayout().getStructLayout(STy);
179+
for (auto EI : llvm::enumerate(STy->elements())) {
180+
auto Offset = SL->getElementOffset(EI.index());
181+
unsigned Op = SL->getElementContainingOffset(Offset);
182+
FindVirtualFunctionsInVTable(M, cast<Constant>(S->getOperand(Op)),
183+
RangeStart, RangeEnd, VFuncs,
184+
BaseOffset + Offset);
185+
}
186+
} else if (auto *A = dyn_cast<ConstantArray>(C)) {
187+
ArrayType *ATy = A->getType();
188+
auto EltSize = M.getDataLayout().getTypeAllocSize(ATy->getElementType());
189+
for (unsigned i = 0, e = ATy->getNumElements(); i != e; ++i) {
190+
FindVirtualFunctionsInVTable(M, cast<Constant>(A->getOperand(i)),
191+
RangeStart, RangeEnd, VFuncs,
192+
BaseOffset + EltSize * i);
193+
}
194+
} else {
195+
for (auto &Op : C->operands()) {
196+
FindVirtualFunctionsInVTable(M, cast<Constant>(Op), RangeStart, RangeEnd,
197+
VFuncs, BaseOffset);
198+
}
199+
}
200+
}
201+
160202
void GlobalDCEPass::ScanVTables(Module &M) {
161203
SmallVector<MDNode *, 2> Types;
162204
LLVM_DEBUG(dbgs() << "Building type info -> vtable map\n");
@@ -196,7 +238,14 @@ void GlobalDCEPass::ScanVTables(Module &M) {
196238
(LTOPostLink &&
197239
TypeVis == GlobalObject::VCallVisibilityLinkageUnit)) {
198240
LLVM_DEBUG(dbgs() << GV.getName() << " is safe for VFE\n");
199-
VFESafeVTables.insert(&GV);
241+
242+
// Find and record all the vfunctions that are within the offset range
243+
// specified in the !vcall_visibility attribute.
244+
auto Range = GO->getVTableOffsetRange();
245+
SmallPtrSet<GlobalValue *, 8> VFuncs;
246+
FindVirtualFunctionsInVTable(M, GV.getInitializer(), std::get<0>(Range),
247+
std::get<1>(Range), &VFuncs);
248+
VFESafeVTablesAndFns[&GV] = VFuncs;
200249
}
201250
}
202251
}
@@ -213,14 +262,14 @@ void GlobalDCEPass::ScanVTableLoad(Function *Caller, Metadata *TypeId,
213262
*Caller->getParent(), VTable);
214263
if (!Ptr) {
215264
LLVM_DEBUG(dbgs() << "can't find pointer in vtable!\n");
216-
VFESafeVTables.erase(VTable);
265+
VFESafeVTablesAndFns.erase(VTable);
217266
return;
218267
}
219268

220269
auto Callee = dyn_cast<Function>(Ptr->stripPointerCasts());
221270
if (!Callee) {
222271
LLVM_DEBUG(dbgs() << "vtable entry is not function pointer!\n");
223-
VFESafeVTables.erase(VTable);
272+
VFESafeVTablesAndFns.erase(VTable);
224273
return;
225274
}
226275

@@ -253,7 +302,7 @@ void GlobalDCEPass::ScanTypeCheckedLoadIntrinsics(Module &M) {
253302
// type.checked.load with a non-constant offset, so assume every entry in
254303
// every matching vtable is used.
255304
for (auto &VTableInfo : TypeIdMap[TypeId]) {
256-
VFESafeVTables.erase(VTableInfo.first);
305+
VFESafeVTablesAndFns.erase(VTableInfo.first);
257306
}
258307
}
259308
}
@@ -274,16 +323,15 @@ void GlobalDCEPass::AddVirtualFunctionDependencies(Module &M) {
274323

275324
ScanVTables(M);
276325

277-
if (VFESafeVTables.empty())
326+
if (VFESafeVTablesAndFns.empty())
278327
return;
279328

280329
ScanTypeCheckedLoadIntrinsics(M);
281330

282-
LLVM_DEBUG(
283-
dbgs() << "VFE safe vtables:\n";
284-
for (auto *VTable : VFESafeVTables)
285-
dbgs() << " " << VTable->getName() << "\n";
286-
);
331+
LLVM_DEBUG(dbgs() << "VFE safe vtables:\n";
332+
for (auto &Entry
333+
: VFESafeVTablesAndFns) dbgs()
334+
<< " " << Entry.first->getName() << "\n";);
287335
}
288336

289337
PreservedAnalyses GlobalDCEPass::run(Module &M, ModuleAnalysisManager &MAM) {
@@ -449,7 +497,7 @@ PreservedAnalyses GlobalDCEPass::run(Module &M, ModuleAnalysisManager &MAM) {
449497
GVDependencies.clear();
450498
ComdatMembers.clear();
451499
TypeIdMap.clear();
452-
VFESafeVTables.clear();
500+
VFESafeVTablesAndFns.clear();
453501

454502
if (Changed)
455503
return PreservedAnalyses::none();
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
; RUN: opt < %s -globaldce -S | FileCheck %s
2+
3+
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
4+
5+
declare { i8*, i1 } @llvm.type.checked.load(i8*, i32, metadata)
6+
7+
; A vtable that contains a non-nfunc entry, @regular_non_virtual_funcA, but
8+
; without a range specific in !vcall_visibility, which means *all* function
9+
; pointers are eligible for VFE, so GlobalDCE will treat the
10+
; @regular_non_virtual_funcA slot as eligible for VFE, and remove it.
11+
@vtableA = internal unnamed_addr constant { [3 x i8*] } { [3 x i8*] [
12+
i8* bitcast (void ()* @vfunc1_live to i8*),
13+
i8* bitcast (void ()* @vfunc2_dead to i8*),
14+
i8* bitcast (void ()* @regular_non_virtual_funcA to i8*)
15+
]}, align 8, !type !{i64 0, !"vfunc1.type"}, !type !{i64 8, !"vfunc2.type"}, !vcall_visibility !{i64 2}
16+
17+
; CHECK: @vtableA = internal unnamed_addr constant { [3 x i8*] } { [3 x i8*] [
18+
; CHECK-SAME: i8* bitcast (void ()* @vfunc1_live to i8*),
19+
; CHECK-SAME: i8* null,
20+
; CHECK-SAME: i8* null
21+
; CHECK-SAME: ] }, align 8
22+
23+
24+
; A vtable that contains a non-nfunc entry, @regular_non_virtual_funcB, with a
25+
; range of [0,16) which means only the first two entries are eligible for VFE.
26+
; GlobalDCE should keep @regular_non_virtual_funcB in the vtable.
27+
@vtableB = internal unnamed_addr constant { [3 x i8*] } { [3 x i8*] [
28+
i8* bitcast (void ()* @vfunc1_live to i8*),
29+
i8* bitcast (void ()* @vfunc2_dead to i8*),
30+
i8* bitcast (void ()* @regular_non_virtual_funcB to i8*)
31+
]}, align 8, !type !{i64 0, !"vfunc1.type"}, !type !{i64 8, !"vfunc2.type"}, !vcall_visibility !{i64 2, i64 0, i64 16}
32+
33+
; CHECK: @vtableB = internal unnamed_addr constant { [3 x i8*] } { [3 x i8*] [
34+
; CHECK-SAME: i8* bitcast (void ()* @vfunc1_live to i8*),
35+
; CHECK-SAME: i8* null,
36+
; CHECK-SAME: i8* bitcast (void ()* @regular_non_virtual_funcB to i8*)
37+
; CHECK-SAME: ] }, align 8
38+
39+
; A vtable that contains a non-nfunc entry, @regular_non_virtual_funcB, with a
40+
; range of [0,16) which means only the first two entries are eligible for VFE.
41+
; GlobalDCE should keep @regular_non_virtual_funcB in the vtable.
42+
@vtableC = internal unnamed_addr constant { [3 x i8*] } { [3 x i8*] [
43+
i8* bitcast (void ()* @regular_non_virtual_funcC to i8*),
44+
i8* bitcast (void ()* @vfunc1_live to i8*),
45+
i8* bitcast (void ()* @vfunc2_dead to i8*)
46+
]}, align 8, !type !{i64 8, !"vfunc1.type"}, !type !{i64 16, !"vfunc2.type"}, !vcall_visibility !{i64 2, i64 8, i64 24}
47+
48+
; CHECK: @vtableC = internal unnamed_addr constant { [3 x i8*] } { [3 x i8*] [
49+
; CHECK-SAME: i8* bitcast (void ()* @regular_non_virtual_funcC to i8*),
50+
; CHECK-SAME: i8* bitcast (void ()* @vfunc1_live to i8*),
51+
; CHECK-SAME: i8* null
52+
; CHECK-SAME: ] }, align 8
53+
54+
; A vtable with "relative pointers"
55+
@vtableD = internal unnamed_addr constant { i32, i32, i8* } {
56+
i32 trunc (i64 sub (i64 ptrtoint (void ()* @vfunc1_live to i64), i64 ptrtoint ({ i32, i32, i8* }* @vtableD to i64)) to i32),
57+
i32 trunc (i64 sub (i64 ptrtoint (void ()* @vfunc2_dead to i64), i64 ptrtoint ({ i32, i32, i8* }* @vtableD to i64)) to i32),
58+
i8* bitcast (void ()* @regular_non_virtual_funcD to i8*)
59+
}, align 8, !type !{i64 0, !"vfunc1.type"}, !type !{i64 4, !"vfunc2.type"}, !vcall_visibility !{i64 2, i64 0, i64 8}
60+
61+
; CHECK: @vtableD = internal unnamed_addr constant { i32, i32, i8* } {
62+
; CHECK-SAME: i32 trunc (i64 sub (i64 ptrtoint (void ()* @vfunc1_live to i64), i64 ptrtoint ({ i32, i32, i8* }* @vtableD to i64)) to i32),
63+
; CHECK-SAME: i32 0,
64+
; CHECK-SAME: i8* bitcast (void ()* @regular_non_virtual_funcD to i8*)
65+
; CHECK-SAME: }, align 8
66+
67+
; (1) vfunc1_live is referenced from @main, stays alive
68+
define internal void @vfunc1_live() {
69+
; CHECK: define internal void @vfunc1_live(
70+
ret void
71+
}
72+
73+
; (2) vfunc2_dead is never referenced, gets removed and vtable slot is null'd
74+
define internal void @vfunc2_dead() {
75+
; CHECK-NOT: define internal void @vfunc2_dead(
76+
ret void
77+
}
78+
79+
; (3) not using a range in !vcall_visibility, global gets removed
80+
define internal void @regular_non_virtual_funcA() {
81+
; CHECK-NOT: define internal void @regular_non_virtual_funcA(
82+
ret void
83+
}
84+
85+
; (4) using a range in !vcall_visibility, pointer is outside of range, so should
86+
; stay alive
87+
define internal void @regular_non_virtual_funcB() {
88+
; CHECK: define internal void @regular_non_virtual_funcB(
89+
ret void
90+
}
91+
92+
; (5) using a range in !vcall_visibility, pointer is outside of range, so should
93+
; stay alive
94+
define internal void @regular_non_virtual_funcC() {
95+
; CHECK: define internal void @regular_non_virtual_funcC(
96+
ret void
97+
}
98+
99+
; (6) using a range in !vcall_visibility, pointer is outside of range, so should
100+
; stay alive
101+
define internal void @regular_non_virtual_funcD() {
102+
; CHECK: define internal void @regular_non_virtual_funcD(
103+
ret void
104+
}
105+
106+
define void @main() {
107+
%1 = ptrtoint { [3 x i8*] }* @vtableA to i64 ; to keep @vtableA alive
108+
%2 = ptrtoint { [3 x i8*] }* @vtableB to i64 ; to keep @vtableB alive
109+
%3 = ptrtoint { [3 x i8*] }* @vtableC to i64 ; to keep @vtableC alive
110+
%4 = ptrtoint { i32, i32, i8* }* @vtableD to i64 ; to keep @vtableD alive
111+
%5 = tail call { i8*, i1 } @llvm.type.checked.load(i8* null, i32 0, metadata !"vfunc1.type")
112+
ret void
113+
}
114+
115+
!999 = !{i32 1, !"Virtual Function Elim", i32 1}
116+
!llvm.module.flags = !{!999}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
; RUN: not opt -S -verify < %s 2>&1 | FileCheck %s
2+
3+
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
4+
5+
; vcall_visibility must have either 1 or 3 operands
6+
@vtableA = internal unnamed_addr constant { [1 x i8*] } { [1 x i8*] [i8* null]}, align 8, !vcall_visibility !{i64 2, i64 42}
7+
; CHECK: bad !vcall_visibility attachment
8+
9+
; range start cannot be greater than range end
10+
@vtableB = internal unnamed_addr constant { [1 x i8*] } { [1 x i8*] [i8* null]}, align 8, !vcall_visibility !{i64 2, i64 10, i64 8}
11+
; CHECK: bad !vcall_visibility attachment
12+
13+
; vcall_visibility range cannot be over 64 bits
14+
@vtableC = internal unnamed_addr constant { [1 x i8*] } { [1 x i8*] [i8* null]}, align 8, !vcall_visibility !{i64 2, i128 0, i128 9223372036854775808000}
15+
; CHECK: bad !vcall_visibility attachment
16+
17+
; range must be two integers
18+
@vtableD = internal unnamed_addr constant { [1 x i8*] } { [1 x i8*] [i8* null]}, align 8, !vcall_visibility !{i64 2, i64 0, !"string"}
19+
; CHECK: bad !vcall_visibility attachment

0 commit comments

Comments
 (0)