Skip to content

Commit

Permalink
[llvm] Teach whole program devirtualization about relative vtables
Browse files Browse the repository at this point in the history
Prior to this patch, WPD was not acting on relative-vtables in C++. This
involves teaching WPD about these things:

- llvm.load.relative which is how relative-vtables are indexed (instead of GEP)
- dso_local_equivalent which is used in the vtable itself when taking the
  offset between a virtual function and vtable
- Update llvm/test/ThinLTO/X86/devirt.ll to use opaque pointers and add
  equivalent tests for RV

Differential Revision: https://reviews.llvm.org/D134320
  • Loading branch information
PiJoules committed Feb 23, 2023
1 parent bbddaad commit db28818
Show file tree
Hide file tree
Showing 12 changed files with 411 additions and 11 deletions.
2 changes: 1 addition & 1 deletion llvm/include/llvm/Analysis/TypeMetadataUtils.h
Expand Up @@ -64,7 +64,7 @@ void findDevirtualizableCallsForTypeCheckedLoad(
/// Used for example from GlobalDCE to find an entry in a C++ vtable that
/// matches a vcall offset.
///
/// To support Swift vtables, getPointerAtOffset can see through "relative
/// To support relative vtables, getPointerAtOffset can see through "relative
/// pointers", i.e. (sub-)expressions of the form of:
///
/// @symbol = ... {
Expand Down
37 changes: 33 additions & 4 deletions llvm/lib/Analysis/ModuleSummaryAnalysis.cpp
Expand Up @@ -22,6 +22,7 @@
#include "llvm/ADT/StringRef.h"
#include "llvm/Analysis/BlockFrequencyInfo.h"
#include "llvm/Analysis/BranchProbabilityInfo.h"
#include "llvm/Analysis/ConstantFolding.h"
#include "llvm/Analysis/IndirectCallPromotionAnalysis.h"
#include "llvm/Analysis/LoopInfo.h"
#include "llvm/Analysis/MemoryProfileInfo.h"
Expand Down Expand Up @@ -580,7 +581,8 @@ static void computeFunctionSummary(
/// within the initializer.
static void findFuncPointers(const Constant *I, uint64_t StartingOffset,
const Module &M, ModuleSummaryIndex &Index,
VTableFuncList &VTableFuncs) {
VTableFuncList &VTableFuncs,
const GlobalVariable &OrigGV) {
// First check if this is a function pointer.
if (I->getType()->isPointerTy()) {
auto C = I->stripPointerCasts();
Expand Down Expand Up @@ -608,15 +610,42 @@ static void findFuncPointers(const Constant *I, uint64_t StartingOffset,
auto Offset = SL->getElementOffset(EI.index());
unsigned Op = SL->getElementContainingOffset(Offset);
findFuncPointers(cast<Constant>(I->getOperand(Op)),
StartingOffset + Offset, M, Index, VTableFuncs);
StartingOffset + Offset, M, Index, VTableFuncs, OrigGV);
}
} else if (auto *C = dyn_cast<ConstantArray>(I)) {
ArrayType *ATy = C->getType();
Type *EltTy = ATy->getElementType();
uint64_t EltSize = DL.getTypeAllocSize(EltTy);
for (unsigned i = 0, e = ATy->getNumElements(); i != e; ++i) {
findFuncPointers(cast<Constant>(I->getOperand(i)),
StartingOffset + i * EltSize, M, Index, VTableFuncs);
StartingOffset + i * EltSize, M, Index, VTableFuncs,
OrigGV);
}
} else if (const auto *CE = dyn_cast<ConstantExpr>(I)) {
// For relative vtables, the next sub-component should be a trunc.
if (CE->getOpcode() != Instruction::Trunc ||
!(CE = dyn_cast<ConstantExpr>(CE->getOperand(0))))
return;

// If this constant can be reduced to the offset between a function and a
// global, then we know this is a valid virtual function if the RHS is the
// original vtable we're scanning through.
if (CE->getOpcode() == Instruction::Sub) {
GlobalValue *LHS, *RHS;
APSInt LHSOffset, RHSOffset;
if (IsConstantOffsetFromGlobal(CE->getOperand(0), LHS, LHSOffset, DL) &&
IsConstantOffsetFromGlobal(CE->getOperand(1), RHS, RHSOffset, DL) &&
RHS == &OrigGV &&

// For relative vtables, this component should point to the callable
// function without any offsets.
LHSOffset == 0 &&

// Also, the RHS should always point to somewhere within the vtable.
RHSOffset <=
static_cast<uint64_t>(DL.getTypeAllocSize(OrigGV.getInitializer()->getType()))) {
findFuncPointers(LHS, StartingOffset, M, Index, VTableFuncs, OrigGV);
}
}
}
}
Expand All @@ -629,7 +658,7 @@ static void computeVTableFuncs(ModuleSummaryIndex &Index,
return;

findFuncPointers(V.getInitializer(), /*StartingOffset=*/0, M, Index,
VTableFuncs);
VTableFuncs, V);

#ifndef NDEBUG
// Validate that the VTableFuncs list is ordered by offset.
Expand Down
16 changes: 15 additions & 1 deletion llvm/lib/Analysis/TypeMetadataUtils.cpp
Expand Up @@ -67,6 +67,14 @@ static void findLoadCallsAtConstantOffset(
findLoadCallsAtConstantOffset(M, DevirtCalls, User, Offset + GEPOffset,
CI, DT);
}
} else if (auto *Call = dyn_cast<CallInst>(User)) {
if (Call->getIntrinsicID() == llvm::Intrinsic::load_relative) {
if (auto *LoadOffset = dyn_cast<ConstantInt>(Call->getOperand(1))) {
findCallsAtConstantOffset(DevirtCalls, nullptr, User,
Offset + LoadOffset->getSExtValue(), CI,
DT);
}
}
}
}
}
Expand Down Expand Up @@ -129,6 +137,12 @@ void llvm::findDevirtualizableCallsForTypeCheckedLoad(

Constant *llvm::getPointerAtOffset(Constant *I, uint64_t Offset, Module &M,
Constant *TopLevelGlobal) {
// TODO: Ideally it would be the caller who knows if it's appropriate to strip
// the DSOLocalEquicalent. More generally, it would feel more appropriate to
// have two functions that handle absolute and relative pointers separately.
if (auto *Equiv = dyn_cast<DSOLocalEquivalent>(I))
I = Equiv->getGlobalValue();

if (I->getType()->isPointerTy()) {
if (Offset == 0)
return I;
Expand Down Expand Up @@ -159,7 +173,7 @@ Constant *llvm::getPointerAtOffset(Constant *I, uint64_t Offset, Module &M,
Offset % ElemSize, M, TopLevelGlobal);
}

// (Swift-specific) relative-pointer support starts here.
// Relative-pointer support starts here.
if (auto *CI = dyn_cast<ConstantInt>(I)) {
if (Offset == 0 && CI->getZExtValue() == 0) {
return I;
Expand Down
2 changes: 1 addition & 1 deletion llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
Expand Up @@ -1006,7 +1006,7 @@ bool DevirtModule::tryFindVirtualCallTargets(
return false;

Constant *Ptr = getPointerAtOffset(TM.Bits->GV->getInitializer(),
TM.Offset + ByteOffset, M);
TM.Offset + ByteOffset, M, TM.Bits->GV);
if (!Ptr)
return false;

Expand Down
95 changes: 92 additions & 3 deletions llvm/test/ThinLTO/X86/devirt.ll
Expand Up @@ -27,24 +27,36 @@
; NOENABLESPLITFLAG-DAG: [[B:\^[0-9]+]] = gv: (name: "_ZTV1B", {{.*}} vTableFuncs: ((virtFunc: [[Bf]], offset: 16), (virtFunc: [[An]], offset: 24)), refs: ([[Bf]], [[An]])
; NOENABLESPLITFLAG-DAG: [[C:\^[0-9]+]] = gv: (name: "_ZTV1C", {{.*}} vTableFuncs: ((virtFunc: [[Cf]], offset: 16), (virtFunc: [[An]], offset: 24)), refs: ([[An]], [[Cf]])
; NOENABLESPLITFLAG-DAG: [[D:\^[0-9]+]] = gv: (name: "_ZTV1D", {{.*}} vTableFuncs: ((virtFunc: [[Dm]], offset: 16)), refs: ([[Dm]])
; NOENABLESPLITFLAG-DAG: [[B_RV:\^[0-9]+]] = gv: (name: "_ZTV1B_RV", {{.*}} vTableFuncs: ((virtFunc: [[Bf]], offset: 8), (virtFunc: [[An]], offset: 12)), refs: ([[B_RV]], [[Bf]], [[An]])
; NOENABLESPLITFLAG-DAG: [[C_RV:\^[0-9]+]] = gv: (name: "_ZTV1C_RV", {{.*}} vTableFuncs: ((virtFunc: [[Cf]], offset: 8), (virtFunc: [[An]], offset: 12)), refs: ([[C_RV]], [[An]], [[Cf]])
; NOENABLESPLITFLAG-DAG: [[D_RV:\^[0-9]+]] = gv: (name: "_ZTV1D_RV", {{.*}} vTableFuncs: ((virtFunc: [[Dm]], offset: 8)), refs: ([[D_RV]], [[Dm]])
; NOENABLESPLITFLAG-DAG: typeidCompatibleVTable: (name: "_ZTS1A", summary: ((offset: 16, [[B]]), (offset: 16, [[C]])))
; NOENABLESPLITFLAG-DAG: typeidCompatibleVTable: (name: "_ZTS1B", summary: ((offset: 16, [[B]])))
; NOENABLESPLITFLAG-DAG: typeidCompatibleVTable: (name: "_ZTS1C", summary: ((offset: 16, [[C]])))
; NOENABLESPLITFLAG-DAG: typeidCompatibleVTable: (name: "_ZTS1A_RV", summary: ((offset: 8, [[B_RV]]), (offset: 8, [[C_RV]])))
; NOENABLESPLITFLAG-DAG: typeidCompatibleVTable: (name: "_ZTS1B_RV", summary: ((offset: 8, [[B_RV]])))
; NOENABLESPLITFLAG-DAG: typeidCompatibleVTable: (name: "_ZTS1C_RV", summary: ((offset: 8, [[C_RV]])))
; Type Id on _ZTV1D should have been promoted
; NOENABLESPLITFLAG-DAG: typeidCompatibleVTable: (name: "1.{{.*}}", summary: ((offset: 16, [[D]])))
; NOENABLESPLITFLAG-DAG: typeidCompatibleVTable: (name: "2.{{.*}}", summary: ((offset: 8, [[D_RV]])))

; Index based WPD
; RUN: llvm-lto2 run %t2.o -save-temps -pass-remarks=. \
; RUN: -whole-program-visibility \
; RUN: -o %t3 \
; RUN: -r=%t2.o,test,px \
; RUN: -r=%t2.o,test_rv,px \
; RUN: -r=%t2.o,_ZN1A1nEi,p \
; RUN: -r=%t2.o,_ZN1B1fEi,p \
; RUN: -r=%t2.o,_ZN1C1fEi,p \
; RUN: -r=%t2.o,_ZN1D1mEi,p \
; RUN: -r=%t2.o,_ZTV1B,px \
; RUN: -r=%t2.o,_ZTV1C,px \
; RUN: -r=%t2.o,_ZTV1D,px 2>&1 | FileCheck %s --check-prefix=REMARK
; RUN: -r=%t2.o,_ZTV1D,px \
; RUN: -r=%t2.o,_ZTV1B_RV,px \
; RUN: -r=%t2.o,_ZTV1C_RV,px \
; RUN: -r=%t2.o,_ZTV1D_RV,px \
; RUN: 2>&1 | FileCheck %s --check-prefix=REMARK
; RUN: llvm-dis %t3.1.4.opt.bc -o - | FileCheck %s --check-prefix=CHECK-IR

; Check that we're able to prevent specific function from being
Expand All @@ -54,34 +66,49 @@
; RUN: -wholeprogramdevirt-skip=_ZN1A1nEi \
; RUN: -o %t3 \
; RUN: -r=%t2.o,test,px \
; RUN: -r=%t2.o,test_rv,px \
; RUN: -r=%t2.o,_ZN1A1nEi,p \
; RUN: -r=%t2.o,_ZN1B1fEi,p \
; RUN: -r=%t2.o,_ZN1C1fEi,p \
; RUN: -r=%t2.o,_ZN1D1mEi,p \
; RUN: -r=%t2.o,_ZTV1B,px \
; RUN: -r=%t2.o,_ZTV1C,px \
; RUN: -r=%t2.o,_ZTV1D,px 2>&1 | FileCheck %s --check-prefix=SKIP
; RUN: -r=%t2.o,_ZTV1D,px \
; RUN: -r=%t2.o,_ZTV1B_RV,px \
; RUN: -r=%t2.o,_ZTV1C_RV,px \
; RUN: -r=%t2.o,_ZTV1D_RV,px \
; RUN: 2>&1 | FileCheck %s --check-prefix=SKIP

; RUN: llvm-lto2 run %t.o -save-temps -pass-remarks=. \
; RUN: -whole-program-visibility \
; RUN: -o %t3 \
; RUN: -r=%t.o,test,px \
; RUN: -r=%t.o,test_rv,px \
; RUN: -r=%t.o,_ZN1A1nEi,p \
; RUN: -r=%t.o,_ZN1B1fEi,p \
; RUN: -r=%t.o,_ZN1C1fEi,p \
; RUN: -r=%t.o,_ZN1D1mEi,p \
; RUN: -r=%t.o,_ZTV1B, \
; RUN: -r=%t.o,_ZTV1C, \
; RUN: -r=%t.o,_ZTV1D, \
; RUN: -r=%t.o,_ZTV1B_RV, \
; RUN: -r=%t.o,_ZTV1C_RV, \
; RUN: -r=%t.o,_ZTV1D_RV, \
; RUN: -r=%t.o,_ZN1A1nEi, \
; RUN: -r=%t.o,_ZN1B1fEi, \
; RUN: -r=%t.o,_ZN1C1fEi, \
; RUN: -r=%t.o,_ZN1D1mEi, \
; RUN: -r=%t.o,_ZTV1B,px \
; RUN: -r=%t.o,_ZTV1C,px \
; RUN: -r=%t.o,_ZTV1D,px 2>&1 | FileCheck %s --check-prefix=REMARK --dump-input=fail
; RUN: -r=%t.o,_ZTV1D,px \
; RUN: -r=%t.o,_ZTV1B_RV,px \
; RUN: -r=%t.o,_ZTV1C_RV,px \
; RUN: -r=%t.o,_ZTV1D_RV,px \
; RUN: 2>&1 | FileCheck %s --check-prefix=REMARK --dump-input=fail
; RUN: llvm-dis %t3.1.4.opt.bc -o - | FileCheck %s --check-prefix=CHECK-IR

; REMARK-DAG: single-impl: devirtualized a call to _ZN1A1nEi
; REMARK-DAG: single-impl: devirtualized a call to _ZN1D1mEi
; REMARK-DAG: single-impl: devirtualized a call to _ZN1A1nEi
; REMARK-DAG: single-impl: devirtualized a call to _ZN1D1mEi

Expand All @@ -99,6 +126,25 @@ target triple = "x86_64-grtev4-linux-gnu"
@_ZTV1C = constant { [4 x ptr] } { [4 x ptr] [ptr null, ptr undef, ptr @_ZN1C1fEi, ptr @_ZN1A1nEi] }, !type !0, !type !2
@_ZTV1D = constant { [3 x ptr] } { [3 x ptr] [ptr null, ptr undef, ptr @_ZN1D1mEi] }, !type !3

@_ZTV1B_RV = constant { [4 x i32] } { [4 x i32] [
i32 0,
i32 undef,
i32 trunc (i64 sub (i64 ptrtoint (ptr dso_local_equivalent @_ZN1B1fEi to i64), i64 ptrtoint (ptr getelementptr inbounds ({ [4 x i32] }, ptr @_ZTV1B_RV, i32 0, i32 0, i32 2) to i64)) to i32),
i32 trunc (i64 sub (i64 ptrtoint (ptr dso_local_equivalent @_ZN1A1nEi to i64), i64 ptrtoint (ptr getelementptr inbounds ({ [4 x i32] }, ptr @_ZTV1B_RV, i32 0, i32 0, i32 3) to i64)) to i32)
] }, !type !7, !type !8

@_ZTV1C_RV = constant { [4 x i32] } { [4 x i32] [
i32 0,
i32 undef,
i32 trunc (i64 sub (i64 ptrtoint (ptr dso_local_equivalent @_ZN1C1fEi to i64), i64 ptrtoint (ptr getelementptr inbounds ({ [4 x i32] }, ptr @_ZTV1C_RV, i32 0, i32 0, i32 2) to i64)) to i32),
i32 trunc (i64 sub (i64 ptrtoint (ptr dso_local_equivalent @_ZN1A1nEi to i64), i64 ptrtoint (ptr getelementptr inbounds ({ [4 x i32] }, ptr @_ZTV1C_RV, i32 0, i32 0, i32 3) to i64)) to i32)
] }, !type !7, !type !9

@_ZTV1D_RV = constant { [3 x i32] } { [3 x i32] [
i32 0,
i32 undef,
i32 trunc (i64 sub (i64 ptrtoint (ptr dso_local_equivalent @_ZN1D1mEi to i64), i64 ptrtoint (ptr getelementptr inbounds ({ [3 x i32] }, ptr @_ZTV1D_RV, i32 0, i32 0, i32 2) to i64)) to i32)
] }, !type !10

; CHECK-IR-LABEL: define i32 @test
define i32 @test(ptr %obj, ptr %obj2, i32 %a) {
Expand Down Expand Up @@ -136,6 +182,43 @@ entry:
; CHECK-IR-LABEL: ret i32
; CHECK-IR-LABEL: }

declare ptr @llvm.load.relative.i32(ptr, i32)

; CHECK-IR-LABEL: define i32 @test_rv
define i32 @test_rv(ptr %obj, ptr %obj2, i32 %a) {
entry:
%vtable = load ptr, ptr %obj
%p = call i1 @llvm.type.test(ptr %vtable, metadata !"_ZTS1A_RV")
call void @llvm.assume(i1 %p)
%fptr1_rv = call ptr @llvm.load.relative.i32(ptr %vtable, i32 4)

; Check that the call was devirtualized.
; CHECK-IR: %call = tail call i32 @_ZN1A1nEi
; Ensure !prof and !callees metadata for indirect call promotion removed.
; CHECK-IR-NOT: prof
; CHECK-IR-NOT: callees
%call = tail call i32 %fptr1_rv(ptr nonnull %obj, i32 %a), !prof !5, !callees !6

%fptr22_rv = call ptr @llvm.load.relative.i32(ptr %vtable, i32 0)

; We still have to call it as virtual.
; CHECK-IR: %call3 = tail call i32 %fptr22
%call3 = tail call i32 %fptr22_rv(ptr nonnull %obj, i32 %call)

%vtable2 = load ptr, ptr %obj2
%p2 = call i1 @llvm.type.test(ptr %vtable2, metadata !11)
call void @llvm.assume(i1 %p2)

%fptr33_rv = call ptr @llvm.load.relative.i32(ptr %vtable2, i32 0)

; Check that the call was devirtualized.
; CHECK-IR: %call4 = tail call i32 @_ZN1D1mEi
%call4 = tail call i32 %fptr33_rv(ptr nonnull %obj2, i32 %call3)
ret i32 %call4
}
; CHECK-IR-LABEL: ret i32
; CHECK-IR-LABEL: }

declare i1 @llvm.type.test(ptr, metadata)
declare void @llvm.assume(i1)

Expand Down Expand Up @@ -165,3 +248,9 @@ attributes #0 = { noinline optnone }
!4 = distinct !{}
!5 = !{!"VP", i32 0, i64 1, i64 1621563287929432257, i64 1}
!6 = !{ptr @_ZN1A1nEi}

!7 = !{i64 8, !"_ZTS1A_RV"}
!8 = !{i64 8, !"_ZTS1B_RV"}
!9 = !{i64 8, !"_ZTS1C_RV"}
!10 = !{i64 8, !11}
!11 = distinct !{}
8 changes: 8 additions & 0 deletions llvm/test/Transforms/WholeProgramDevirt/Inputs/export.yaml
Expand Up @@ -5,14 +5,22 @@ GlobalValueMap:
TypeTestAssumeVCalls:
- GUID: 14276520915468743435 # typeid1
Offset: 0
- GUID: 271751036925422857 # typeid1_rv
Offset: 0
TypeCheckedLoadVCalls:
- GUID: 15427464259790519041 # typeid2
Offset: 0
- GUID: 1146149264729288256 # typeid2_rv
Offset: 0
TypeTestAssumeConstVCalls:
- VFunc:
GUID: 3515965990081467659 # typeid3
Offset: 0
Args: [12, 24]
- VFunc:
GUID: 2777626534618191571 # typeid3_rv
Offset: 0
Args: [12, 24]
TypeCheckedLoadConstVCalls:
- VFunc:
GUID: 17525413373118030901 # typeid4
Expand Down

0 comments on commit db28818

Please sign in to comment.