diff --git a/gdb/dwarf2/cooked-index.c b/gdb/dwarf2/cooked-index.c
index 6612585649f..9d3a4b03489 100644
--- a/gdb/dwarf2/cooked-index.c
+++ b/gdb/dwarf2/cooked-index.c
@@ -368,13 +368,11 @@ cooked_index_shard::handle_gnat_encoded_entry
       cooked_index_entry *last = (cooked_index_entry *) *slot;
       if (last == nullptr || last->per_cu != entry->per_cu)
 	{
-	  gdb::unique_xmalloc_ptr<char> new_name
-	    = make_unique_xstrndup (name.data (), name.length ());
+	  const char *new_name = m_names.insert (name);
 	  last = create (entry->die_offset, DW_TAG_module,
-			 IS_SYNTHESIZED, language_ada, new_name.get (), parent,
+			 IS_SYNTHESIZED, language_ada, new_name, parent,
 			 entry->per_cu);
 	  last->canonical = last->name;
-	  m_names.push_back (std::move (new_name));
 	  new_entries.push_back (last);
 	  *slot = last;
 	}
@@ -383,9 +381,7 @@ cooked_index_shard::handle_gnat_encoded_entry
     }
 
   entry->set_parent (parent);
-  auto new_canon = make_unique_xstrndup (tail.data (), tail.length ());
-  entry->canonical = new_canon.get ();
-  m_names.push_back (std::move (new_canon));
+  entry->canonical = m_names.insert (tail);
 }
 
 /* See cooked-index.h.  */
@@ -503,10 +499,7 @@ cooked_index_shard::finalize (const parent_map_map *parent_maps)
 	      if (canon_name == nullptr)
 		entry->canonical = entry->name;
 	      else
-		{
-		  entry->canonical = canon_name.get ();
-		  m_names.push_back (std::move (canon_name));
-		}
+		entry->canonical = m_names.insert (std::move (canon_name));
 	      *slot = entry;
 	    }
 	  else
@@ -526,7 +519,6 @@ cooked_index_shard::finalize (const parent_map_map *parent_maps)
   m_entries.insert (m_entries.end (), new_gnat_entries.begin (),
 		    new_gnat_entries.end ());
 
-  m_names.shrink_to_fit ();
   m_entries.shrink_to_fit ();
   std::sort (m_entries.begin (), m_entries.end (),
 	     [] (const cooked_index_entry *a, const cooked_index_entry *b)
diff --git a/gdb/dwarf2/cooked-index.h b/gdb/dwarf2/cooked-index.h
index f6586359770..c6911c23781 100644
--- a/gdb/dwarf2/cooked-index.h
+++ b/gdb/dwarf2/cooked-index.h
@@ -32,6 +32,7 @@
 #include "dwarf2/read.h"
 #include "dwarf2/parent-map.h"
 #include "gdbsupport/range-chain.h"
+#include "gdbsupport/string-set.h"
 #include "complaints.h"
 
 #if CXX_STD_THREAD
@@ -367,7 +368,7 @@ class cooked_index_shard
   /* The addrmap.  This maps address ranges to dwarf2_per_cu objects.  */
   addrmap_fixed *m_addrmap = nullptr;
   /* Storage for canonical names.  */
-  std::vector<gdb::unique_xmalloc_ptr<char>> m_names;
+  gdb::string_set m_names;
 };
 
 using cooked_index_shard_up = std::unique_ptr<cooked_index_shard>;
diff --git a/gdbsupport/string-set.h b/gdbsupport/string-set.h
new file mode 100644
index 00000000000..c2507454650
--- /dev/null
+++ b/gdbsupport/string-set.h
@@ -0,0 +1,129 @@
+/* String-interning set
+
+   Copyright (C) 2025 Free Software Foundation, Inc.
+
+   This file is part of GDB.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+#ifndef GDBSUPPORT_STRING_SET_H
+#define GDBSUPPORT_STRING_SET_H
+
+#include "gdbsupport/unordered_set.h"
+#include <string_view>
+
+namespace gdb
+{
+
+/* This is a string-interning set.  It manages storage for strings,
+   ensuring that just a single copy of a given string is kept.  The
+   underlying C string will remain valid for the lifetime of this
+   object.  */
+
+class string_set
+{
+public:
+
+  /* Insert STR into this set.  Returns a pointer to the interned
+     string.  */
+  const char *insert (const char *str)
+  {
+    /* We need to take the length to hash the string anyway, so it's
+       convenient to just wrap it here.  */
+    return insert (std::string_view (str));
+  }
+
+  /* An overload accepting a string view.  */
+  const char *insert (std::string_view str)
+  {
+    return m_set.insert (str).first->get ();
+  }
+
+  /* An overload that takes ownership of the string.  */
+  const char *insert (gdb::unique_xmalloc_ptr<char> str)
+  {
+    return m_set.insert (local_string (std::move (str))).first->get ();
+  }
+
+private:
+
+  /* The type of string we store.  Note that we do not store
+     std::string here to avoid the small-string optimization
+     invalidating a pointer on rehash.  */
+  struct local_string
+  {
+    explicit local_string (std::string_view str)
+      : contents (xstrndup (str.data (), str.size ())),
+	len (str.size ())
+    { }
+
+    explicit local_string (gdb::unique_xmalloc_ptr<char> str)
+      : contents (std::move (str)),
+	len (strlen (contents.get ()))
+    { }
+
+    const char *get () const
+    { return contents.get (); }
+
+    std::string_view as_view () const
+    { return std::string_view (contents.get (), len); }
+
+    /* \0-terminated string contents.  */
+    gdb::unique_xmalloc_ptr<char> contents;
+    /* Length of the string.  */
+    size_t len;
+  };
+
+  /* Equality object for the set.  */
+  struct str_eq
+  {
+    using is_transparent = void;
+
+    bool operator() (std::string_view lhs, const local_string &rhs)
+      const noexcept
+    {
+      return lhs == rhs.as_view ();
+    }
+
+    bool operator() (const local_string &lhs, const local_string &rhs)
+      const noexcept
+    {
+      return (*this) (lhs.as_view (), rhs);
+    }
+  };
+
+  /* Hash object for the set.  */
+  struct str_hash
+  {
+    using is_transparent = void;
+    using is_avalanching = void;
+
+    bool operator() (const local_string &rhs) const noexcept
+    {
+      return (*this) (rhs.as_view ());
+    }
+
+    bool operator() (std::string_view rhs) const noexcept
+    {
+      return ankerl::unordered_dense::hash<std::string_view> () (rhs);
+    }
+  };
+
+  /* The strings.  */
+  gdb::unordered_set<local_string, str_hash, str_eq> m_set;
+};
+
+}
+
+#endif /* GDBSUPPORT_STRING_SET_H */
