Skip to content

TableGen: Emit static hash table for runtime libcalls #150192

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

Conversation

arsenm
Copy link
Contributor

@arsenm arsenm commented Jul 23, 2025

a961210 reverted a change
to use a binary search on the string name table because it
was too slow. This replaces it with a static string hash
table based on the known set of libcall names. Microbenchmarking
shows this is similarly fast to using DenseMap. It's possibly
slightly slower than using StringSet, though these aren't an
exact comparison. This also saves on the one time use construction
of the map, so it could be better in practice.

This search isn't simple set check, since it does find the
range of possible matches with the same name. There's also
an additional check for whether the current target supports
the name. The runtime constructed set doesn't require this,
since it only adds the symbols live for the target.

Followed algorithm from this post
http://0x80.pl/notesen/2023-04-30-lookup-in-strings.html

I'm also thinking the 2 special case global symbols should
just be added to RuntimeLibcalls. There are also other global
references emitted in the backend that aren't tracked; we probably
should just use this as a centralized database for all compiler
selected symbols.

@llvmbot
Copy link
Member

llvmbot commented Jul 23, 2025

@llvm/pr-subscribers-tablegen
@llvm/pr-subscribers-llvm-ir

@llvm/pr-subscribers-llvm-binary-utilities

Author: Matt Arsenault (arsenm)

Changes

a961210 reverted a change
to use a binary search on the string name table because it
was too slow. This replaces it with a static string hash
table based on the known set of libcall names. Microbenchmarking
shows this is similarly fast to using DenseMap. It's possibly
slightly slower than using StringSet, though these aren't an
exact comparison. This also saves on the one time use construction
of the map, so it could be better in practice.

This search isn't simple set check, since it does find the
range of possible matches with the same name. There's also
an additional check for whether the current target supports
the name. The runtime constructed set doesn't require this,
since it only adds the symbols live for the target.

Followed algorithm from this post
http://0x80.pl/notesen/2023-04-30-lookup-in-strings.html

I'm also thinking the 2 special case global symbols should
just be added to RuntimeLibcalls. There are also other global
references emitted in the backend that aren't tracked; we probably
should just use this as a centralized database for all compiler
selected symbols.


Patch is 31.05 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/150192.diff

9 Files Affected:

  • (modified) llvm/benchmarks/CMakeLists.txt (+17)
  • (added) llvm/benchmarks/RuntimeLibcalls.cpp (+116)
  • (modified) llvm/include/llvm/IR/RuntimeLibcalls.h (+36-8)
  • (modified) llvm/lib/IR/RuntimeLibcalls.cpp (+17-41)
  • (modified) llvm/lib/Object/IRSymtab.cpp (+22-24)
  • (modified) llvm/test/TableGen/RuntimeLibcallEmitter.td (+29-3)
  • (modified) llvm/unittests/IR/CMakeLists.txt (+1)
  • (added) llvm/unittests/IR/RuntimeLibcallsTest.cpp (+63)
  • (modified) llvm/utils/TableGen/Basic/RuntimeLibcallsEmitter.cpp (+199-13)
diff --git a/llvm/benchmarks/CMakeLists.txt b/llvm/benchmarks/CMakeLists.txt
index 1078efa55f497..9613678d2e0ac 100644
--- a/llvm/benchmarks/CMakeLists.txt
+++ b/llvm/benchmarks/CMakeLists.txt
@@ -11,3 +11,20 @@ add_benchmark(FormatVariadicBM FormatVariadicBM.cpp PARTIAL_SOURCES_INTENDED)
 add_benchmark(GetIntrinsicInfoTableEntriesBM GetIntrinsicInfoTableEntriesBM.cpp PARTIAL_SOURCES_INTENDED)
 add_benchmark(SandboxIRBench SandboxIRBench.cpp PARTIAL_SOURCES_INTENDED)
 
+# Extract the list of symbols in a random utility as sample data.
+set(SYMBOL_TEST_DATA_FILE "sample_symbol_list.txt")
+set(SYMBOL_TEST_DATA_SOURCE_BINARY $<TARGET_FILE:llc>)
+
+add_custom_command(OUTPUT ${SYMBOL_TEST_DATA_FILE}
+  COMMAND $<TARGET_FILE:llvm-nm> --no-demangle --no-sort
+  --format=just-symbols
+  ${SYMBOL_TEST_DATA_SOURCE_BINARY} > ${SYMBOL_TEST_DATA_FILE}
+  DEPENDS "$<TARGET_FILE:llvm-nm>" "$<TARGET_FILE:llc>")
+
+add_custom_target(generate-runtime-libcalls-sample-symbol-list
+                  DEPENDS ${SYMBOL_TEST_DATA_FILE})
+add_benchmark(RuntimeLibcallsBench RuntimeLibcalls.cpp PARTIAL_SOURCES_INTENDED)
+
+add_dependencies(RuntimeLibcallsBench generate-runtime-libcalls-sample-symbol-list)
+target_compile_definitions(RuntimeLibcallsBench PRIVATE
+  -DSYMBOL_TEST_DATA_FILE="${CMAKE_CURRENT_BINARY_DIR}/${SYMBOL_TEST_DATA_FILE}")
diff --git a/llvm/benchmarks/RuntimeLibcalls.cpp b/llvm/benchmarks/RuntimeLibcalls.cpp
new file mode 100644
index 0000000000000..47f68abff1e0d
--- /dev/null
+++ b/llvm/benchmarks/RuntimeLibcalls.cpp
@@ -0,0 +1,116 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/IR/RuntimeLibcalls.h"
+#include "benchmark/benchmark.h"
+#include "llvm/IR/DataLayout.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/LineIterator.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/TargetParser/Triple.h"
+#include <random>
+#include <string>
+using namespace llvm;
+
+static constexpr unsigned MaxFuncNameSize = 53;
+
+static std::vector<StringRef> getLibcallNameStringRefs() {
+  std::vector<StringRef> Names(RTLIB::NumLibcallImpls);
+  // Keep the strlens on the StringRef construction out of the benchmark loop.
+  for (RTLIB::LibcallImpl LC : RTLIB::libcall_impls()) {
+    const char *Name = RTLIB::RuntimeLibcallsInfo::getLibcallImplName(LC);
+    Names[LC] = StringRef(Name);
+  }
+
+  return Names;
+}
+
+static std::vector<std::string> getRandomFuncNames() {
+  std::mt19937_64 Rng;
+  std::uniform_int_distribution<> StringLengthDistribution(1, MaxFuncNameSize);
+  std::uniform_int_distribution<> CharDistribution(1, 255);
+  int NumTestFuncs = 1 << 10;
+  std::vector<std::string> TestFuncNames(NumTestFuncs);
+
+  for (std::string &TestFuncName : TestFuncNames) {
+    for (int I = 0, E = StringLengthDistribution(Rng); I != E; ++I)
+      TestFuncName += static_cast<char>(CharDistribution(Rng));
+  }
+
+  return TestFuncNames;
+}
+
+static std::vector<std::string> readSymbolsFromFile(StringRef InputFile) {
+  auto BufOrError = MemoryBuffer::getFileOrSTDIN(InputFile, /*IsText=*/true);
+  if (!BufOrError) {
+    reportFatalUsageError("failed to open \'" + Twine(InputFile) +
+                          "\': " + BufOrError.getError().message());
+  }
+
+  // Hackily figure out if there's a prefix on the symbol names - llvm-nm
+  // appears to not have a flag to skip this.
+  llvm::Triple HostTriple(LLVM_HOST_TRIPLE);
+  std::string DummyDatalayout = "e";
+  DummyDatalayout += DataLayout::getManglingComponent(HostTriple);
+
+  DataLayout DL(DummyDatalayout);
+  char GlobalPrefix = DL.getGlobalPrefix();
+
+  std::vector<std::string> Lines;
+  for (line_iterator LineIt(**BufOrError, /*SkipBlanks=*/true);
+       !LineIt.is_at_eof(); ++LineIt) {
+    StringRef SymbolName = *LineIt;
+    SymbolName.consume_front(StringRef(&GlobalPrefix, 1));
+
+    Lines.push_back(SymbolName.str());
+  }
+  return Lines;
+}
+
+static void BM_LookupRuntimeLibcallByNameKnownCalls(benchmark::State &State) {
+  std::vector<StringRef> Names = getLibcallNameStringRefs();
+
+  for (auto _ : State) {
+    for (StringRef Name : Names) {
+      benchmark::DoNotOptimize(
+          RTLIB::RuntimeLibcallsInfo::lookupLibcallImplName(Name).empty());
+    }
+  }
+}
+
+static void BM_LookupRuntimeLibcallByNameRandomCalls(benchmark::State &State) {
+  std::vector<std::string> TestFuncNames = getRandomFuncNames();
+
+  for (auto _ : State) {
+    for (const std::string &Name : TestFuncNames) {
+      benchmark::DoNotOptimize(
+          RTLIB::RuntimeLibcallsInfo::lookupLibcallImplName(StringRef(Name))
+              .empty());
+    }
+  }
+}
+
+// This isn't fully representative, it doesn't include any anonymous functions.
+// nm -n --no-demangle --format=just-symbols sample-binary > sample.txt
+static void BM_LookupRuntimeLibcallByNameSampleData(benchmark::State &State) {
+  std::vector<std::string> TestFuncNames =
+      readSymbolsFromFile(SYMBOL_TEST_DATA_FILE);
+  for (auto _ : State) {
+    for (const std::string &Name : TestFuncNames) {
+      benchmark::DoNotOptimize(
+          RTLIB::RuntimeLibcallsInfo::lookupLibcallImplName(StringRef(Name))
+              .empty());
+    }
+  }
+}
+
+BENCHMARK(BM_LookupRuntimeLibcallByNameKnownCalls);
+BENCHMARK(BM_LookupRuntimeLibcallByNameRandomCalls);
+BENCHMARK(BM_LookupRuntimeLibcallByNameSampleData);
+
+BENCHMARK_MAIN();
diff --git a/llvm/include/llvm/IR/RuntimeLibcalls.h b/llvm/include/llvm/IR/RuntimeLibcalls.h
index 89ad4e5bc6ca4..acd220f08160b 100644
--- a/llvm/include/llvm/IR/RuntimeLibcalls.h
+++ b/llvm/include/llvm/IR/RuntimeLibcalls.h
@@ -132,11 +132,41 @@ struct RuntimeLibcallsInfo {
     return ImplToLibcall[Impl];
   }
 
+  /// Check if a function name is a recognized runtime call of any kind. This
+  /// does not consider if this call is available for any current compilation,
+  /// just that it is a known call somewhere. This returns the set of all
+  /// LibcallImpls which match the name; multiple implementations with the same
+  /// name may exist but differ in interpretation based on the target context.
+  ///
+  /// Generated by tablegen.
+  LLVM_ABI static inline iota_range<RTLIB::LibcallImpl>
+  lookupLibcallImplName(StringRef Name){
+  // Inlining the early exit on the string name appears to be worthwhile when
+  // querying a real set of symbols
+#define GET_LOOKUP_LIBCALL_IMPL_NAME_BODY
+#include "llvm/IR/RuntimeLibcalls.inc"
+#undef GET_LOOKUP_LIBCALL_IMPL_NAME_BODY
+  }
+
   /// Check if this is valid libcall for the current module, otherwise
   /// RTLIB::Unsupported.
-  RTLIB::LibcallImpl getSupportedLibcallImpl(StringRef FuncName) const;
+  LLVM_ABI RTLIB::LibcallImpl
+      getSupportedLibcallImpl(StringRef FuncName) const {
+    for (RTLIB::LibcallImpl Impl : lookupLibcallImplName(FuncName)) {
+      // FIXME: This should not depend on looking up ImplToLibcall, only the
+      // list of libcalls for the module.
+      RTLIB::LibcallImpl Recognized = LibcallImpls[ImplToLibcall[Impl]];
+      if (Recognized != RTLIB::Unsupported)
+        return Recognized;
+    }
+
+    return RTLIB::Unsupported;
+  }
 
 private:
+  LLVM_ABI static iota_range<RTLIB::LibcallImpl>
+  lookupLibcallImplNameImpl(StringRef Name);
+
   static const RTLIB::LibcallImpl
       DefaultLibcallImpls[RTLIB::UNKNOWN_LIBCALL + 1];
 
@@ -160,13 +190,11 @@ struct RuntimeLibcallsInfo {
   /// Map from a concrete LibcallImpl implementation to its RTLIB::Libcall kind.
   LLVM_ABI static const RTLIB::Libcall ImplToLibcall[RTLIB::NumLibcallImpls];
 
-  /// Check if a function name is a recognized runtime call of any kind. This
-  /// does not consider if this call is available for any current compilation,
-  /// just that it is a known call somewhere. This returns the set of all
-  /// LibcallImpls which match the name; multiple implementations with the same
-  /// name may exist but differ in interpretation based on the target context.
-  LLVM_ABI static iterator_range<ArrayRef<uint16_t>::const_iterator>
-  getRecognizedLibcallImpls(StringRef FuncName);
+  /// Utility function for tablegenerated lookup function. Return a range of
+  /// enum values that apply for the function name at \p NameOffsetEntry with
+  /// the value \p StrOffset.
+  static inline iota_range<RTLIB::LibcallImpl>
+  libcallImplNameHit(uint16_t NameOffsetEntry, uint16_t StrOffset);
 
   static bool darwinHasSinCosStret(const Triple &TT) {
     if (!TT.isOSDarwin())
diff --git a/llvm/lib/IR/RuntimeLibcalls.cpp b/llvm/lib/IR/RuntimeLibcalls.cpp
index 5936ac7d0287f..51f8a146f86a7 100644
--- a/llvm/lib/IR/RuntimeLibcalls.cpp
+++ b/llvm/lib/IR/RuntimeLibcalls.cpp
@@ -8,15 +8,18 @@
 
 #include "llvm/IR/RuntimeLibcalls.h"
 #include "llvm/ADT/StringTable.h"
+#include "llvm/Support/xxhash.h"
 
 using namespace llvm;
 using namespace RTLIB;
 
 #define GET_INIT_RUNTIME_LIBCALL_NAMES
 #define GET_SET_TARGET_RUNTIME_LIBCALL_SETS
+#define DEFINE_GET_LOOKUP_LIBCALL_IMPL_NAME
 #include "llvm/IR/RuntimeLibcalls.inc"
 #undef GET_INIT_RUNTIME_LIBCALL_NAMES
 #undef GET_SET_TARGET_RUNTIME_LIBCALL_SETS
+#undef DEFINE_GET_LOOKUP_LIBCALL_IMPL_NAME
 
 static void setARMLibcallNames(RuntimeLibcallsInfo &Info, const Triple &TT,
                                FloatABI::ABIType FloatABIType,
@@ -135,49 +138,22 @@ void RuntimeLibcallsInfo::initLibcalls(const Triple &TT,
   }
 }
 
-RTLIB::LibcallImpl
-RuntimeLibcallsInfo::getSupportedLibcallImpl(StringRef FuncName) const {
-  const ArrayRef<uint16_t> RuntimeLibcallNameOffsets(
-      RuntimeLibcallNameOffsetTable);
-
-  iterator_range<ArrayRef<uint16_t>::const_iterator> Range =
-      getRecognizedLibcallImpls(FuncName);
-
-  for (auto I = Range.begin(); I != Range.end(); ++I) {
-    RTLIB::LibcallImpl Impl =
-        static_cast<RTLIB::LibcallImpl>(I - RuntimeLibcallNameOffsets.begin());
-
-    // FIXME: This should not depend on looking up ImplToLibcall, only the list
-    // of libcalls for the module.
-    RTLIB::LibcallImpl Recognized = LibcallImpls[ImplToLibcall[Impl]];
-    if (Recognized != RTLIB::Unsupported)
-      return Recognized;
+LLVM_ATTRIBUTE_ALWAYS_INLINE
+iota_range<RTLIB::LibcallImpl>
+RuntimeLibcallsInfo::libcallImplNameHit(uint16_t NameOffsetEntry,
+                                        uint16_t StrOffset) {
+  int NumAliases = 1;
+  for (int E = std::size(RuntimeLibcallNameOffsetTable);
+       NameOffsetEntry + NumAliases != E &&
+       RuntimeLibcallNameOffsetTable[NameOffsetEntry + NumAliases] == StrOffset;
+       ++NumAliases) {
   }
 
-  return RTLIB::Unsupported;
-}
-
-iterator_range<ArrayRef<uint16_t>::const_iterator>
-RuntimeLibcallsInfo::getRecognizedLibcallImpls(StringRef FuncName) {
-  StringTable::Iterator It = lower_bound(RuntimeLibcallImplNameTable, FuncName);
-  if (It == RuntimeLibcallImplNameTable.end() || *It != FuncName)
-    return iterator_range(ArrayRef<uint16_t>());
-
-  uint16_t IndexVal = It.offset().value();
-  const ArrayRef<uint16_t> TableRef(RuntimeLibcallNameOffsetTable);
-
-  ArrayRef<uint16_t>::const_iterator E = TableRef.end();
-  ArrayRef<uint16_t>::const_iterator EntriesBegin =
-      std::lower_bound(TableRef.begin(), E, IndexVal);
-  ArrayRef<uint16_t>::const_iterator EntriesEnd = EntriesBegin;
-
-  while (EntriesEnd != E && *EntriesEnd == IndexVal)
-    ++EntriesEnd;
-
-  assert(EntriesBegin != E &&
-         "libcall found in name table but not offset table");
-
-  return make_range(EntriesBegin, EntriesEnd);
+  RTLIB::LibcallImpl ImplStart = static_cast<RTLIB::LibcallImpl>(
+      &RuntimeLibcallNameOffsetTable[NameOffsetEntry] -
+      &RuntimeLibcallNameOffsetTable[0]);
+  return enum_seq(ImplStart,
+                  static_cast<RTLIB::LibcallImpl>(ImplStart + NumAliases));
 }
 
 bool RuntimeLibcallsInfo::darwinHasExp10(const Triple &TT) {
diff --git a/llvm/lib/Object/IRSymtab.cpp b/llvm/lib/Object/IRSymtab.cpp
index 0f194953787e6..7bba6f15dd1b5 100644
--- a/llvm/lib/Object/IRSymtab.cpp
+++ b/llvm/lib/Object/IRSymtab.cpp
@@ -54,6 +54,11 @@ static const char *PreservedSymbols[] = {
     "__stack_chk_guard",
 };
 
+static bool isPreservedGlobalVarName(StringRef Name) {
+  return StringRef(PreservedSymbols[0]) == Name ||
+         StringRef(PreservedSymbols[1]) == Name;
+}
+
 namespace {
 
 const char *getExpectedProducerName() {
@@ -81,12 +86,16 @@ struct Builder {
   // The StringTableBuilder does not create a copy of any strings added to it,
   // so this provides somewhere to store any strings that we create.
   Builder(SmallVector<char, 0> &Symtab, StringTableBuilder &StrtabBuilder,
-          BumpPtrAllocator &Alloc)
-      : Symtab(Symtab), StrtabBuilder(StrtabBuilder), Saver(Alloc) {}
+          BumpPtrAllocator &Alloc, const Triple &TT)
+      : Symtab(Symtab), StrtabBuilder(StrtabBuilder), Saver(Alloc), TT(TT),
+        Libcalls(TT) {}
 
   DenseMap<const Comdat *, int> ComdatMap;
   Mangler Mang;
-  Triple TT;
+  const Triple &TT;
+
+  // FIXME: This shouldn't be here.
+  RTLIB::RuntimeLibcallsInfo Libcalls;
 
   std::vector<storage::Comdat> Comdats;
   std::vector<storage::Module> Mods;
@@ -98,6 +107,10 @@ struct Builder {
 
   std::vector<storage::Str> DependentLibraries;
 
+  bool isPreservedLibFuncName(StringRef Name) {
+    return Libcalls.getSupportedLibcallImpl(Name) != RTLIB::Unsupported;
+  }
+
   void setStr(storage::Str &S, StringRef Value) {
     S.Offset = StrtabBuilder.add(Value);
     S.Size = Value.size();
@@ -213,19 +226,6 @@ Expected<int> Builder::getComdatIndex(const Comdat *C, const Module *M) {
   return P.first->second;
 }
 
-static StringSet<> buildPreservedSymbolsSet(const Triple &TT) {
-  StringSet<> PreservedSymbolSet;
-  PreservedSymbolSet.insert(std::begin(PreservedSymbols),
-                            std::end(PreservedSymbols));
-  // FIXME: Do we need to pass in ABI fields from TargetOptions?
-  RTLIB::RuntimeLibcallsInfo Libcalls(TT);
-  for (RTLIB::LibcallImpl Impl : Libcalls.getLibcallImpls()) {
-    if (Impl != RTLIB::Unsupported)
-      PreservedSymbolSet.insert(Libcalls.getLibcallImplName(Impl));
-  }
-  return PreservedSymbolSet;
-}
-
 Error Builder::addSymbol(const ModuleSymbolTable &Msymtab,
                          const SmallPtrSet<GlobalValue *, 4> &Used,
                          ModuleSymbolTable::Symbol Msym) {
@@ -279,13 +279,11 @@ Error Builder::addSymbol(const ModuleSymbolTable &Msymtab,
     return Error::success();
   }
 
-  setStr(Sym.IRName, GV->getName());
-
-  static const StringSet<> PreservedSymbolsSet =
-      buildPreservedSymbolsSet(GV->getParent()->getTargetTriple());
-  bool IsPreservedSymbol = PreservedSymbolsSet.contains(GV->getName());
+  StringRef GVName = GV->getName();
+  setStr(Sym.IRName, GVName);
 
-  if (Used.count(GV) || IsPreservedSymbol)
+  if (Used.count(GV) || isPreservedLibFuncName(GVName) ||
+      isPreservedGlobalVarName(GVName))
     Sym.Flags |= 1 << storage::Symbol::FB_used;
   if (GV->isThreadLocal())
     Sym.Flags |= 1 << storage::Symbol::FB_tls;
@@ -352,7 +350,6 @@ Error Builder::build(ArrayRef<Module *> IRMods) {
   setStr(Hdr.Producer, kExpectedProducerName);
   setStr(Hdr.TargetTriple, IRMods[0]->getTargetTriple().str());
   setStr(Hdr.SourceFileName, IRMods[0]->getSourceFileName());
-  TT = IRMods[0]->getTargetTriple();
 
   for (auto *M : IRMods)
     if (Error Err = addModule(M))
@@ -378,7 +375,8 @@ Error Builder::build(ArrayRef<Module *> IRMods) {
 Error irsymtab::build(ArrayRef<Module *> Mods, SmallVector<char, 0> &Symtab,
                       StringTableBuilder &StrtabBuilder,
                       BumpPtrAllocator &Alloc) {
-  return Builder(Symtab, StrtabBuilder, Alloc).build(Mods);
+  const Triple &TT = Mods[0]->getTargetTriple();
+  return Builder(Symtab, StrtabBuilder, Alloc, TT).build(Mods);
 }
 
 // Upgrade a vector of bitcode modules created by an old version of LLVM by
diff --git a/llvm/test/TableGen/RuntimeLibcallEmitter.td b/llvm/test/TableGen/RuntimeLibcallEmitter.td
index 579e3c7dd62ab..2bebe2b2082eb 100644
--- a/llvm/test/TableGen/RuntimeLibcallEmitter.td
+++ b/llvm/test/TableGen/RuntimeLibcallEmitter.td
@@ -95,8 +95,8 @@ def BlahLibrary : SystemRuntimeLibrary<isBlahArch, (add calloc, LibraryWithCondi
 // CHECK-NEXT:  __lshrdi3 = 4, // __lshrdi3
 // CHECK-NEXT:  bzero = 5, // bzero
 // CHECK-NEXT:  calloc = 6, // calloc
-// CHECK-NEXT:  sqrtl_f80 = 7, // sqrtl
-// CHECK-NEXT:  sqrtl_f128 = 8, // sqrtl
+// CHECK-NEXT:  sqrtl_f128 = 7, // sqrtl
+// CHECK-NEXT:  sqrtl_f80 = 8, // sqrtl
 // CHECK-NEXT:  NumLibcallImpls = 9
 // CHECK-NEXT: };
 // CHECK-NEXT: } // End namespace RTLIB
@@ -157,10 +157,36 @@ def BlahLibrary : SystemRuntimeLibrary<isBlahArch, (add calloc, LibraryWithCondi
 // CHECK-NEXT: RTLIB::SRL_I64, // RTLIB::__lshrdi3
 // CHECK-NEXT: RTLIB::BZERO, // RTLIB::bzero
 // CHECK-NEXT: RTLIB::CALLOC, // RTLIB::calloc
-// CHECK-NEXT: RTLIB::SQRT_F80, // RTLIB::sqrtl_f80
 // CHECK-NEXT: RTLIB::SQRT_F128, // RTLIB::sqrtl_f128
+// CHECK-NEXT: RTLIB::SQRT_F80, // RTLIB::sqrtl_f80
 // CHECK-NEXT: };
 
+// CHECK: #ifdef GET_LOOKUP_LIBCALL_IMPL_NAME_BODY
+// CHECK-NEXT: size_t Size = Name.size();
+// CHECK-NEXT: if (Size == 0 || Size > 9)
+// CHECK-NEXT:   return enum_seq(RTLIB::Unsupported, RTLIB::Unsupported);
+// CHECK-NEXT: return lookupLibcallImplNameImpl(Name);
+// CHECK-NEXT: #endif
+
+// CHECK: #ifdef DEFINE_GET_LOOKUP_LIBCALL_IMPL_NAME
+// CHECK-NEXT: static inline uint64_t hash(StringRef Str) {
+// CHECK-NEXT: return static_cast<uint32_t>(xxh3_64bits(Str));
+// CHECK-NEXT: }
+
+// CHECK: iota_range<RTLIB::LibcallImpl> RTLIB::RuntimeLibcallsInfo::lookupLibcallImplNameImpl(StringRef Name) {
+// CHECK: static const std::pair<uint16_t, uint16_t> HashTableNameToEnum[16] = {
+// CHECK: {2, 9}, // 0x000000705301b8, ___memset
+// CHECK: {0, 0},
+// CHECK: {6, 6}, // 0x0000001417a2af, calloc
+// CHECK: {0, 0},
+// CHECK: };
+
+// CHECK: unsigned Idx = (hash(Name) % 8) * 2;
+// CHECK: for (int I = 0; I != 2; ++I) {
+// CHECK: return libcallImplNameHit(Entry, StrOffset);
+
+// CHECK: return enum_seq(RTLIB::Unsupported, RTLIB::Unsupported);
+// CHECK-NEXT: }
 
 // CHECK: void llvm::RTLIB::RuntimeLibcallsInfo::setTargetRuntimeLibcallSets(const llvm::Triple &TT, FloatABI::ABIType FloatABI) {
 // CHECK-NEXT:  struct LibcallImplPair {
diff --git a/llvm/unittests/IR/CMakeLists.txt b/llvm/unittests/IR/CMakeLists.txt
index b66eae93f9339..8b7bd3997ea27 100644
--- a/llvm/unittests/IR/CMakeLists.txt
+++ b/llvm/unittests/IR/CMakeLists.txt
@@ -43,6 +43,7 @@ add_llvm_unittest(IRTests
   PatternMatch.cpp
   ShuffleVectorInstTest.cpp
   StructuralHashTest.cpp
+  RuntimeLibcallsTest.cpp
   TimePassesTest.cpp
   TypesTest.cpp
   UseTest.cpp
diff --git a/llvm/unittests/IR/RuntimeLibcallsTest.cpp b/llvm/unittests/IR/RuntimeLibcallsTest.cpp
new file mode 100644
index 0000000000000..1ef79e74f9a4b
--- /dev/null
+++ b/llvm/unittests/IR/RuntimeLibcallsTest.cpp
@@ -0,0 +1,63 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/IR/RuntimeLibcalls.h"
+#include "llvm/ADT/STLExtras.h"
+#include "gtest/gtest.h"
+using namespace llvm;
+
+namespace {
+
+TEST(RuntimeLibcallsTest, LibcallImplByName) {
+  EXPECT_TRUE(RTLIB::RuntimeLibcallsInfo::lookupLibcallImplName("").empty());
+  EXPECT_TRUE(
+      RTLIB::RuntimeLibcallsInfo::lookupLibcallImplName("unknown").empty());
+  EXPECT_TRUE(
+      RTLIB::RuntimeLibcallsInfo::lookupLibcallImplName("Unsupported").empty());
+  EXPECT_TRUE(
+      RTLIB::RuntimeLibcallsInfo::lookupLibcallImplName("unsupported").empty());
+
+  for (RTLIB::LibcallImpl LC : RTLIB::libcall_impls()) {
+    const char *Name = RTLIB::RuntimeLibcallsInfo::getLibcallImplName(LC);
+    EXPECT_FALSE(
+        RTLIB::RuntimeLibcallsInfo::lookupLibcallImplName(Name).empty());
+  }
+
+  // Test first libcall name
+  EXPECT_EQ(
+      RTLIB::arm64ec__Unwind_Resume,
+      *RTLIB::RuntimeLibcallsInfo::lookupLibcallImplName("#_Unwind_Resume")
+           ...
[truncated]

@vvereschaka
Copy link
Contributor

@efriedma-quic
Copy link
Collaborator

ping

(https://lab.llvm.org/buildbot/#/builders/187/builds/8516)

Did you mean to put this comment somewhere else? This isn't merged.

@vvereschaka
Copy link
Contributor

Sorry, wasn't clear. This PR should fix the broken RuntimeLibcallEmitter.td test on the expensive check builders. The problem remains there for almost a week and it would be good to fix it. Because of it i'm trying to understand when this PR will be merged.

@nikic
Copy link
Contributor

nikic commented Jul 25, 2025

@vvereschaka That issue only needs the change to the sort predicate, not this whole PR.

@vvereschaka
Copy link
Contributor

@vvereschaka That issue only needs the change to the sort predicate, not this whole PR.
got it, @arsenm is it possible to create a separate PR to fix the test failure? It could taka a lot of time for this PR to get merged. Thank you.

arsenm added a commit that referenced this pull request Jul 26, 2025
Extracted from #150192, this hopefully fixes occasional EXPENSIVE_CHECKS
failures.
arsenm added a commit that referenced this pull request Jul 26, 2025
Extracted from #150192, this hopefully fixes occasional EXPENSIVE_CHECKS
failures.
arsenm added a commit that referenced this pull request Jul 26, 2025
Extracted from #150192, this hopefully fixes occasional EXPENSIVE_CHECKS
failures.
arsenm added a commit that referenced this pull request Jul 26, 2025
Extracted from #150192, this hopefully fixes occasional EXPENSIVE_CHECKS
failures.
arsenm added a commit that referenced this pull request Jul 26, 2025
Extracted from #150192, this hopefully fixes occasional EXPENSIVE_CHECKS
failures.
arsenm added a commit that referenced this pull request Jul 26, 2025
Extracted from #150192, this hopefully fixes occasional EXPENSIVE_CHECKS
failures.
Base automatically changed from users/arsenm/object/use-stringset-preserved-symbols to main July 28, 2025 00:51
@arsenm arsenm force-pushed the users/arsenm/tablegen/emit-perfect-hash-table-runtime-libcalls branch from 75713d9 to 5819523 Compare July 28, 2025 00:52
@arsenm arsenm force-pushed the users/arsenm/tablegen/emit-perfect-hash-table-runtime-libcalls branch from dc7db82 to 7dd76ec Compare August 6, 2025 01:32
@arsenm
Copy link
Contributor Author

arsenm commented Aug 6, 2025

ping

1 similar comment
@arsenm
Copy link
Contributor Author

arsenm commented Aug 11, 2025

ping

@arsenm arsenm force-pushed the users/arsenm/tablegen/emit-perfect-hash-table-runtime-libcalls branch from 7dd76ec to d476610 Compare August 12, 2025 15:25
// functions.
//
// TODO: It may make more sense to split the search by string size more. There
// are a few outliers, most call names are small.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Splitting by string size reduces the size of the tables, makes the string comparisons cheaper, and potentially allows using a simpler hash function. Probably the primary cost here is memory accesses, though, not computation, so not sure that helps in practice.

I'd be interested to know the value of Collisions in practice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's 6

Copy link
Collaborator

Choose a reason for hiding this comment

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

That implies you're ending up with a really low effective load factor (at least 5 empty entries per filled entry). You can probably save a lot of space using linear probing instead of fixed 6-entry buckets.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just did a quick experiment trying to switch to linear probing; I can get the hit case to be slightly faster. The realistic miss cases are much, much slower so far

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed a bug, think this is going faster now. I'm leaning towards making this switch in a follow up PR

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The fastest set of options I've found uses a bigger table with linear probing, I'll post a new PR on top of #153210 after I've explored a few more configurations

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is another bug here, this is currently trying to re-hash the duplicate name cases running up the collision count unnecessarily. With this size factor the number is really 2

@arsenm arsenm force-pushed the users/arsenm/tablegen/emit-perfect-hash-table-runtime-libcalls branch from b8b7a46 to fb4e9c5 Compare August 13, 2025 07:50
Copy link
Contributor

@nikic nikic left a comment

Choose a reason for hiding this comment

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

Is all of this really necessary? Would dynamically computing the map once when constructing the Builder be good enough (or possibly at a higher level, if many builders get constructed)?

@arsenm
Copy link
Contributor Author

arsenm commented Aug 13, 2025

Is all of this really necessary? Would dynamically computing the map once when constructing the Builder be good enough (or possibly at a higher level, if many builders get constructed)?

I think it's much nicer to have a static table

@arsenm
Copy link
Contributor Author

arsenm commented Aug 13, 2025

I think it's much nicer to have a static table

Plus I think an ordinary map makes the multiple names resolve to different entries case more awkward

arsenm added 6 commits August 14, 2025 23:42
a961210 reverted a change
to use a binary search on the string name table because it
was too slow. This replaces it with a static string hash
table based on the known set of libcall names. Microbenchmarking
shows this is similarly fast to using DenseMap. It's possibly
slightly slower than using StringSet, though these aren't an
exact comparison. This also saves on the one time use construction
of the map, so it could be better in practice.

This search isn't simple set check, since it does find the
range of possible matches with the same name. There's also
an additional check for whether the current target supports
the name. The runtime constructed set doesn't require this,
since it only adds the symbols live for the target.

Followed algorithm from this post
http://0x80.pl/notesen/2023-04-30-lookup-in-strings.html

I'm also thinking the 2 special case global symbols should
just be added to RuntimeLibcalls. There are also other global
references emitted in the backend that aren't tracked; we probably
should just use this as a centralized database for all compiler
selected symbols.
@arsenm arsenm force-pushed the users/arsenm/tablegen/emit-perfect-hash-table-runtime-libcalls branch from fb4e9c5 to 8896713 Compare August 14, 2025 14:46
Copy link

github-actions bot commented Aug 14, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

Copy link
Collaborator

@efriedma-quic efriedma-quic left a comment

Choose a reason for hiding this comment

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

This is not really a "perfect hash" in the traditional sense; this is just constructing a conventional hashtable at compile-time with optimized table and bucket sizes. Maybe tweak the commit message a bit to reflect that.

Otherwise LGTM

@arsenm arsenm changed the title TableGen: Emit perfect hash function for runtime libcalls TableGen: Emit statically generated hash function for runtime libcalls Aug 15, 2025
@arsenm arsenm changed the title TableGen: Emit statically generated hash function for runtime libcalls TableGen: Emit statically generated hash table for runtime libcalls Aug 15, 2025
@arsenm arsenm changed the title TableGen: Emit statically generated hash table for runtime libcalls TableGen: Emit static hash table for runtime libcalls Aug 15, 2025
@arsenm arsenm merged commit 769a905 into main Aug 15, 2025
9 checks passed
@arsenm arsenm deleted the users/arsenm/tablegen/emit-perfect-hash-table-runtime-libcalls branch August 15, 2025 00:02
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.

5 participants