Skip to content

[Linker] Propagate nobuiltin attributes when linking known libcalls #89431

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

Merged
merged 1 commit into from
May 9, 2024

Conversation

jhuber6
Copy link
Contributor

@jhuber6 jhuber6 commented Apr 19, 2024

Summary:
As discussed in https://discourse.llvm.org/t/rfc-libc-ffreestanding-fno-builtin.

LLVM ascribes special semantics to several functions that are known to
be libcalls. These are functions that middle-end optimizations may
transforms calls into or perform optimizations based off of known
semantics. However, these assumptions require an opaque function call to
be known valid. In situations like LTO or IR linking it is possible to
bring a libcall definition into the current module. Once this happens,
we can no longer make any guarantees about the semantics of these
functions.

We currently attempt to solve this by preventing all inlining if the
called function has no-builtin https://reviews.llvm.org/D74162.
However, this is overly pessimistic as it prevents all inlining even for
non-libcall functions.

This patch modifies the IRMover class to track known libcalls enabled
for the given target. If we encounter a known libcall during IR linking,
we then need to append the nobuiltin attribute to the destination
module. Afterwards, all new definitions we link in will be applied as
well.

@llvmbot llvmbot added the LTO Link time optimization (regular/full LTO or ThinLTO) label Apr 19, 2024
@llvmbot
Copy link
Member

llvmbot commented Apr 19, 2024

@llvm/pr-subscribers-lto

Author: Joseph Huber (jhuber6)

Changes

Summary:
As discussed in https://discourse.llvm.org/t/rfc-libc-ffreestanding-fno-builtin.

LLVM ascribes special semantics to several functions that are known to
be libcalls. These are functions that middle-end optimizations may
transforms calls into or perform optimizations based off of known
semantics. However, these assumptions require an opaque function call to
be known valid. In situations like LTO or IR linking it is possible to
bring a libcall definition into the current module. Once this happens,
we can no long make any gurantees about the semantics of these
functions.

We currently attempt to solve this by preventing all inlining if the
called function has no-builtin https://reviews.llvm.org/D74162.
However, this is overly pessimistic as it prevents all inlining even for
non-libcall functions.

This patch modifies the IRMover class to track known libcalls enabled
for the given target. If we encounter a known libcall during IR linking,
we then need to append the nobuiltin attribute to the destination
module. Afterwards, all new definitions we link in will be applied as
well.


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

5 Files Affected:

  • (modified) llvm/include/llvm/Linker/IRMover.h (+33-1)
  • (modified) llvm/lib/Linker/CMakeLists.txt (+1)
  • (modified) llvm/lib/Linker/IRMover.cpp (+46-3)
  • (added) llvm/test/Linker/Inputs/strlen.ll (+19)
  • (added) llvm/test/Linker/libcalls.ll (+25)
diff --git a/llvm/include/llvm/Linker/IRMover.h b/llvm/include/llvm/Linker/IRMover.h
index 1e3c5394ffa2af..57c6fd2488f14a 100644
--- a/llvm/include/llvm/Linker/IRMover.h
+++ b/llvm/include/llvm/Linker/IRMover.h
@@ -12,11 +12,14 @@
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/DenseSet.h"
 #include "llvm/ADT/FunctionExtras.h"
+#include "llvm/ADT/StringSet.h"
+#include "llvm/IR/GlobalValue.h"
+#include "llvm/Support/StringSaver.h"
+#include "llvm/TargetParser/Triple.h"
 #include <functional>
 
 namespace llvm {
 class Error;
-class GlobalValue;
 class Metadata;
 class Module;
 class StructType;
@@ -60,6 +63,34 @@ class IRMover {
     bool hasType(StructType *Ty);
   };
 
+  /// Utility for handling linking of known libcall functions. If a merged
+  /// module contains a recognized library call we can no longer perform any
+  /// libcall related transformations.
+  class LibcallHandler {
+    bool HasLibcalls = false;
+
+    StringSet<> Libcalls;
+    StringSet<> Triples;
+
+    BumpPtrAllocator Alloc;
+    StringSaver Saver;
+
+  public:
+    LibcallHandler() : Saver(Alloc) {}
+
+    void updateLibcalls(llvm::Triple Triple);
+
+    bool checkLibcalls(llvm::GlobalValue *GV) {
+      if (HasLibcalls)
+        return false;
+      HasLibcalls = GV && isa<Function>(GV) && !GV->isDeclaration() &&
+                    Libcalls.count(GV->getName());
+      return HasLibcalls;
+    }
+
+    bool hasLibcalls() const { return HasLibcalls; }
+  };
+
   IRMover(Module &M);
 
   typedef std::function<void(GlobalValue &)> ValueAdder;
@@ -84,6 +115,7 @@ class IRMover {
   Module &Composite;
   IdentifiedStructTypeSet IdentifiedStructTypes;
   MDMapT SharedMDs; ///< A Metadata map to use for all calls to \a move().
+  LibcallHandler Libcalls;
 };
 
 } // End llvm namespace
diff --git a/llvm/lib/Linker/CMakeLists.txt b/llvm/lib/Linker/CMakeLists.txt
index 5afb40f8b58842..25001c09a62d25 100644
--- a/llvm/lib/Linker/CMakeLists.txt
+++ b/llvm/lib/Linker/CMakeLists.txt
@@ -9,6 +9,7 @@ add_llvm_component_library(LLVMLinker
   intrinsics_gen
 
   LINK_COMPONENTS
+  Analysis
   Core
   Object
   Support
diff --git a/llvm/lib/Linker/IRMover.cpp b/llvm/lib/Linker/IRMover.cpp
index 7a5aa0c8047821..d60d6d1feb153b 100644
--- a/llvm/lib/Linker/IRMover.cpp
+++ b/llvm/lib/Linker/IRMover.cpp
@@ -12,6 +12,7 @@
 #include "llvm/ADT/SetVector.h"
 #include "llvm/ADT/SmallPtrSet.h"
 #include "llvm/ADT/SmallString.h"
+#include "llvm/Analysis/TargetLibraryInfo.h"
 #include "llvm/IR/AutoUpgrade.h"
 #include "llvm/IR/Constants.h"
 #include "llvm/IR/DebugInfoMetadata.h"
@@ -399,6 +400,9 @@ class IRLinker {
   /// A metadata map that's shared between IRLinker instances.
   MDMapT &SharedMDs;
 
+  /// A list of libcalls that the current target may call.
+  IRMover::LibcallHandler &Libcalls;
+
   /// Mapping of values from what they used to be in Src, to what they are now
   /// in DstM.  ValueToValueMapTy is a ValueMap, which involves some overhead
   /// due to the use of Value handles which the Linker doesn't actually need,
@@ -540,10 +544,12 @@ class IRLinker {
   IRLinker(Module &DstM, MDMapT &SharedMDs,
            IRMover::IdentifiedStructTypeSet &Set, std::unique_ptr<Module> SrcM,
            ArrayRef<GlobalValue *> ValuesToLink,
+           llvm::IRMover::LibcallHandler &Libcalls,
            IRMover::LazyCallback AddLazyFor, bool IsPerformingImport)
       : DstM(DstM), SrcM(std::move(SrcM)), AddLazyFor(std::move(AddLazyFor)),
         TypeMap(Set), GValMaterializer(*this), LValMaterializer(*this),
-        SharedMDs(SharedMDs), IsPerformingImport(IsPerformingImport),
+        SharedMDs(SharedMDs), Libcalls(Libcalls),
+        IsPerformingImport(IsPerformingImport),
         Mapper(ValueMap, RF_ReuseAndMutateDistinctMDs | RF_IgnoreMissingLocals,
                &TypeMap, &GValMaterializer),
         IndirectSymbolMCID(Mapper.registerAlternateMappingContext(
@@ -1605,14 +1611,27 @@ Error IRLinker::run() {
 
   DstM.setTargetTriple(SrcTriple.merge(DstTriple));
 
+  // Update the target triple's libcall information if it was changed.
+  Libcalls.updateLibcalls(Triple(DstM.getTargetTriple()));
+
   // Loop over all of the linked values to compute type mappings.
   computeTypeMapping();
 
+  bool AddsLibcalls;
   std::reverse(Worklist.begin(), Worklist.end());
   while (!Worklist.empty()) {
     GlobalValue *GV = Worklist.back();
     Worklist.pop_back();
 
+    // If the module already contains libcall functions we need every function
+    // linked in to have `nobuiltin` attributes. Otherwise check if this is a
+    // libcall definition.
+    if (Function *F = dyn_cast<Function>(GV); F && Libcalls.hasLibcalls()) {
+      F->setAttributes(F->getAttributes().addFnAttribute(
+          F->getParent()->getContext(), Attribute::NoBuiltin));
+    } else
+      AddsLibcalls = Libcalls.checkLibcalls(GV);
+
     // Already mapped.
     if (ValueMap.find(GV) != ValueMap.end() ||
         IndirectSymbolValueMap.find(GV) != IndirectSymbolValueMap.end())
@@ -1675,6 +1694,14 @@ Error IRLinker::run() {
     }
   }
 
+  // If we have imported a recognized libcall function we can no longer make any
+  // reasonable optimizations based off of its semantics. Add the 'nobuiltin'
+  // attribute to every function to suppress libcall detection.
+  if (AddsLibcalls)
+    for (Function &F : DstM.functions())
+      F.setAttributes(F.getAttributes().addFnAttribute(DstM.getContext(),
+                                                       Attribute::NoBuiltin));
+
   // Merge the module flags into the DstM module.
   return linkModuleFlagsMetadata();
 }
@@ -1757,6 +1784,22 @@ bool IRMover::IdentifiedStructTypeSet::hasType(StructType *Ty) {
   return I == NonOpaqueStructTypes.end() ? false : *I == Ty;
 }
 
+void IRMover::LibcallHandler::updateLibcalls(llvm::Triple Triple) {
+  if (Triples.count(Triple.getTriple()))
+    return;
+  Triples.insert(Saver.save(Triple.getTriple()));
+
+  // Collect the names of runtime functions that the target may want to call.
+  TargetLibraryInfoImpl TLII(Triple);
+  TargetLibraryInfo TLI(TLII);
+  for (unsigned I = 0, E = static_cast<unsigned>(LibFunc::NumLibFuncs); I != E;
+       ++I) {
+    LibFunc F = static_cast<LibFunc>(I);
+    if (TLI.has(F))
+      Libcalls.insert(TLI.getName(F));
+  }
+}
+
 IRMover::IRMover(Module &M) : Composite(M) {
   TypeFinder StructTypes;
   StructTypes.run(M, /* OnlyNamed */ false);
@@ -1778,8 +1821,8 @@ Error IRMover::move(std::unique_ptr<Module> Src,
                     ArrayRef<GlobalValue *> ValuesToLink,
                     LazyCallback AddLazyFor, bool IsPerformingImport) {
   IRLinker TheIRLinker(Composite, SharedMDs, IdentifiedStructTypes,
-                       std::move(Src), ValuesToLink, std::move(AddLazyFor),
-                       IsPerformingImport);
+                       std::move(Src), ValuesToLink, Libcalls,
+                       std::move(AddLazyFor), IsPerformingImport);
   Error E = TheIRLinker.run();
   Composite.dropTriviallyDeadConstantArrays();
   return E;
diff --git a/llvm/test/Linker/Inputs/strlen.ll b/llvm/test/Linker/Inputs/strlen.ll
new file mode 100644
index 00000000000000..9d8b2cd9f0e2a5
--- /dev/null
+++ b/llvm/test/Linker/Inputs/strlen.ll
@@ -0,0 +1,19 @@
+target triple = "x86_64-unknown-linux-gnu"
+
+define i64 @strlen(ptr %s) {
+entry:
+  br label %for.cond
+
+for.cond:
+  %s.addr.0 = phi ptr [ %s, %entry ], [ %incdec.ptr, %for.cond ]
+  %0 = load i8, ptr %s.addr.0, align 1
+  %tobool.not = icmp eq i8 %0, 0
+  %incdec.ptr = getelementptr inbounds i8, ptr %s.addr.0, i64 1
+  br i1 %tobool.not, label %for.end, label %for.cond
+
+for.end:
+  %sub.ptr.lhs.cast = ptrtoint ptr %s.addr.0 to i64
+  %sub.ptr.rhs.cast = ptrtoint ptr %s to i64
+  %sub.ptr.sub = sub i64 %sub.ptr.lhs.cast, %sub.ptr.rhs.cast
+  ret i64 %sub.ptr.sub
+}
diff --git a/llvm/test/Linker/libcalls.ll b/llvm/test/Linker/libcalls.ll
new file mode 100644
index 00000000000000..a6c560585cbe4f
--- /dev/null
+++ b/llvm/test/Linker/libcalls.ll
@@ -0,0 +1,25 @@
+; RUN: llvm-link %s %S/Inputs/strlen.ll -S -o - 2>%t.a.err | FileCheck %s --check-prefix=CHECK1
+; RUN: llvm-link %S/Inputs/strlen.ll %s -S -o - 2>%t.a.err | FileCheck %s --check-prefix=CHECK2
+
+target triple = "x86_64-unknown-linux-gnu"
+
+@.str = private unnamed_addr constant [7 x i8] c"string\00", align 1
+@str = dso_local global ptr @.str, align 8
+
+declare i64 @strlen(ptr)
+
+define i64 @foo() {
+entry:
+  %0 = load ptr, ptr @str, align 8
+  %call = call i64 @strlen(ptr noundef %0)
+  ret i64 %call
+}
+
+; CHECK1: define i64 @foo() #[[ATTR0:[0-9]+]]
+; CHECK1: define i64 @strlen(ptr [[S:%.*]]) #[[ATTR0]]
+
+; CHECK2: define i64 @strlen(ptr [[S:%.*]]) #[[ATTR0:[0-9]+]]
+; CHECK2: define i64 @foo() #[[ATTR0]]
+
+; CHECK1: attributes #[[ATTR0]] = { nobuiltin }
+; CHECK2: attributes #[[ATTR0]] = { nobuiltin }

jhuber6 added a commit to jhuber6/llvm-project that referenced this pull request Apr 19, 2024
Summary:
Previously, we refused to inline functions between a callee that had the
`"no-builtins"` attribute and a caller that did not. This was done to
prevent a bug where libcalls (such as strlen) would be infinitely
inlined into the caller and then turned back into the libcall.

This solution did not cover all cases, as we can do IPO modifications on
the imported libcalls without strictly inlining them that is also
invalid. Additionally, this prevented *all* LTO inlining between modules
compiled with `-fno-builtin` and those that did not, which heavily
pessimized GPU performance when going through the LTO pipeline.

The underlying problem should be solved in a related patch
llvm#89431. This instead appends
the `nobuiltin` attribute to every function in the module once we import
a known libcall. This should prevent the behavior that this original
patch was trying to prevent, so we can now permit inlining.

Depends on: llvm#89431
Copy link
Member

@jdoerfert jdoerfert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this works without a module flag because the user might switch the order; linking the application into libc, or linking something else later into the application with libc. Either way, we wouldn't link in libcalls but we already had them. Still we can't perform libcall reasoning on the new functions.

Comment on lines 1626 to 1631
// If the module already contains libcall functions we need every function
// linked in to have `nobuiltin` attributes. Otherwise check if this is a
// libcall definition.
if (Function *F = dyn_cast<Function>(GV); F && Libcalls.hasLibcalls())
F->setAttributes(F->getAttributes().addFnAttribute(F->getContext(),
Attribute::NoBuiltin));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you have to do this anyway at the end? Just have a single location to do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, there are two cases.

  1. Linking in a module that contains libcalls.

When this happens we need to modify all definitions in the destination / merged module. However, once we do this once we know that the merged module is already processed.

  1. Linking a module into a module that already contains libcalls

Here we only need to add the attributes to the definitions as they come in because we know the destination module has already been handled (If the boolean is set true). I did it this way to avoid looping over every single function that we've already touched. I could make it do that if you think the code clarity is better.

@jhuber6
Copy link
Contributor Author

jhuber6 commented Apr 19, 2024

I don't think this works without a module flag because the user might switch the order; linking the application into libc, or linking something else later into the application with libc. Either way, we wouldn't link in libcalls but we already had them. Still we can't perform libcall reasoning on the new functions.

Doesn't this cover that case? The test file links the application and libc definition in either order. Both cases put nobuiltin on all the resulting functions.

@jhuber6 jhuber6 force-pushed the LTOBuiltins branch 2 times, most recently from 4f0fee7 to 6d7224e Compare April 19, 2024 21:18
@jhuber6
Copy link
Contributor Author

jhuber6 commented Apr 21, 2024

I wonder if this should add "no-builtins" as well. I don't really know what that does that "no-builtins" doesnt, but I noticed that there's some logic in the TargetLibraryInfo that optionally takes a function and disables all libcalls if it has "no-builtins" so it's possible something we should propagate.

@jhuber6
Copy link
Contributor Author

jhuber6 commented Apr 25, 2024

ping

@jhuber6 jhuber6 requested a review from frobtech May 3, 2024 01:53
@jhuber6
Copy link
Contributor Author

jhuber6 commented May 3, 2024

ping

@gchatelet gchatelet removed their request for review May 3, 2024 11:56
@gchatelet
Copy link
Contributor

@jhuber6 I don't know the linking part enough to do a good review here so I will leave it to more knowledgeable people. @teresajohnson would you be able to help?

Copy link
Member

@jdoerfert jdoerfert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test seems to show my concerns were unwarranted.
See the comment there to make it a little more elaborate.
The code looks good and I don't think it'll impact compile time.


@.str = private unnamed_addr constant [7 x i8] c"string\00", align 1
@str = dso_local global ptr @.str, align 8

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add another function here, so the order in the output should be
something
strlen
something
and all should be annotated.

Summary:
As discussed in https://discourse.llvm.org/t/rfc-libc-ffreestanding-fno-builtin.

LLVM ascribes special semantics to several functions that are known to
be `libcalls`. These are functions that middle-end optimizations may
transforms calls into or perform optimizations based off of known
semantics. However, these assumptions require an opaque function call to
be known valid. In situations like LTO or IR linking it is possible to
bring a libcall definition into the current module. Once this happens,
we can no long make any gurantees about the semantics of these
functions.

We currently attempt to solve this by preventing all inlining if the
called function has `no-builtin` https://reviews.llvm.org/D74162.
However, this is overly pessimistic as it prevents all inlining even for
non-libcall functions.

This patch modifies the IRMover class to track known libcalls enabled
for the given target. If we encounter a known libcall during IR linking,
we then need to append the `nobuiltin` attribute to the destination
module. Afterwards, all new definitions we link in will be applied as
well.
@jhuber6 jhuber6 merged commit aa16de6 into llvm:main May 9, 2024
4 checks passed
jhuber6 added a commit that referenced this pull request May 9, 2024
…libcalls (#89431)"

This apparently breaks AMDGPU offloading for unknown reasons. Reverting
for now.

This reverts commit aa16de6.
jhuber6 added a commit to jhuber6/llvm-project that referenced this pull request May 21, 2024
…lvm#89431

Summary:
As discussed in
https://discourse.llvm.org/t/rfc-libc-ffreestanding-fno-builtin.

LLVM ascribes special semantics to several functions that are known to
be `libcalls`. These are functions that middle-end optimizations may
transforms calls into or perform optimizations based off of known
semantics. However, these assumptions require an opaque function call to
be known valid. In situations like LTO or IR linking it is possible to
bring a libcall definition into the current module. Once this happens,
we can no longer make any guarantees about the semantics of these
functions.

We currently attempt to solve this by preventing all inlining if the
called function has `no-builtin` https://reviews.llvm.org/D74162.
However, this is overly pessimistic as it prevents all inlining even for
non-libcall functions.

This patch modifies the IRMover class to track known libcalls enabled
for the given target. If we encounter a known libcall during IR linking,
we then need to append the `nobuiltin` attribute to the destination
module. Afterwards, all new definitions we link in will be applied as
well.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LTO Link time optimization (regular/full LTO or ThinLTO)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants