-
Notifications
You must be signed in to change notification settings - Fork 12.4k
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
[Attributor] Teach HeapToStack about conservative GC allocators #113299
base: main
Are you sure you want to change the base?
Conversation
When a conservative GC allocates memory, and this memory is flagged as never explicitly freed, the memory can be stack allocated even if a pointer is passed to functions that don't have 'nofree' set. This is very useful for a conservative GC where memory is known to be never explicitly freed.
@llvm/pr-subscribers-llvm-ir @llvm/pr-subscribers-llvm-analysis Author: Ayke (aykevl) ChangesWhen a conservative GC allocates memory, and this memory is flagged as never explicitly freed, the memory can be stack allocated even if a pointer is passed to functions that don't have 'nofree' set. This is very useful for a conservative GC where memory is known to never be explicitly freed. Context: I plan on using this optimization in TinyGo, so that we don't need a custom heap-to-stack transformation pass but can instead rely on the Attributor pass instead (which will likely do a better job anyway). I am not entirely sure about the "nofree" Full diff: https://github.com/llvm/llvm-project/pull/113299.diff 9 Files Affected:
diff --git a/llvm/docs/LangRef.rst b/llvm/docs/LangRef.rst
index b83675c6ed97aa..7edeb090bce959 100644
--- a/llvm/docs/LangRef.rst
+++ b/llvm/docs/LangRef.rst
@@ -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
diff --git a/llvm/include/llvm/Analysis/MemoryBuiltins.h b/llvm/include/llvm/Analysis/MemoryBuiltins.h
index 7b48844cc9e8e9..c03e5cadecb8ff 100644
--- a/llvm/include/llvm/Analysis/MemoryBuiltins.h
+++ b/llvm/include/llvm/Analysis/MemoryBuiltins.h
@@ -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
diff --git a/llvm/include/llvm/IR/Attributes.h b/llvm/include/llvm/IR/Attributes.h
index 2755ced404dddb..c7eb8189c13f04 100644
--- a/llvm/include/llvm/IR/Attributes.h
+++ b/llvm/include/llvm/IR/Attributes.h
@@ -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)
};
diff --git a/llvm/lib/Analysis/MemoryBuiltins.cpp b/llvm/lib/Analysis/MemoryBuiltins.cpp
index dc2dc4c1733b5e..4ec0db03ade21f 100644
--- a/llvm/lib/Analysis/MemoryBuiltins.cpp
+++ b/llvm/lib/Analysis/MemoryBuiltins.cpp
@@ -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);
diff --git a/llvm/lib/AsmParser/LLParser.cpp b/llvm/lib/AsmParser/LLParser.cpp
index 6a2372c9751408..7fcabb64f17965 100644
--- a/llvm/lib/AsmParser/LLParser.cpp
+++ b/llvm/lib/AsmParser/LLParser.cpp
@@ -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);
}
diff --git a/llvm/lib/IR/Attributes.cpp b/llvm/lib/IR/Attributes.cpp
index e9daa01b899e8f..c63375edc2dd40 100644
--- a/llvm/lib/IR/Attributes.cpp
+++ b/llvm/lib/IR/Attributes.cpp
@@ -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();
diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index f34fe7594c8602..19666f4cc43bc4 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -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;
diff --git a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
index 5d17ffa61272ee..3c21e5599a0742 100644
--- a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
+++ b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
@@ -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 &&
diff --git a/llvm/test/Transforms/Attributor/heap_to_stack.ll b/llvm/test/Transforms/Attributor/heap_to_stack.ll
index 33ac066e43d093..d1f387ddc5a1e7 100644
--- a/llvm/test/Transforms/Attributor/heap_to_stack.ll
+++ b/llvm/test/Transforms/Attributor/heap_to_stack.ll
@@ -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:
@@ -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
;
@@ -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:
@@ -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:
@@ -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:
@@ -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 }
@@ -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: {{.*}}
|
@llvm/pr-subscribers-llvm-transforms Author: Ayke (aykevl) ChangesWhen a conservative GC allocates memory, and this memory is flagged as never explicitly freed, the memory can be stack allocated even if a pointer is passed to functions that don't have 'nofree' set. This is very useful for a conservative GC where memory is known to never be explicitly freed. Context: I plan on using this optimization in TinyGo, so that we don't need a custom heap-to-stack transformation pass but can instead rely on the Attributor pass instead (which will likely do a better job anyway). I am not entirely sure about the "nofree" Full diff: https://github.com/llvm/llvm-project/pull/113299.diff 9 Files Affected:
diff --git a/llvm/docs/LangRef.rst b/llvm/docs/LangRef.rst
index b83675c6ed97aa..7edeb090bce959 100644
--- a/llvm/docs/LangRef.rst
+++ b/llvm/docs/LangRef.rst
@@ -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
diff --git a/llvm/include/llvm/Analysis/MemoryBuiltins.h b/llvm/include/llvm/Analysis/MemoryBuiltins.h
index 7b48844cc9e8e9..c03e5cadecb8ff 100644
--- a/llvm/include/llvm/Analysis/MemoryBuiltins.h
+++ b/llvm/include/llvm/Analysis/MemoryBuiltins.h
@@ -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
diff --git a/llvm/include/llvm/IR/Attributes.h b/llvm/include/llvm/IR/Attributes.h
index 2755ced404dddb..c7eb8189c13f04 100644
--- a/llvm/include/llvm/IR/Attributes.h
+++ b/llvm/include/llvm/IR/Attributes.h
@@ -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)
};
diff --git a/llvm/lib/Analysis/MemoryBuiltins.cpp b/llvm/lib/Analysis/MemoryBuiltins.cpp
index dc2dc4c1733b5e..4ec0db03ade21f 100644
--- a/llvm/lib/Analysis/MemoryBuiltins.cpp
+++ b/llvm/lib/Analysis/MemoryBuiltins.cpp
@@ -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);
diff --git a/llvm/lib/AsmParser/LLParser.cpp b/llvm/lib/AsmParser/LLParser.cpp
index 6a2372c9751408..7fcabb64f17965 100644
--- a/llvm/lib/AsmParser/LLParser.cpp
+++ b/llvm/lib/AsmParser/LLParser.cpp
@@ -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);
}
diff --git a/llvm/lib/IR/Attributes.cpp b/llvm/lib/IR/Attributes.cpp
index e9daa01b899e8f..c63375edc2dd40 100644
--- a/llvm/lib/IR/Attributes.cpp
+++ b/llvm/lib/IR/Attributes.cpp
@@ -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();
diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index f34fe7594c8602..19666f4cc43bc4 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -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;
diff --git a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
index 5d17ffa61272ee..3c21e5599a0742 100644
--- a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
+++ b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
@@ -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 &&
diff --git a/llvm/test/Transforms/Attributor/heap_to_stack.ll b/llvm/test/Transforms/Attributor/heap_to_stack.ll
index 33ac066e43d093..d1f387ddc5a1e7 100644
--- a/llvm/test/Transforms/Attributor/heap_to_stack.ll
+++ b/llvm/test/Transforms/Attributor/heap_to_stack.ll
@@ -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:
@@ -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
;
@@ -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:
@@ -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:
@@ -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:
@@ -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 }
@@ -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: {{.*}}
|
This PR needs to be separated into two: 1) add the support of |
For your own purpose, you can disable/filter out H2S AA explicitly when you construct the attributor. |
What do you mean by that? Are you referring to the other idea of using the |
Nvm. I misunderstood your description. |
Ok.
What do you think about this? I've come to prefer this, for added flexibility. If you agree I can update the PR (or split it in two as requested depending on how it works out) to use |
I'm not sure. If the memory can be free at background, a pointer could be potentially not dereferenceable. That contradicts to the |
Are you saying that the background freeing could happen while pointers to the memory are reachable? This is explicitly not the case for GC systems that arrange for every pointer to be unreachable before freeing the memory. |
I see. I'd suggest to start a |
When a conservative GC allocates memory, and this memory is flagged as never explicitly freed, the memory can be stack allocated even if a pointer is passed to functions that don't have 'nofree' set.
This is very useful for a conservative GC where memory is known to never be explicitly freed.
Context: I plan on using this optimization in TinyGo, so that we don't need a custom heap-to-stack transformation pass but can instead rely on the Attributor pass instead (which will likely do a better job anyway).
I am not entirely sure about the "nofree"
allockind
. This means that memory returned by the allocator will never be explicitly freed. Instead, it might be better to use thenofree
parameter attribute on the return value (which is currently not allowed). This way, the attribute can be added to a call site and each call site can opt into the "no explicit free" behavior if the compiler knows this particular allocation is never explicitly freed. What do you think?