Skip to content

[Attributor] Teach HeapToStack about conservative GC allocators #113299

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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions llvm/docs/LangRef.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1887,6 +1887,8 @@ example:
zeroed.
* "aligned": the function returns memory aligned according to the
``allocalign`` parameter.
* "nofree": the block of memory is not explicitly freed (but might be freed
by a conservative GC when unreferenced).

The first three options are mutually exclusive, and the remaining options
describe more details of how the function behaves. The remaining options
Expand Down
5 changes: 5 additions & 0 deletions llvm/include/llvm/Analysis/MemoryBuiltins.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ Value *getFreedOperand(const CallBase *CB, const TargetLibraryInfo *TLI);
/// insertion or speculative execution of allocation routines.
bool isRemovableAlloc(const CallBase *V, const TargetLibraryInfo *TLI);

// Whether this is a function that allocates memory that will never be
// explicitly freed. The memory might be freed in the background by a GC when
// unreferenced.
bool isNoFreeAllocFunction(const CallBase *CB);

/// Gets the alignment argument for an aligned_alloc-like function, using either
/// built-in knowledge based on fuction names/signatures or allocalign
/// attributes. Note: the Value returned may not indicate a valid alignment, per
Expand Down
2 changes: 2 additions & 0 deletions llvm/include/llvm/IR/Attributes.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ enum class AllocFnKind : uint64_t {
Zeroed = 1 << 4, // Allocator function returns zeroed memory
Aligned = 1 << 5, // Allocator function aligns allocations per the
// `allocalign` argument
NoFree = 1 << 6, // Allocator function returns memory that's never
// freed
LLVM_MARK_AS_BITMASK_ENUM(/* LargestValue = */ Aligned)
};

Expand Down
4 changes: 4 additions & 0 deletions llvm/lib/Analysis/MemoryBuiltins.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,10 @@ bool llvm::isRemovableAlloc(const CallBase *CB, const TargetLibraryInfo *TLI) {
return isAllocLikeFn(CB, TLI);
}

bool llvm::isNoFreeAllocFunction(const CallBase *CB) {
return checkFnAllocKind(CB, AllocFnKind::NoFree);
}

Value *llvm::getAllocAlignment(const CallBase *V,
const TargetLibraryInfo *TLI) {
const std::optional<AllocFnsTy> FnData = getAllocationData(V, AnyAlloc, TLI);
Expand Down
2 changes: 2 additions & 0 deletions llvm/lib/AsmParser/LLParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2470,6 +2470,8 @@ bool LLParser::parseAllocKind(AllocFnKind &Kind) {
Kind |= AllocFnKind::Zeroed;
} else if (A == "aligned") {
Kind |= AllocFnKind::Aligned;
} else if (A == "nofree") {
Kind |= AllocFnKind::NoFree;
} else {
return error(KindLoc, Twine("unknown allockind ") + A);
}
Expand Down
2 changes: 2 additions & 0 deletions llvm/lib/IR/Attributes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,8 @@ std::string Attribute::getAsString(bool InAttrGrp) const {
parts.push_back("zeroed");
if ((Kind & AllocFnKind::Aligned) != AllocFnKind::Unknown)
parts.push_back("aligned");
if ((Kind & AllocFnKind::NoFree) != AllocFnKind::Unknown)
parts.push_back("nofree");
return ("allockind(\"" +
Twine(llvm::join(parts.begin(), parts.end(), ",")) + "\")")
.str();
Expand Down
3 changes: 2 additions & 1 deletion llvm/lib/IR/Verifier.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2318,7 +2318,8 @@ void Verifier::verifyFunctionAttrs(FunctionType *FT, AttributeList Attrs,
"'allockind()' requires exactly one of alloc, realloc, and free");
if ((Type == AllocFnKind::Free) &&
((K & (AllocFnKind::Uninitialized | AllocFnKind::Zeroed |
AllocFnKind::Aligned)) != AllocFnKind::Unknown))
AllocFnKind::Aligned | AllocFnKind::NoFree)) !=
AllocFnKind::Unknown))
CheckFailed("'allockind(\"free\")' doesn't allow uninitialized, zeroed, "
"or aligned modifiers.");
AllocFnKind ZeroedUninit = AllocFnKind::Uninitialized | AllocFnKind::Zeroed;
Expand Down
13 changes: 9 additions & 4 deletions llvm/lib/Transforms/IPO/AttributorAttributes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7063,10 +7063,15 @@ ChangeStatus AAHeapToStackFunction::updateImpl(Attributor &A) {
bool IsAssumedNoCapture = AA::hasAssumedIRAttr<Attribute::NoCapture>(
A, this, CBIRP, DepClassTy::OPTIONAL, IsKnownNoCapture);

// If a call site argument use is nofree, we are fine.
bool IsKnownNoFree;
bool IsAssumedNoFree = AA::hasAssumedIRAttr<Attribute::NoFree>(
A, this, CBIRP, DepClassTy::OPTIONAL, IsKnownNoFree);
// Check for potentially calls only when the allocator returns memory
// that may be freed explicitly.
bool IsAssumedNoFree = true;
if (!isNoFreeAllocFunction(AI.CB)) {
// If a call site argument use is nofree, we are fine.
bool IsKnownNoFree;
IsAssumedNoFree = AA::hasAssumedIRAttr<Attribute::NoFree>(
A, this, CBIRP, DepClassTy::OPTIONAL, IsKnownNoFree);
}

if (!IsAssumedNoCapture ||
(AI.LibraryFunctionId != LibFunc___kmpc_alloc_shared &&
Expand Down
52 changes: 43 additions & 9 deletions llvm/test/Transforms/Attributor/heap_to_stack.ll
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ define void @h2s_value_simplify_interaction(i1 %c, ptr %A) {
; CHECK: f2:
; CHECK-NEXT: [[L:%.*]] = load i8, ptr [[M]], align 16
; CHECK-NEXT: call void @usei8(i8 [[L]])
; CHECK-NEXT: call void @no_sync_func(ptr noalias nocapture nofree noundef nonnull align 16 dereferenceable(1) [[M]]) #[[ATTR11:[0-9]+]]
; CHECK-NEXT: call void @no_sync_func(ptr noalias nocapture nofree noundef nonnull align 16 dereferenceable(1) [[M]]) #[[ATTR12:[0-9]+]]
; CHECK-NEXT: br label [[J]]
; CHECK: dead:
; CHECK-NEXT: unreachable
; CHECK: j:
; CHECK-NEXT: [[PHI:%.*]] = phi ptr [ [[M]], [[F]] ], [ null, [[F2]] ]
; CHECK-NEXT: tail call void @no_sync_func(ptr nocapture nofree noundef align 16 [[PHI]]) #[[ATTR11]]
; CHECK-NEXT: tail call void @no_sync_func(ptr nocapture nofree noundef align 16 [[PHI]]) #[[ATTR12]]
; CHECK-NEXT: ret void
;
entry:
Expand Down Expand Up @@ -359,7 +359,7 @@ define void @test9() {
; CHECK-NEXT: [[I:%.*]] = tail call noalias ptr @malloc(i64 noundef 4)
; CHECK-NEXT: tail call void @no_sync_func(ptr nocapture nofree [[I]])
; CHECK-NEXT: store i32 10, ptr [[I]], align 4
; CHECK-NEXT: tail call void @foo_nounw(ptr nofree nonnull align 4 dereferenceable(4) [[I]]) #[[ATTR11]]
; CHECK-NEXT: tail call void @foo_nounw(ptr nofree nonnull align 4 dereferenceable(4) [[I]]) #[[ATTR12]]
; CHECK-NEXT: tail call void @free(ptr nocapture nonnull align 4 dereferenceable(4) [[I]])
; CHECK-NEXT: ret void
;
Expand Down Expand Up @@ -419,7 +419,7 @@ define void @test11() {
; CHECK-LABEL: define {{[^@]+}}@test11() {
; CHECK-NEXT: bb:
; CHECK-NEXT: [[I_H2S:%.*]] = alloca i8, i64 4, align 1
; CHECK-NEXT: tail call void @sync_will_return(ptr [[I_H2S]]) #[[ATTR11]]
; CHECK-NEXT: tail call void @sync_will_return(ptr [[I_H2S]]) #[[ATTR12]]
; CHECK-NEXT: ret void
;
bb:
Expand Down Expand Up @@ -670,7 +670,7 @@ define void @test16c(i8 %v, ptr %P) {
; CHECK-NEXT: bb:
; CHECK-NEXT: [[I_H2S:%.*]] = alloca i8, i64 4, align 1
; CHECK-NEXT: store ptr [[I_H2S]], ptr [[P]], align 8
; CHECK-NEXT: tail call void @no_sync_func(ptr nocapture nofree [[I_H2S]]) #[[ATTR11]]
; CHECK-NEXT: tail call void @no_sync_func(ptr nocapture nofree [[I_H2S]]) #[[ATTR12]]
; CHECK-NEXT: ret void
;
bb:
Expand Down Expand Up @@ -703,7 +703,7 @@ define void @test16e(i8 %v) norecurse {
; CHECK-NEXT: bb:
; CHECK-NEXT: [[I_H2S:%.*]] = alloca i8, i64 4, align 1
; CHECK-NEXT: store ptr [[I_H2S]], ptr @G, align 8
; CHECK-NEXT: call void @usei8p(ptr nocapture nofree [[I_H2S]]) #[[ATTR12:[0-9]+]]
; CHECK-NEXT: call void @usei8p(ptr nocapture nofree [[I_H2S]]) #[[ATTR13:[0-9]+]]
; CHECK-NEXT: ret void
;
bb:
Expand All @@ -715,6 +715,39 @@ bb:
ret void
}

declare noalias ptr @gc_alloc(i64) allockind("alloc,zeroed,nofree") allocsize(0)

; Check that a heap allocated object that is not captured and not explicitly
; freed can be converted to a stack object. This is often true for GCs.
define void @test_alloc_nofree_nocapture() {
; CHECK-LABEL: define {{[^@]+}}@test_alloc_nofree_nocapture() {
; CHECK-NEXT: bb:
; CHECK-NEXT: [[I_H2S:%.*]] = alloca i8, i64 4, align 1
; CHECK-NEXT: call void @llvm.memset.p0.i64(ptr [[I_H2S]], i8 0, i64 4, i1 false)
; CHECK-NEXT: tail call void @nocapture_func_frees_pointer(ptr noalias nocapture [[I_H2S]])
; CHECK-NEXT: ret void
;
bb:
%i = tail call noalias ptr @gc_alloc(i64 4)
tail call void @nocapture_func_frees_pointer(ptr %i)
ret void
}

; Check that a nofree heap allocated object that is captured is not converted to
; a stack object.
define void @test_alloc_nofree_captured() {
; CHECK-LABEL: define {{[^@]+}}@test_alloc_nofree_captured() {
; CHECK-NEXT: bb:
; CHECK-NEXT: [[I:%.*]] = tail call noalias ptr @gc_alloc(i64 noundef 4)
; CHECK-NEXT: tail call void @foo(ptr [[I]])
; CHECK-NEXT: ret void
;
bb:
%i = tail call noalias ptr @gc_alloc(i64 4)
tail call void @foo(ptr %i)
ret void
}

;.
; CHECK: attributes #[[ATTR0:[0-9]+]] = { allockind("alloc,uninitialized") allocsize(0) }
; CHECK: attributes #[[ATTR1:[0-9]+]] = { nounwind willreturn }
Expand All @@ -726,9 +759,10 @@ bb:
; CHECK: attributes #[[ATTR7:[0-9]+]] = { allockind("alloc,uninitialized,aligned") allocsize(1) }
; CHECK: attributes #[[ATTR8:[0-9]+]] = { allockind("alloc,zeroed") allocsize(0,1) }
; CHECK: attributes #[[ATTR9]] = { norecurse }
; CHECK: attributes #[[ATTR10:[0-9]+]] = { nocallback nofree nounwind willreturn memory(argmem: write) }
; CHECK: attributes #[[ATTR11]] = { nounwind }
; CHECK: attributes #[[ATTR12]] = { nocallback nosync nounwind willreturn }
; CHECK: attributes #[[ATTR10:[0-9]+]] = { allockind("alloc,zeroed,nofree") allocsize(0) }
; CHECK: attributes #[[ATTR11:[0-9]+]] = { nocallback nofree nounwind willreturn memory(argmem: write) }
; CHECK: attributes #[[ATTR12]] = { nounwind }
; CHECK: attributes #[[ATTR13]] = { nocallback nosync nounwind willreturn }
;.
;; NOTE: These prefixes are unused and the list is autogenerated. Do not add tests below this line:
; CGSCC: {{.*}}
Expand Down
Loading