Add support for putting constructors and destructos in explicit comdats.

There are situations when clang knows that the C1 and C2 constructors
or the D1 and D2 destructors are identical. We already optimize some
of these cases, but cannot optimize it when the GlobalValue is
weak_odr.

The problem with weak_odr is that an old TU seeing the same code will
have a C1 and a C2 comdat with the corresponding symbols. We cannot
suddenly start putting the C2 symbol in the C1 comdat as we cannot
guarantee that the linker will not pick a .o with only C1 in it.

The solution implemented by GCC is to expand the ABI to have a comdat
whose name uses a C5/D5 suffix and always has both symbols. That is
what this patch implements.

llvm-svn: 217874
This commit is contained in:
Rafael Espindola 2014-09-16 15:18:21 +00:00
parent 65f1057191
commit 1e4df92f49
12 changed files with 211 additions and 63 deletions

View File

@ -156,6 +156,11 @@ public:
virtual void mangleItaniumThreadLocalWrapper(const VarDecl *D, virtual void mangleItaniumThreadLocalWrapper(const VarDecl *D,
raw_ostream &) = 0; raw_ostream &) = 0;
virtual void mangleCXXCtorComdat(const CXXConstructorDecl *D,
raw_ostream &) = 0;
virtual void mangleCXXDtorComdat(const CXXDestructorDecl *D,
raw_ostream &) = 0;
static bool classof(const MangleContext *C) { static bool classof(const MangleContext *C) {
return C->getKind() == MK_Itanium; return C->getKind() == MK_Itanium;
} }

View File

@ -24,14 +24,15 @@ namespace clang {
enum CXXCtorType { enum CXXCtorType {
Ctor_Complete, ///< Complete object ctor Ctor_Complete, ///< Complete object ctor
Ctor_Base, ///< Base object ctor Ctor_Base, ///< Base object ctor
Ctor_CompleteAllocating ///< Complete object allocating ctor Ctor_Comdat ///< The COMDAT used for ctors
}; };
/// \brief C++ destructor types. /// \brief C++ destructor types.
enum CXXDtorType { enum CXXDtorType {
Dtor_Deleting, ///< Deleting dtor Dtor_Deleting, ///< Deleting dtor
Dtor_Complete, ///< Complete object dtor Dtor_Complete, ///< Complete object dtor
Dtor_Base ///< Base object dtor Dtor_Base, ///< Base object dtor
Dtor_Comdat ///< The COMDAT used for dtors
}; };
/// \brief A return adjustment. /// \brief A return adjustment.

View File

@ -150,6 +150,8 @@ public:
void mangleCXXDtor(const CXXDestructorDecl *D, CXXDtorType Type, void mangleCXXDtor(const CXXDestructorDecl *D, CXXDtorType Type,
raw_ostream &) override; raw_ostream &) override;
void mangleCXXCtorComdat(const CXXConstructorDecl *D, raw_ostream &) override;
void mangleCXXDtorComdat(const CXXDestructorDecl *D, raw_ostream &) override;
void mangleStaticGuardVariable(const VarDecl *D, raw_ostream &) override; void mangleStaticGuardVariable(const VarDecl *D, raw_ostream &) override;
void mangleDynamicInitializer(const VarDecl *D, raw_ostream &Out) override; void mangleDynamicInitializer(const VarDecl *D, raw_ostream &Out) override;
void mangleDynamicAtExitDestructor(const VarDecl *D, void mangleDynamicAtExitDestructor(const VarDecl *D,
@ -3249,8 +3251,8 @@ void CXXNameMangler::mangleFunctionParam(const ParmVarDecl *parm) {
void CXXNameMangler::mangleCXXCtorType(CXXCtorType T) { void CXXNameMangler::mangleCXXCtorType(CXXCtorType T) {
// <ctor-dtor-name> ::= C1 # complete object constructor // <ctor-dtor-name> ::= C1 # complete object constructor
// ::= C2 # base object constructor // ::= C2 # base object constructor
// ::= C3 # complete object allocating constructor
// //
// In addition, C5 is a comdat name with C1 and C2 in it.
switch (T) { switch (T) {
case Ctor_Complete: case Ctor_Complete:
Out << "C1"; Out << "C1";
@ -3258,8 +3260,8 @@ void CXXNameMangler::mangleCXXCtorType(CXXCtorType T) {
case Ctor_Base: case Ctor_Base:
Out << "C2"; Out << "C2";
break; break;
case Ctor_CompleteAllocating: case Ctor_Comdat:
Out << "C3"; Out << "C5";
break; break;
} }
} }
@ -3269,6 +3271,7 @@ void CXXNameMangler::mangleCXXDtorType(CXXDtorType T) {
// ::= D1 # complete object destructor // ::= D1 # complete object destructor
// ::= D2 # base object destructor // ::= D2 # base object destructor
// //
// In addition, D5 is a comdat name with D1, D2 and, if virtual, D0 in it.
switch (T) { switch (T) {
case Dtor_Deleting: case Dtor_Deleting:
Out << "D0"; Out << "D0";
@ -3279,6 +3282,9 @@ void CXXNameMangler::mangleCXXDtorType(CXXDtorType T) {
case Dtor_Base: case Dtor_Base:
Out << "D2"; Out << "D2";
break; break;
case Dtor_Comdat:
Out << "D5";
break;
} }
} }
@ -3689,6 +3695,18 @@ void ItaniumMangleContextImpl::mangleCXXDtor(const CXXDestructorDecl *D,
Mangler.mangle(D); Mangler.mangle(D);
} }
void ItaniumMangleContextImpl::mangleCXXCtorComdat(const CXXConstructorDecl *D,
raw_ostream &Out) {
CXXNameMangler Mangler(*this, Out, D, Ctor_Comdat);
Mangler.mangle(D);
}
void ItaniumMangleContextImpl::mangleCXXDtorComdat(const CXXDestructorDecl *D,
raw_ostream &Out) {
CXXNameMangler Mangler(*this, Out, D, Dtor_Comdat);
Mangler.mangle(D);
}
void ItaniumMangleContextImpl::mangleThunk(const CXXMethodDecl *MD, void ItaniumMangleContextImpl::mangleThunk(const CXXMethodDecl *MD,
const ThunkInfo &Thunk, const ThunkInfo &Thunk,
raw_ostream &Out) { raw_ostream &Out) {

View File

@ -854,6 +854,8 @@ void MicrosoftCXXNameMangler::mangleCXXDtorType(CXXDtorType T) {
// <operator-name> ::= ?_E # vector deleting destructor // <operator-name> ::= ?_E # vector deleting destructor
// FIXME: Add a vector deleting dtor type. It goes in the vtable, so we need // FIXME: Add a vector deleting dtor type. It goes in the vtable, so we need
// it. // it.
case Dtor_Comdat:
llvm_unreachable("not expecting a COMDAT");
} }
llvm_unreachable("Unsupported dtor type?"); llvm_unreachable("Unsupported dtor type?");
} }

View File

@ -1292,6 +1292,9 @@ void CodeGenFunction::EmitDestructorBody(FunctionArgList &Args) {
// we'd introduce *two* handler blocks. In the Microsoft ABI, we // we'd introduce *two* handler blocks. In the Microsoft ABI, we
// always delegate because we might not have a definition in this TU. // always delegate because we might not have a definition in this TU.
switch (DtorType) { switch (DtorType) {
case Dtor_Comdat:
llvm_unreachable("not expecting a COMDAT");
case Dtor_Deleting: llvm_unreachable("already handled deleting case"); case Dtor_Deleting: llvm_unreachable("already handled deleting case");
case Dtor_Complete: case Dtor_Complete:

View File

@ -198,6 +198,10 @@ void CodeGenModule::createCUDARuntime() {
CUDARuntime = CreateNVCUDARuntime(*this); CUDARuntime = CreateNVCUDARuntime(*this);
} }
void CodeGenModule::addReplacement(StringRef Name, llvm::Constant *C) {
Replacements[Name] = C;
}
void CodeGenModule::applyReplacements() { void CodeGenModule::applyReplacements() {
for (ReplacementsTy::iterator I = Replacements.begin(), for (ReplacementsTy::iterator I = Replacements.begin(),
E = Replacements.end(); E = Replacements.end();

View File

@ -1058,9 +1058,17 @@ public:
void setFunctionDefinitionAttributes(const FunctionDecl *D, void setFunctionDefinitionAttributes(const FunctionDecl *D,
llvm::Function *F); llvm::Function *F);
private:
llvm::GlobalValue *GetGlobalValue(StringRef Ref); llvm::GlobalValue *GetGlobalValue(StringRef Ref);
/// Set attributes which are common to any form of a global definition (alias,
/// Objective-C method, function, global variable).
///
/// NOTE: This should only be called for definitions.
void SetCommonAttributes(const Decl *D, llvm::GlobalValue *GV);
void addReplacement(StringRef Name, llvm::Constant *C);
private:
llvm::Constant * llvm::Constant *
GetOrCreateLLVMFunction(StringRef MangledName, llvm::Type *Ty, GlobalDecl D, GetOrCreateLLVMFunction(StringRef MangledName, llvm::Type *Ty, GlobalDecl D,
bool ForVTable, bool DontDefer = false, bool ForVTable, bool DontDefer = false,
@ -1070,12 +1078,6 @@ private:
llvm::PointerType *PTy, llvm::PointerType *PTy,
const VarDecl *D); const VarDecl *D);
/// Set attributes which are common to any form of a global definition (alias,
/// Objective-C method, function, global variable).
///
/// NOTE: This should only be called for definitions.
void SetCommonAttributes(const Decl *D, llvm::GlobalValue *GV);
void setNonAliasAttributes(const Decl *D, llvm::GlobalObject *GO); void setNonAliasAttributes(const Decl *D, llvm::GlobalObject *GO);
/// Set function attributes for a function declaration. /// Set function attributes for a function declaration.

View File

@ -80,8 +80,8 @@ inline StructorType getFromCtorType(CXXCtorType T) {
return StructorType::Complete; return StructorType::Complete;
case Ctor_Base: case Ctor_Base:
return StructorType::Base; return StructorType::Base;
case Ctor_CompleteAllocating: case Ctor_Comdat:
llvm_unreachable("invalid enum"); llvm_unreachable("not expecting a COMDAT");
} }
llvm_unreachable("not a CXXCtorType"); llvm_unreachable("not a CXXCtorType");
} }
@ -106,6 +106,8 @@ inline StructorType getFromDtorType(CXXDtorType T) {
return StructorType::Complete; return StructorType::Complete;
case Dtor_Base: case Dtor_Base:
return StructorType::Base; return StructorType::Base;
case Dtor_Comdat:
llvm_unreachable("not expecting a COMDAT");
} }
llvm_unreachable("not a CXXDtorType"); llvm_unreachable("not a CXXDtorType");
} }

View File

@ -2999,35 +2999,102 @@ ItaniumCXXABI::RTTIUniquenessKind ItaniumCXXABI::classifyRTTIUniqueness(
return RUK_NonUniqueVisible; return RUK_NonUniqueVisible;
} }
static void emitCXXConstructor(CodeGenModule &CGM, // Find out how to codegen the complete destructor and constructor
const CXXConstructorDecl *ctor, namespace {
StructorType ctorType) { enum class StructorCodegen { Emit, RAUW, Alias, COMDAT };
if (!ctor->getParent()->getNumVBases() && }
(ctorType == StructorType::Complete || ctorType == StructorType::Base)) { static StructorCodegen getCodegenToUse(CodeGenModule &CGM,
// The complete constructor is equivalent to the base constructor const CXXMethodDecl *MD) {
// for classes with no virtual bases. Try to emit it as an alias. if (!CGM.getCodeGenOpts().CXXCtorDtorAliases)
bool ProducedAlias = !CGM.TryEmitDefinitionAsAlias( return StructorCodegen::Emit;
GlobalDecl(ctor, Ctor_Complete), GlobalDecl(ctor, Ctor_Base), true);
if (ctorType == StructorType::Complete && ProducedAlias)
return;
}
CGM.codegenCXXStructor(ctor, ctorType); // The complete and base structors are not equivalent if there are any virtual
// bases, so emit separate functions.
if (MD->getParent()->getNumVBases())
return StructorCodegen::Emit;
GlobalDecl AliasDecl;
if (const auto *DD = dyn_cast<CXXDestructorDecl>(MD)) {
AliasDecl = GlobalDecl(DD, Dtor_Complete);
} else {
const auto *CD = cast<CXXConstructorDecl>(MD);
AliasDecl = GlobalDecl(CD, Ctor_Complete);
}
llvm::GlobalValue::LinkageTypes Linkage = CGM.getFunctionLinkage(AliasDecl);
if (llvm::GlobalValue::isDiscardableIfUnused(Linkage))
return StructorCodegen::RAUW;
// FIXME: Should we allow available_externally aliases?
if (!llvm::GlobalAlias::isValidLinkage(Linkage))
return StructorCodegen::RAUW;
if (llvm::GlobalValue::isWeakForLinker(Linkage))
return StructorCodegen::COMDAT;
return StructorCodegen::Alias;
} }
static void emitCXXDestructor(CodeGenModule &CGM, const CXXDestructorDecl *dtor, static void emitConstructorDestructorAlias(CodeGenModule &CGM,
StructorType dtorType) { GlobalDecl AliasDecl,
// The complete destructor is equivalent to the base destructor for GlobalDecl TargetDecl) {
// classes with no virtual bases, so try to emit it as an alias. llvm::GlobalValue::LinkageTypes Linkage = CGM.getFunctionLinkage(AliasDecl);
if (!dtor->getParent()->getNumVBases() &&
(dtorType == StructorType::Complete || dtorType == StructorType::Base)) { StringRef MangledName = CGM.getMangledName(AliasDecl);
bool ProducedAlias = !CGM.TryEmitDefinitionAsAlias( llvm::GlobalValue *Entry = CGM.GetGlobalValue(MangledName);
GlobalDecl(dtor, Dtor_Complete), GlobalDecl(dtor, Dtor_Base), true); if (Entry && !Entry->isDeclaration())
if (ProducedAlias) { return;
if (dtorType == StructorType::Complete)
return; auto *Aliasee = cast<llvm::GlobalValue>(CGM.GetAddrOfGlobal(TargetDecl));
if (dtor->isVirtual()) llvm::PointerType *AliasType = Aliasee->getType();
CGM.getVTables().EmitThunks(GlobalDecl(dtor, Dtor_Complete));
// Create the alias with no name.
auto *Alias = llvm::GlobalAlias::create(
AliasType->getElementType(), 0, Linkage, "", Aliasee, &CGM.getModule());
// Switch any previous uses to the alias.
if (Entry) {
assert(Entry->getType() == AliasType &&
"declaration exists with different type");
Alias->takeName(Entry);
Entry->replaceAllUsesWith(Alias);
Entry->eraseFromParent();
} else {
Alias->setName(MangledName);
}
// Finally, set up the alias with its proper name and attributes.
CGM.SetCommonAttributes(cast<NamedDecl>(AliasDecl.getDecl()), Alias);
}
void ItaniumCXXABI::emitCXXStructor(const CXXMethodDecl *MD,
StructorType Type) {
auto *CD = dyn_cast<CXXConstructorDecl>(MD);
const CXXDestructorDecl *DD = CD ? nullptr : cast<CXXDestructorDecl>(MD);
StructorCodegen CGType = getCodegenToUse(CGM, MD);
if (Type == StructorType::Complete) {
GlobalDecl CompleteDecl;
GlobalDecl BaseDecl;
if (CD) {
CompleteDecl = GlobalDecl(CD, Ctor_Complete);
BaseDecl = GlobalDecl(CD, Ctor_Base);
} else {
CompleteDecl = GlobalDecl(DD, Dtor_Complete);
BaseDecl = GlobalDecl(DD, Dtor_Base);
}
if (CGType == StructorCodegen::Alias || CGType == StructorCodegen::COMDAT) {
emitConstructorDestructorAlias(CGM, CompleteDecl, BaseDecl);
return;
}
if (CGType == StructorCodegen::RAUW) {
StringRef MangledName = CGM.getMangledName(CompleteDecl);
auto *Aliasee = cast<llvm::GlobalValue>(CGM.GetAddrOfGlobal(BaseDecl));
CGM.addReplacement(MangledName, Aliasee);
return;
} }
} }
@ -3035,17 +3102,20 @@ static void emitCXXDestructor(CodeGenModule &CGM, const CXXDestructorDecl *dtor,
// base class if there is exactly one non-virtual base class with a // base class if there is exactly one non-virtual base class with a
// non-trivial destructor, there are no fields with a non-trivial // non-trivial destructor, there are no fields with a non-trivial
// destructor, and the body of the destructor is trivial. // destructor, and the body of the destructor is trivial.
if (dtorType == StructorType::Base && !CGM.TryEmitBaseDestructorAsAlias(dtor)) if (DD && Type == StructorType::Base && CGType != StructorCodegen::COMDAT &&
!CGM.TryEmitBaseDestructorAsAlias(DD))
return; return;
CGM.codegenCXXStructor(dtor, dtorType); llvm::Function *Fn = CGM.codegenCXXStructor(MD, Type);
}
void ItaniumCXXABI::emitCXXStructor(const CXXMethodDecl *MD, if (CGType == StructorCodegen::COMDAT) {
StructorType Type) { SmallString<256> Buffer;
if (auto *CD = dyn_cast<CXXConstructorDecl>(MD)) { llvm::raw_svector_ostream Out(Buffer);
emitCXXConstructor(CGM, CD, Type); if (DD)
return; getMangleContext().mangleCXXDtorComdat(DD, Out);
else
getMangleContext().mangleCXXCtorComdat(CD, Out);
llvm::Comdat *C = CGM.getModule().getOrInsertComdat(Out.str());
Fn->setComdat(C);
} }
emitCXXDestructor(CGM, cast<CXXDestructorDecl>(MD), Type);
} }

View File

@ -6,18 +6,23 @@
// RUN: FileCheck --check-prefix=CHECK3 --input-file=%t %s // RUN: FileCheck --check-prefix=CHECK3 --input-file=%t %s
// RUN: FileCheck --check-prefix=CHECK4 --input-file=%t %s // RUN: FileCheck --check-prefix=CHECK4 --input-file=%t %s
// RUN: FileCheck --check-prefix=CHECK5 --input-file=%t %s // RUN: FileCheck --check-prefix=CHECK5 --input-file=%t %s
// RUN: FileCheck --check-prefix=CHECK6 --input-file=%t %s
namespace test1 { namespace test1 {
// test that we don't produce an alias when the destructor is weak_odr. The // Test that we produce the apropriate comdats when creating aliases to
// reason to avoid it that another TU might have no explicit template // weak_odr constructors and destructors.
// instantiation definition or declaration, causing it to to output only
// one of the destructors as linkonce_odr, producing a different comdat.
// CHECK1: define weak_odr void @_ZN5test16foobarIvEC2Ev // CHECK1: @_ZN5test16foobarIvEC1Ev = weak_odr alias void {{.*}} @_ZN5test16foobarIvEC2Ev
// CHECK1: define weak_odr void @_ZN5test16foobarIvEC1Ev // CHECK1: @_ZN5test16foobarIvED1Ev = weak_odr alias void (%"struct.test1::foobar"*)* @_ZN5test16foobarIvED2Ev
// CHECK1: define weak_odr void @_ZN5test16foobarIvEC2Ev({{.*}} comdat $_ZN5test16foobarIvEC5Ev
// CHECK1: define weak_odr void @_ZN5test16foobarIvED2Ev({{.*}} comdat $_ZN5test16foobarIvED5Ev
// CHECK1: define weak_odr void @_ZN5test16foobarIvED0Ev({{.*}} comdat $_ZN5test16foobarIvED5Ev
// CHECK1-NOT: comdat
template <typename T> struct foobar { template <typename T>
struct foobar {
foobar() {} foobar() {}
virtual ~foobar() {}
}; };
template struct foobar<void>; template struct foobar<void>;
@ -187,3 +192,38 @@ void
fn1() { fn1() {
new C; new C;
} }
namespace test10 {
// Test that if a destructor is in a comdat, we don't try to emit is as an
// alias to a base class destructor.
struct bar {
~bar();
};
bar::~bar() {
}
} // closing the namespace causes ~bar to be sent to CodeGen
namespace test10 {
template <typename T>
struct foo : public bar {
~foo();
};
template <typename T>
foo<T>::~foo() {}
template class foo<int>;
// CHECK5: define weak_odr void @_ZN6test103fooIiED2Ev({{.*}} comdat $_ZN6test103fooIiED5Ev
}
namespace test11 {
// Test that when we don't have to worry about COMDATs we produce an alias
// from complate to base and from base to base class base.
struct bar {
~bar();
};
bar::~bar() {}
struct foo : public bar {
~foo();
};
foo::~foo() {}
// CHECK6: @_ZN6test113fooD2Ev = alias {{.*}} @_ZN6test113barD2Ev
// CHECK6: @_ZN6test113fooD1Ev = alias {{.*}} @_ZN6test113fooD2Ev
}

View File

@ -204,18 +204,14 @@ namespace test3 {
// CHECK4: call void @_ZN5test312_GLOBAL__N_11DD0Ev( // CHECK4: call void @_ZN5test312_GLOBAL__N_11DD0Ev(
// CHECK4: ret void // CHECK4: ret void
// CHECK4-LABEL: define internal void @_ZThn8_N5test312_GLOBAL__N_11CD1Ev( // CHECK4-LABEL: declare void @_ZN5test31BD2Ev(
// CHECK4: getelementptr inbounds i8* {{.*}}, i64 -8 // CHECK4-LABEL: declare void @_ZN5test31AD2Ev(
// CHECK4: call void @_ZN5test312_GLOBAL__N_11CD2Ev(
// CHECK4: ret void
// CHECK4-LABEL: define internal void @_ZN5test312_GLOBAL__N_11CD2Ev(%"struct.test3::(anonymous namespace)::C"* %this) unnamed_addr // CHECK4-LABEL: define internal void @_ZN5test312_GLOBAL__N_11CD2Ev(%"struct.test3::(anonymous namespace)::C"* %this) unnamed_addr
// CHECK4: invoke void @_ZN5test31BD2Ev( // CHECK4: invoke void @_ZN5test31BD2Ev(
// CHECK4: call void @_ZN5test31AD2Ev( // CHECK4: call void @_ZN5test31AD2Ev(
// CHECK4: ret void // CHECK4: ret void
// CHECK4: declare void @_ZN5test31BD2Ev(
// CHECK4: declare void @_ZN5test31AD2Ev(
// CHECK4-LABEL: define internal void @_ZN5test312_GLOBAL__N_11CD0Ev(%"struct.test3::(anonymous namespace)::C"* %this) unnamed_addr // CHECK4-LABEL: define internal void @_ZN5test312_GLOBAL__N_11CD0Ev(%"struct.test3::(anonymous namespace)::C"* %this) unnamed_addr
// CHECK4: invoke void @_ZN5test312_GLOBAL__N_11CD2Ev( // CHECK4: invoke void @_ZN5test312_GLOBAL__N_11CD2Ev(
@ -226,6 +222,11 @@ namespace test3 {
// CHECK4: call void @_ZdlPv({{.*}}) [[NUW]] // CHECK4: call void @_ZdlPv({{.*}}) [[NUW]]
// CHECK4: resume { i8*, i32 } // CHECK4: resume { i8*, i32 }
// CHECK4-LABEL: define internal void @_ZThn8_N5test312_GLOBAL__N_11CD1Ev(
// CHECK4: getelementptr inbounds i8* {{.*}}, i64 -8
// CHECK4: call void @_ZN5test312_GLOBAL__N_11CD2Ev(
// CHECK4: ret void
// CHECK4-LABEL: define internal void @_ZThn8_N5test312_GLOBAL__N_11CD0Ev( // CHECK4-LABEL: define internal void @_ZThn8_N5test312_GLOBAL__N_11CD0Ev(
// CHECK4: getelementptr inbounds i8* {{.*}}, i64 -8 // CHECK4: getelementptr inbounds i8* {{.*}}, i64 -8
// CHECK4: call void @_ZN5test312_GLOBAL__N_11CD0Ev( // CHECK4: call void @_ZN5test312_GLOBAL__N_11CD0Ev(

View File

@ -17,8 +17,8 @@ struct B : A {
// CHECK: @_ZN1BD1Ev = alias {{.*}} @_ZN1BD2Ev // CHECK: @_ZN1BD1Ev = alias {{.*}} @_ZN1BD2Ev
// (aliases from C) // (aliases from C)
// CHECK: @_ZN1CD1Ev = alias {{.*}} @_ZN1CD2Ev
// CHECK: @_ZN1CD2Ev = alias bitcast {{.*}} @_ZN1BD2Ev // CHECK: @_ZN1CD2Ev = alias bitcast {{.*}} @_ZN1BD2Ev
// CHECK: @_ZN1CD1Ev = alias {{.*}} @_ZN1CD2Ev
// Base dtor: actually calls A's base dtor. // Base dtor: actually calls A's base dtor.
// CHECK-LABEL: define void @_ZN1BD2Ev(%struct.B* %this) unnamed_addr // CHECK-LABEL: define void @_ZN1BD2Ev(%struct.B* %this) unnamed_addr