Skip to content
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

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

Conversation

aykevl
Copy link
Contributor

@aykevl aykevl commented Oct 22, 2024

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 the nofree 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?

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.
@llvmbot
Copy link
Member

llvmbot commented Oct 22, 2024

@llvm/pr-subscribers-llvm-ir

@llvm/pr-subscribers-llvm-analysis

Author: Ayke (aykevl)

Changes

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 the nofree 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?


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

9 Files Affected:

  • (modified) llvm/docs/LangRef.rst (+2)
  • (modified) llvm/include/llvm/Analysis/MemoryBuiltins.h (+5)
  • (modified) llvm/include/llvm/IR/Attributes.h (+2)
  • (modified) llvm/lib/Analysis/MemoryBuiltins.cpp (+4)
  • (modified) llvm/lib/AsmParser/LLParser.cpp (+2)
  • (modified) llvm/lib/IR/Attributes.cpp (+2)
  • (modified) llvm/lib/IR/Verifier.cpp (+2-1)
  • (modified) llvm/lib/Transforms/IPO/AttributorAttributes.cpp (+9-4)
  • (modified) llvm/test/Transforms/Attributor/heap_to_stack.ll (+43-9)
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: {{.*}}

@llvmbot
Copy link
Member

llvmbot commented Oct 22, 2024

@llvm/pr-subscribers-llvm-transforms

Author: Ayke (aykevl)

Changes

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 the nofree 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?


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

9 Files Affected:

  • (modified) llvm/docs/LangRef.rst (+2)
  • (modified) llvm/include/llvm/Analysis/MemoryBuiltins.h (+5)
  • (modified) llvm/include/llvm/IR/Attributes.h (+2)
  • (modified) llvm/lib/Analysis/MemoryBuiltins.cpp (+4)
  • (modified) llvm/lib/AsmParser/LLParser.cpp (+2)
  • (modified) llvm/lib/IR/Attributes.cpp (+2)
  • (modified) llvm/lib/IR/Verifier.cpp (+2-1)
  • (modified) llvm/lib/Transforms/IPO/AttributorAttributes.cpp (+9-4)
  • (modified) llvm/test/Transforms/Attributor/heap_to_stack.ll (+43-9)
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: {{.*}}

@shiltian
Copy link
Contributor

This PR needs to be separated into two: 1) add the support of nofree in allockind as well as the corresponding IR tests 2) use it in attributor as well as tests

@shiltian
Copy link
Contributor

For your own purpose, you can disable/filter out H2S AA explicitly when you construct the attributor.

@aykevl
Copy link
Contributor Author

aykevl commented Oct 23, 2024

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 nofree parameter attribute on the return value of the allocator?

@shiltian
Copy link
Contributor

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 nofree parameter attribute on the return value of the allocator?

Nvm. I misunderstood your description.

@aykevl
Copy link
Contributor Author

aykevl commented Oct 24, 2024

Ok.

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 the nofree 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?

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 nofree on the return value, similar to how align on the return value is currently used to set the alignment of the alloca instruction.

@shiltian
Copy link
Contributor

I'm not sure. If the memory can be free at background, a pointer could be potentially not dereferenceable. That contradicts to the IsAssumedNoFree here, because this nofree means the pointer is still dereferenceable after the function call.

@eliasnaur
Copy link

I'm not sure. If the memory can be free at background, a pointer could be potentially not dereferenceable. That contradicts to the IsAssumedNoFree here, because this nofree means the pointer is still dereferenceable after the function call.

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.

Copy link
Contributor

shiltian commented Nov 12, 2024

I see. I'd suggest to start a NFCRFC PR about adding nofree to allockind and explaining the expected behavior. If that PR is merged, we can update attributor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants