Skip to content

[RFC] [clang] [CodeGen] Avoid creating global variable repeatedly when type are not specified #114948

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

ChuanqiXu9
Copy link
Member

This comes from an internal crash. I know generally it is better to reproduce it first but I do feel the pattern is pretty risky. So I am wondering if we can discuss it first. So maybe this is more of a discussion instead of a pure PR.

Then story is, when we try to get or create a LLVM global for a C/C++'s global, we will try to look up the name first for the existing globals. And if we find one, we will perform some checks. If the checks pass, we will return the found one. If not, we will create a new one and replace the previous one. (Why do we want to do this? My instinct reaction is that we should abort here):

llvm::GlobalValue *Entry = GetGlobalValue(MangledName);
unsigned TargetAS = getContext().getTargetAddressSpace(AddrSpace);
if (Entry) {
if (WeakRefReferences.erase(Entry)) {
if (D && !D->hasAttr<WeakAttr>())
Entry->setLinkage(llvm::Function::ExternalLinkage);
}
// Handle dropped DLL attributes.
if (D && shouldDropDLLAttribute(D, Entry))
Entry->setDLLStorageClass(llvm::GlobalValue::DefaultStorageClass);
if (LangOpts.OpenMP && !LangOpts.OpenMPSimd && D)
getOpenMPRuntime().registerTargetGlobalVariable(D, Entry);
if (Entry->getValueType() == Ty && Entry->getAddressSpace() == TargetAS)
return Entry;

auto *GV = new llvm::GlobalVariable(
getModule(), Ty, false, llvm::GlobalValue::ExternalLinkage, nullptr,
MangledName, nullptr, llvm::GlobalVariable::NotThreadLocal,
getContext().getTargetAddressSpace(DAddrSpace));
// If we already created a global with the same mangled name (but different
// type) before, take its name and remove it from its parent.
if (Entry) {
GV->takeName(Entry);
if (!Entry->use_empty()) {
Entry->replaceAllUsesWith(GV);
}
Entry->eraseFromParent();
}

The problem is, if we store the address of a global variable and the global variable got replaced later, the address we stored became a wild pointer! e.g.

void CodeGenModule::AddGlobalCtor(llvm::Function *Ctor, int Priority,
unsigned LexOrder,
llvm::Constant *AssociatedData) {
// FIXME: Type coercion of void()* types.
GlobalCtors.push_back(Structor(Priority, LexOrder, Ctor, AssociatedData));
}

I feel this is pretty dangerous. And to my knowledge, I think we'd better to not remove things emitted during CodeGen.

Then, one of the trigger for the problem is CodeGenModule::GetAddrOfGlobalVar:

https://github.com/llvm/llvm-project/blob/283273fa1e3be4a03f06a5efd08a8c818be981fd/clang/lib/CodeGen/CodeGenModule.cpp#L5232C17-L5243

The arguments except D can be omitted. And if we don't specify Ty, the function will try to deduce the type from D. And use the type to get or create a LLVM global in the above process. And the Ty arguments may not always be omitted, e.g., in

const VarDecl *InitDecl;
const Expr *InitExpr = D->getAnyInitializer(InitDecl);
std::optional<ConstantEmitter> emitter;
// CUDA E.2.4.1 "__shared__ variables cannot have an initialization
// as part of their declaration." Sema has already checked for
// error cases, so we just need to set Init to UndefValue.
bool IsCUDASharedVar =
getLangOpts().CUDAIsDevice && D->hasAttr<CUDASharedAttr>();
// Shadows of initialized device-side global variables are also left
// undefined.
// Managed Variables should be initialized on both host side and device side.
bool IsCUDAShadowVar =
!getLangOpts().CUDAIsDevice && !D->hasAttr<HIPManagedAttr>() &&
(D->hasAttr<CUDAConstantAttr>() || D->hasAttr<CUDADeviceAttr>() ||
D->hasAttr<CUDASharedAttr>());
bool IsCUDADeviceShadowVar =
getLangOpts().CUDAIsDevice && !D->hasAttr<HIPManagedAttr>() &&
(D->getType()->isCUDADeviceBuiltinSurfaceType() ||
D->getType()->isCUDADeviceBuiltinTextureType());
if (getLangOpts().CUDA &&
(IsCUDASharedVar || IsCUDAShadowVar || IsCUDADeviceShadowVar))
Init = llvm::UndefValue::get(getTypes().ConvertTypeForMem(ASTTy));
else if (D->hasAttr<LoaderUninitializedAttr>())
Init = llvm::UndefValue::get(getTypes().ConvertTypeForMem(ASTTy));
else if (!InitExpr) {
// This is a tentative definition; tentative definitions are
// implicitly initialized with { 0 }.
//
// Note that tentative definitions are only emitted at the end of
// a translation unit, so they should never have incomplete
// type. In addition, EmitTentativeDefinition makes sure that we
// never attempt to emit a tentative definition if a real one
// exists. A use may still exists, however, so we still may need
// to do a RAUW.
assert(!ASTTy->isIncompleteType() && "Unexpected incomplete type");
Init = EmitNullConstant(D->getType());
} else {
initializedGlobalDecl = GlobalDecl(D);
emitter.emplace(*this);
llvm::Constant *Initializer = emitter->tryEmitForInitializer(*InitDecl);
if (!Initializer) {
QualType T = InitExpr->getType();
if (D->getType()->isReferenceType())
T = D->getType();
if (getLangOpts().CPlusPlus) {
Init = EmitNullConstant(T);
if (!IsDefinitionAvailableExternally)
NeedsGlobalCtor = true;
if (InitDecl->hasFlexibleArrayInit(getContext())) {
ErrorUnsupported(D, "flexible array initializer");
// We cannot create ctor for flexible array initializer
NeedsGlobalCtor = false;
}
} else {
ErrorUnsupported(D, "static initializer");
Init = llvm::UndefValue::get(getTypes().ConvertType(T));
}
} else {
Init = Initializer;
// We don't need an initializer, so remove the entry for the delayed
// initializer position (just in case this entry was delayed) if we
// also don't need to register a destructor.
if (getLangOpts().CPlusPlus && !NeedsGlobalDtor)
DelayedCXXInitPosition.erase(D);
#ifndef NDEBUG
CharUnits VarSize = getContext().getTypeSizeInChars(ASTTy) +
InitDecl->getFlexibleArrayInitChars(getContext());
CharUnits CstSize = CharUnits::fromQuantity(
getDataLayout().getTypeAllocSize(Init->getType()));
assert(VarSize == CstSize && "Emitted constant has unexpected size");
#endif
}
}
llvm::Type* InitType = Init->getType();
llvm::Constant *Entry =
GetAddrOfGlobalVar(D, InitType, ForDefinition_t(!IsTentative));
, we will try to deduce the LLVM type directly.

Then problem happens, sometimes we try to get or create the global variable by the AST type, but sometimes we try to get or create the same global variable by deduced type, and if the two types differs, we may be in the trouble of wild pointer.

(the two types are compatible: e.g., one is struct { %another.struct} with %another.struct = { ptr } and another type is { { ptr } }).

The solution or one workaround I got is, in CodeGenModule::GetAddrOfGlobalVar, if we didn't specify the Ty and we have the same variable, return the variable directly. I think it makes sense since if the Ty is not specified, it implies the caller doesn't care about it too much.

WDYT?

@ChuanqiXu9 ChuanqiXu9 added the clang:codegen IR generation bugs: mangling, exceptions, etc. label Nov 5, 2024
@ChuanqiXu9 ChuanqiXu9 self-assigned this Nov 5, 2024
@llvmbot llvmbot added the clang Clang issues not falling into any other category label Nov 5, 2024
@llvmbot
Copy link
Member

llvmbot commented Nov 5, 2024

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-codegen

Author: Chuanqi Xu (ChuanqiXu9)

Changes

This comes from an internal crash. I know generally it is better to reproduce it first but I do feel the pattern is pretty risky. So I am wondering if we can discuss it first. So maybe this is more of a discussion instead of a pure PR.

Then story is, when we try to get or create a LLVM global for a C/C++'s global, we will try to look up the name first for the existing globals. And if we find one, we will perform some checks. If the checks pass, we will return the found one. If not, we will create a new one and replace the previous one. (Why do we want to do this? My instinct reaction is that we should abort here):

llvm::GlobalValue *Entry = GetGlobalValue(MangledName);
unsigned TargetAS = getContext().getTargetAddressSpace(AddrSpace);
if (Entry) {
if (WeakRefReferences.erase(Entry)) {
if (D && !D->hasAttr<WeakAttr>())
Entry->setLinkage(llvm::Function::ExternalLinkage);
}
// Handle dropped DLL attributes.
if (D && shouldDropDLLAttribute(D, Entry))
Entry->setDLLStorageClass(llvm::GlobalValue::DefaultStorageClass);
if (LangOpts.OpenMP && !LangOpts.OpenMPSimd && D)
getOpenMPRuntime().registerTargetGlobalVariable(D, Entry);
if (Entry->getValueType() == Ty && Entry->getAddressSpace() == TargetAS)
return Entry;

auto *GV = new llvm::GlobalVariable(
getModule(), Ty, false, llvm::GlobalValue::ExternalLinkage, nullptr,
MangledName, nullptr, llvm::GlobalVariable::NotThreadLocal,
getContext().getTargetAddressSpace(DAddrSpace));
// If we already created a global with the same mangled name (but different
// type) before, take its name and remove it from its parent.
if (Entry) {
GV->takeName(Entry);
if (!Entry->use_empty()) {
Entry->replaceAllUsesWith(GV);
}
Entry->eraseFromParent();
}

The problem is, if we store the address of a global variable and the global variable got replaced later, the address we stored became a wild pointer! e.g.

void CodeGenModule::AddGlobalCtor(llvm::Function *Ctor, int Priority,
unsigned LexOrder,
llvm::Constant *AssociatedData) {
// FIXME: Type coercion of void()* types.
GlobalCtors.push_back(Structor(Priority, LexOrder, Ctor, AssociatedData));
}

I feel this is pretty dangerous. And to my knowledge, I think we'd better to not remove things emitted during CodeGen.

Then, one of the trigger for the problem is CodeGenModule::GetAddrOfGlobalVar:

https://github.com/llvm/llvm-project/blob/283273fa1e3be4a03f06a5efd08a8c818be981fd/clang/lib/CodeGen/CodeGenModule.cpp#L5232C17-L5243

The arguments except D can be omitted. And if we don't specify Ty, the function will try to deduce the type from D. And use the type to get or create a LLVM global in the above process. And the Ty arguments may not always be omitted, e.g., in

const VarDecl *InitDecl;
const Expr *InitExpr = D->getAnyInitializer(InitDecl);
std::optional<ConstantEmitter> emitter;
// CUDA E.2.4.1 "__shared__ variables cannot have an initialization
// as part of their declaration." Sema has already checked for
// error cases, so we just need to set Init to UndefValue.
bool IsCUDASharedVar =
getLangOpts().CUDAIsDevice && D->hasAttr<CUDASharedAttr>();
// Shadows of initialized device-side global variables are also left
// undefined.
// Managed Variables should be initialized on both host side and device side.
bool IsCUDAShadowVar =
!getLangOpts().CUDAIsDevice && !D->hasAttr<HIPManagedAttr>() &&
(D->hasAttr<CUDAConstantAttr>() || D->hasAttr<CUDADeviceAttr>() ||
D->hasAttr<CUDASharedAttr>());
bool IsCUDADeviceShadowVar =
getLangOpts().CUDAIsDevice && !D->hasAttr<HIPManagedAttr>() &&
(D->getType()->isCUDADeviceBuiltinSurfaceType() ||
D->getType()->isCUDADeviceBuiltinTextureType());
if (getLangOpts().CUDA &&
(IsCUDASharedVar || IsCUDAShadowVar || IsCUDADeviceShadowVar))
Init = llvm::UndefValue::get(getTypes().ConvertTypeForMem(ASTTy));
else if (D->hasAttr<LoaderUninitializedAttr>())
Init = llvm::UndefValue::get(getTypes().ConvertTypeForMem(ASTTy));
else if (!InitExpr) {
// This is a tentative definition; tentative definitions are
// implicitly initialized with { 0 }.
//
// Note that tentative definitions are only emitted at the end of
// a translation unit, so they should never have incomplete
// type. In addition, EmitTentativeDefinition makes sure that we
// never attempt to emit a tentative definition if a real one
// exists. A use may still exists, however, so we still may need
// to do a RAUW.
assert(!ASTTy->isIncompleteType() && "Unexpected incomplete type");
Init = EmitNullConstant(D->getType());
} else {
initializedGlobalDecl = GlobalDecl(D);
emitter.emplace(*this);
llvm::Constant *Initializer = emitter->tryEmitForInitializer(*InitDecl);
if (!Initializer) {
QualType T = InitExpr->getType();
if (D->getType()->isReferenceType())
T = D->getType();
if (getLangOpts().CPlusPlus) {
Init = EmitNullConstant(T);
if (!IsDefinitionAvailableExternally)
NeedsGlobalCtor = true;
if (InitDecl->hasFlexibleArrayInit(getContext())) {
ErrorUnsupported(D, "flexible array initializer");
// We cannot create ctor for flexible array initializer
NeedsGlobalCtor = false;
}
} else {
ErrorUnsupported(D, "static initializer");
Init = llvm::UndefValue::get(getTypes().ConvertType(T));
}
} else {
Init = Initializer;
// We don't need an initializer, so remove the entry for the delayed
// initializer position (just in case this entry was delayed) if we
// also don't need to register a destructor.
if (getLangOpts().CPlusPlus && !NeedsGlobalDtor)
DelayedCXXInitPosition.erase(D);
#ifndef NDEBUG
CharUnits VarSize = getContext().getTypeSizeInChars(ASTTy) +
InitDecl->getFlexibleArrayInitChars(getContext());
CharUnits CstSize = CharUnits::fromQuantity(
getDataLayout().getTypeAllocSize(Init->getType()));
assert(VarSize == CstSize && "Emitted constant has unexpected size");
#endif
}
}
llvm::Type* InitType = Init->getType();
llvm::Constant *Entry =
GetAddrOfGlobalVar(D, InitType, ForDefinition_t(!IsTentative));
, we will try to deduce the LLVM type directly.

Then problem happens, sometimes we try to get or create the global variable by the AST type, but sometimes we try to get or create the same global variable by deduced type, and if the two types differs, we may be in the trouble of wild pointer.

(the two types are compatible: e.g., one is struct { %another.struct} with %another.struct = { ptr } and another type is { { ptr } }).

The solution or one workaround I got is, in CodeGenModule::GetAddrOfGlobalVar, if we didn't specify the Ty and we have the same variable, return the variable directly. I think it makes sense since if the Ty is not specified, it implies the caller doesn't care about it too much.

WDYT?


Full diff: https://github.com/llvm/llvm-project/pull/114948.diff

2 Files Affected:

  • (modified) clang/lib/CodeGen/CodeGenModule.cpp (+8-1)
  • (modified) clang/test/CodeGen/attr-weakref2.c (+1-1)
diff --git a/clang/lib/CodeGen/CodeGenModule.cpp b/clang/lib/CodeGen/CodeGenModule.cpp
index ba376f9ecfacde..9566cfb8d6e794 100644
--- a/clang/lib/CodeGen/CodeGenModule.cpp
+++ b/clang/lib/CodeGen/CodeGenModule.cpp
@@ -5233,11 +5233,18 @@ llvm::Constant *CodeGenModule::GetAddrOfGlobalVar(const VarDecl *D,
                                                   llvm::Type *Ty,
                                            ForDefinition_t IsForDefinition) {
   assert(D->hasGlobalStorage() && "Not a global variable");
+
+  StringRef MangledName = getMangledName(D);
+  llvm::GlobalValue *Entry = GetGlobalValue(MangledName);
   QualType ASTTy = D->getType();
+  LangAS AddrSpace = ASTTy.getAddressSpace();
+
+  if (Entry && !Ty && Entry->getAddressSpace() == getContext().getTargetAddressSpace(AddrSpace))
+    return Entry;
+
   if (!Ty)
     Ty = getTypes().ConvertTypeForMem(ASTTy);
 
-  StringRef MangledName = getMangledName(D);
   return GetOrCreateLLVMGlobal(MangledName, Ty, ASTTy.getAddressSpace(), D,
                                IsForDefinition);
 }
diff --git a/clang/test/CodeGen/attr-weakref2.c b/clang/test/CodeGen/attr-weakref2.c
index 114f048a851832..a67f906810faf3 100644
--- a/clang/test/CodeGen/attr-weakref2.c
+++ b/clang/test/CodeGen/attr-weakref2.c
@@ -33,7 +33,7 @@ int test4_h(void) {
 }
 int test4_f;
 
-// CHECK: @test5_f = external global i32
+// CHECK: @test5_f = extern_weak global i32
 extern int test5_f;
 static int test5_g __attribute__((weakref("test5_f")));
 int test5_h(void) {

Copy link

github-actions bot commented Nov 5, 2024

⚠️ C/C++ code formatter, clang-format found issues in your code. ⚠️

You can test this locally with the following command:
git-clang-format --diff 70de0b8bea31bb734bce86581574a60a0968d838 5b7def2c1deb4315cd043bc090a7364edbaeb84c --extensions c,cpp -- clang/lib/CodeGen/CodeGenModule.cpp clang/test/CodeGen/attr-weakref2.c
View the diff from clang-format here.
diff --git a/clang/lib/CodeGen/CodeGenModule.cpp b/clang/lib/CodeGen/CodeGenModule.cpp
index 9566cfb8d6..75c1eb8bfa 100644
--- a/clang/lib/CodeGen/CodeGenModule.cpp
+++ b/clang/lib/CodeGen/CodeGenModule.cpp
@@ -5239,7 +5239,8 @@ llvm::Constant *CodeGenModule::GetAddrOfGlobalVar(const VarDecl *D,
   QualType ASTTy = D->getType();
   LangAS AddrSpace = ASTTy.getAddressSpace();
 
-  if (Entry && !Ty && Entry->getAddressSpace() == getContext().getTargetAddressSpace(AddrSpace))
+  if (Entry && !Ty &&
+      Entry->getAddressSpace() == getContext().getTargetAddressSpace(AddrSpace))
     return Entry;
 
   if (!Ty)

@rjmccall
Copy link
Contributor

rjmccall commented Nov 5, 2024

Two things are at play here.

The first is that it is possible in various ways to instruct CodeGen to try to use or define the same symbol with wildly different types. Users generally expect these things to "just work" by making them resolve to the same entity. Sema used to put CodeGen into this situation all the time with incompatible local extern declarations in C; the checking there has gotten stricter, but I believe in some cases it is still only enforced with a warning. The GNU asm label extension is the bigger problem here, as a declaration with an asm label can end up colliding with anything else in the module; we can even end up with variable vs. function conflicts.

The second is that you used to not be able to change the type of an LLVM global variable. I'm not sure if this is still true; I know that the pointer type changes added some flexibility here, but I don't know if it got us all the way to what CodeGen needs. Regardless, even if it is no longer true, for the extended period that it was true, CodeGen had no choice but to replace the existing global in order to change its type. It's possible that this can be simplified now.

CodeGen needs to be able to change the IR type of a global variable for two reasons:

  • First, C allows a global variable to be used while its type is still incomplete. This means CodeGen must generate a declaration with a placeholder type, then potentially fill that in later when the definition is processed. This can happen both with incomplete struct and union types (extern struct foo x;) and with array types of unspecified bound (extern int y[];).
  • Second, the type of the initializer for a global variable in LLVM IR does not always match the IR type that we would abstractly prefer to use for its C type. Typically this happens with unions, which the LLVM type system cannot directly express. For example, when lowering the type struct { union { double d; char c; } u; }, CodeGen might turn this into struct { double } in LLVM IR, but a global variable that actually initializes u.c will have an initializer with a different type.

@nikic
Copy link
Contributor

nikic commented Nov 5, 2024

See also #102553 which stopped doing the global replacement for changes to the initializer type.

I think it's reasonable to do something similar here, but I believe the change for that should be inside GetOrCreateLLVMGlobal, not in GetAddrOfGlobalVar.

I think the main remaining limitation in this area is that we can't change global AS in-place, as that is part of the pointer type.

@ChuanqiXu9
Copy link
Member Author

ChuanqiXu9 commented Nov 5, 2024

Thanks for the quick reply!

If we want to change the type of a global variable, maybe we can use

/// Mutate the type of this Value to be of the specified type.
///
/// Note that this is an extremely dangerous operation which can create
/// completely invalid IR very easily. It is strongly recommended that you
/// recreate IR objects with the right types instead of mutating them in
/// place.
void mutateType(Type *Ty) {
VTy = Ty;
}
?

I hesitated since its comment say it is dangerous. But @rjmccall 's comments say it is more or less "just works" now. And I feel the wild pointers are dangerous too..

I think the main remaining limitation in this area is that we can't change global AS in-place, as that is part of the pointer type.

How about only do this only if the AS are the same? e.g.:

    if (Entry->getAddressSpace() == TargetAS) {
      if (Entry->getValueType() == Ty)
        return Entry;

      Entry->mutateType(Ty);
      return Entry;
    }

@rjmccall
Copy link
Contributor

rjmccall commented Nov 5, 2024

I don't think there's any situation in which Clang needs to change the address space of a declaration. It can happen if the programmer has declarations that disagree about the address space in which the entity is defined, but it's fair to just emit an error in that situation.

@efriedma-quic
Copy link
Collaborator

If we want to change the type of a global variable, maybe we can use Value::mutateType

Trying to mutate the type of a global is still unsafe. The benefit of opaque pointers here is that getValueType() is independent from getType(), so it's safe to rewrite the ValueType. (This is what GlobalVariable::replaceInitializer() does.)

I don't think there's any situation in which Clang needs to change the address space of a declaration.

I wouldn't be surprised if there's some GPU stuff that relies on this, maybe by accident.

@ChuanqiXu9
Copy link
Member Author

I think it's reasonable to do something similar here, but I believe the change for that should be inside GetOrCreateLLVMGlobal, not in GetAddrOfGlobalVar.

The problem to make it in GetOrCreateLLVMGlobal is that the argument of Ty is not skippable. So the semantics is slightly odd. Currently I feel it makes more or less sense that we can do some decisions if the user of GetAddrOfGlobalVar don't specify a Type, which implies the user doesn't care about the type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:codegen IR generation bugs: mangling, exceptions, etc. clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants