[gn] Shard StringAtomSet to avoid lock contention

Previously, StringAtomSet used a single global std::mutex to protect the
global KeySet and Slab storage. On high-core-count machines, worker
threads frequently miss their thread-local cache for new strings across
thousands of files, causing intense lock contention on this single
mutex.

Profiling Chromium's //out/Default build generation with official GN
release build settings (Fuchsia Clang + libc++ + static libjemalloc.a +
LTO) showed that kernel futex waits on StringAtomSet::find() accounted
for over 70% of the total System CPU time.

This change shards StringAtomSet into 256 independent, cache-aligned
(alignas(64)) partitions indexed by the string hash's upper bits. In
addition, both StringAtomSet and ThreadLocalCache pre-insert
kEmptyString in their constructors so that empty strings hit directly
without slab allocation or extra branches in the lookup hot path.

Benchmark results (`gn gen out/Default` on Chromium, 5 runs with
hyperfine, official release build configuration):
- Wall-clock time: 2.755s ± 0.029s -> 2.262s ± 0.038s (-493ms / 1.22x faster)
- System CPU time: 14.079s -> 4.030s (-10.05s / -71.4% reduction!)
- User CPU time:   23.740s -> 22.889s (-0.85s)

Output ninja files remain bit-for-bit identical across all 14,639 files.

TAG=agy
CONV=4cb5d931-60a8-4f44-8632-3e8d9fb10a8d
Change-Id: I2d09d9f25c6c86482ef420282a41606308b8d8d4
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/26540
Commit-Queue: Takuto Ikuta <tikuta@google.com>
Reviewed-by: Matt Stark <msta@google.com>
diff --git a/src/gn/string_atom.cc b/src/gn/string_atom.cc
index 71f9b87..f9b3be7 100644
--- a/src/gn/string_atom.cc
+++ b/src/gn/string_atom.cc
@@ -25,17 +25,18 @@
 //    - a set of string pointers, corresponding to the known strings in the
 //      group.
 //
-//    - a mutex to ensure correct thread-safety.
+//    - sharded mutexes and sets to ensure thread-safety without global
+//      contention.
 //
-//    - a find() method that takes an std::string_view argument, and uses it
-//      to find a matching entry in the string tree. If none is available,
-//      a new std::string is allocated and its address inserted into the tree
-//      before being returned.
+//    - a find() method that takes an std::string_view argument and hash, and
+//      uses it to find a matching entry in the sharded string tree. If none is
+//      available, a new std::string is allocated and its address inserted into
+//      the tree before being returned.
 //
-// Because the mutex is a large bottleneck, each thread implements
-// its own local string pointer cache, and will only call StringAtomSet::find()
-// in case of a lookup miss. This is critical for good performance.
-//
+// Because a single global mutex is a massive bottleneck on multi-core machines,
+// the set is partitioned into 64 cache-aligned shards. Furthermore, each
+// thread implements its own local string pointer cache, and will only call
+// StringAtomSet::find() in case of a lookup miss.
 
 static const std::string kEmptyString;
 
@@ -107,46 +108,26 @@
 
 class StringAtomSet {
  public:
-  StringAtomSet() {
-    // Ensure kEmptyString is in our set while not being allocated
-    // from a slab. The end result is that find("") should always
-    // return this address.
-    //
-    // This allows the StringAtom() default initializer to use the same
-    // address directly, avoiding a table lookup.
-    size_t hash = set_.Hash("");
-    auto* node = set_.Lookup(hash, "");
-    set_.Insert(node, hash, &kEmptyString);
-  }
+  static constexpr size_t kShardBits = 8;  // 256 shards
+  static constexpr size_t kShardCount = 1 << kShardBits;
 
-#ifdef ASAN_ENABLED
-  ~StringAtomSet() {
-    if (!slabs_.empty()) {
-      Slab* last_slab = slabs_.back();
-      for (Slab* slab : slabs_) {
-        slab->destroy(slab == last_slab ? slab_index_ : kStringsPerSlab);
-        delete slab;
-      }
-    }
+  StringAtomSet() {
+    // Ensure find("") always returns &kEmptyString without a slab allocation.
+    // This allows the StringAtom() default initializer to use the same address
+    // directly without an extra branch in find().
+    size_t hash = KeySet::Hash("");
+    size_t shard_idx =
+        (hash >> (sizeof(size_t) * 8 - kShardBits)) & (kShardCount - 1);
+    auto& shard = shards_[shard_idx];
+    auto* node = shard.set_.Lookup(hash, "");
+    shard.set_.Insert(node, hash, &kEmptyString);
   }
-#endif
 
   // Find the unique constant string pointer for |key|.
-  const std::string* find(std::string_view key) {
-    std::lock_guard<std::mutex> lock(mutex_);
-    size_t hash = set_.Hash(key);
-    auto* node = set_.Lookup(hash, key);
-    if (node->key)
-      return node->key;
-
-    // Allocate new string, insert its address in the set.
-    if (slab_index_ >= kStringsPerSlab) {
-      slabs_.push_back(new Slab());
-      slab_index_ = 0;
-    }
-    std::string* result = slabs_.back()->init(slab_index_++, key);
-    set_.Insert(node, hash, result);
-    return result;
+  const std::string* find(std::string_view key, size_t hash) {
+    size_t shard_idx =
+        (hash >> (sizeof(size_t) * 8 - kShardBits)) & (kShardCount - 1);
+    return shards_[shard_idx].find(key, hash);
   }
 
  private:
@@ -189,10 +170,53 @@
     StringStorage items_[kStringsPerSlab];
   };
 
-  std::mutex mutex_;
-  KeySet set_;
-  std::vector<Slab*> slabs_;
-  unsigned int slab_index_ = kStringsPerSlab;
+  // Align each Shard to a 64-byte cache line boundary to prevent false sharing
+  // between adjacent shards in the std::array when multiple threads lock and
+  // modify different shards concurrently.
+  struct alignas(64) Shard {
+    std::mutex mutex_;
+    KeySet set_;
+    std::vector<Slab*> slabs_;
+    unsigned int slab_index_ = kStringsPerSlab;
+
+#ifdef ASAN_ENABLED
+    ~Shard() {
+      if (!slabs_.empty()) {
+        Slab* last_slab = slabs_.back();
+        for (Slab* slab : slabs_) {
+          slab->destroy(slab == last_slab ? slab_index_ : kStringsPerSlab);
+          delete slab;
+        }
+      }
+    }
+#endif
+
+    const std::string* find(std::string_view key, size_t hash) {
+      std::lock_guard<std::mutex> lock(mutex_);
+
+      auto* node = set_.Lookup(hash, key);
+      if (node->key)
+        return node->key;
+
+      if (slab_index_ >= kStringsPerSlab) {
+        slabs_.push_back(new Slab());
+        slab_index_ = 0;
+      }
+      std::string* result = slabs_.back()->init(slab_index_++, key);
+      set_.Insert(node, hash, result);
+      return result;
+    }
+#if defined(_MSC_VER)
+#pragma warning(push)
+// C4324: structure was padded due to alignment specifier.
+#pragma warning(disable : 4324)
+#endif
+  };
+#if defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+
+  std::array<Shard, kShardCount> shards_;
 };
 
 StringAtomSet& GetStringAtomSet() {
@@ -204,6 +228,12 @@
 // without taking any mutex in most cases.
 class ThreadLocalCache {
  public:
+  ThreadLocalCache() {
+    size_t hash = local_set_.Hash("");
+    auto* node = local_set_.Lookup(hash, "");
+    local_set_.Insert(node, hash, &kEmptyString);
+  }
+
   // Find the unique constant string pointer for |key| in this cache,
   // and fallback to the global one in case of a miss.
   KeyType find(std::string_view key) {
@@ -212,7 +242,7 @@
     if (node->key)
       return node->key;
 
-    KeyType result = GetStringAtomSet().find(key);
+    KeyType result = GetStringAtomSet().find(key, hash);
     local_set_.Insert(node, hash, result);
     return result;
   }