Elide transient heap allocation in absl::StrSplit ConvertToContainer<std::vector<std::string>> for <= 16 tokens.

When splitting into a std::vector<std::string>, absl::StrSplit at HEAD unconditionally materialized a temporary std::vector<absl::string_view> on the heap (16..256 B across TCMalloc size classes 16..256 for 1..16 tokens) solely to count elements before reserving space and constructing the returned vector.

For splits producing at most 16 tokens, stage the string views in the existing 16-element stack array (matching the std::vector<absl::string_view> specialization) and emplace directly into the result vector. This eliminates 1 transient heap allocation (-50% allocs/op) and reduces requested memory by up to -40% on small inputs while improving CPU latency by ~17.5% across benchmark regimes. Splits producing > 16 tokens spill to the existing heap path without changes.

PiperOrigin-RevId: 1005395175
diff --git a/absl/strings/internal/str_split_internal.h b/absl/strings/internal/str_split_internal.h
index c7cc087..d539ff5 100644
--- a/absl/strings/internal/str_split_internal.h
+++ b/absl/strings/internal/str_split_internal.h
@@ -410,6 +410,24 @@
     }
   };
 
+  struct raw_view {
+    const char* data;
+    size_t size;
+    operator absl::string_view() const {  // NOLINT(runtime/explicit)
+      return {data, size};
+    }
+  };
+
+  static size_t FillChunk(const_iterator& it, std::array<raw_view, 16>& ar) {
+    size_t index = 0;
+    do {
+      ar[index].data = it->data();
+      ar[index].size = it->size();
+      ++it;
+    } while (++index != ar.size() && !it.at_end());
+    return index;
+  }
+
   // Partial specialization for a std::vector<absl::string_view>.
   //
   // Optimized for the common case of splitting to a
@@ -420,22 +438,10 @@
                             absl::string_view, false> {
     std::vector<absl::string_view, A> operator()(
         const Splitter& splitter) const {
-      struct raw_view {
-        const char* data;
-        size_t size;
-        operator absl::string_view() const {  // NOLINT(runtime/explicit)
-          return {data, size};
-        }
-      };
       std::vector<absl::string_view, A> v;
       std::array<raw_view, 16> ar;
       for (auto it = splitter.begin(); !it.at_end();) {
-        size_t index = 0;
-        do {
-          ar[index].data = it->data();
-          ar[index].size = it->size();
-          ++it;
-        } while (++index != ar.size() && !it.at_end());
+        const size_t index = FillChunk(it, ar);
         // We static_cast index to a signed type to work around overzealous
         // compiler warnings about signedness.
         v.insert(v.end(), ar.begin(),
@@ -448,13 +454,32 @@
   // Partial specialization for a std::vector<std::string>.
   //
   // Optimized for the common case of splitting to a std::vector<std::string>.
-  // In this case we first split the results to a std::vector<absl::string_view>
-  // so the returned std::vector<std::string> can have space reserved to avoid
-  // std::string moves.
+  // For splits with at most 16 pieces, we construct the returned
+  // std::vector<std::string> directly from a stack-allocated array of views to
+  // avoid allocating a temporary heap std::vector<absl::string_view> while
+  // still reserving exact space to avoid std::string moves.
   template <typename A>
   struct ConvertToContainer<std::vector<std::string, A>, std::string, false> {
     std::vector<std::string, A> operator()(const Splitter& splitter) const {
-      const std::vector<absl::string_view> v = splitter;
+      std::array<raw_view, 16> ar;
+      auto it = splitter.begin();
+      if (it.at_end()) return std::vector<std::string, A>();
+      size_t index = FillChunk(it, ar);
+      if (it.at_end()) {
+        std::vector<std::string, A> result;
+        result.reserve(index);
+        for (size_t i = 0; i != index; ++i) {
+          result.emplace_back(ar[i].data, ar[i].size);
+        }
+        return result;
+      }
+      std::vector<absl::string_view> v(
+          ar.begin(), ar.begin() + static_cast<ptrdiff_t>(index));
+      while (!it.at_end()) {
+        index = FillChunk(it, ar);
+        v.insert(v.end(), ar.begin(),
+                 ar.begin() + static_cast<ptrdiff_t>(index));
+      }
       return std::vector<std::string, A>(v.begin(), v.end());
     }
   };
diff --git a/absl/strings/str_split_test.cc b/absl/strings/str_split_test.cc
index 7487e00..c2bcee3 100644
--- a/absl/strings/str_split_test.cc
+++ b/absl/strings/str_split_test.cc
@@ -1104,4 +1104,77 @@
                std::initializer_list<int>>::value));
 }
 
+template <typename T>
+struct CustomVectorAllocator : public std::allocator<T> {
+  CustomVectorAllocator() = default;
+  template <typename U>
+  explicit CustomVectorAllocator(const CustomVectorAllocator<U>&) noexcept {}
+  template <typename U>
+  struct rebind {
+    using other = CustomVectorAllocator<U>;
+  };
+};
+
+TEST(Split, VectorStringAllocationBoundaries) {
+  // Test 0 tokens (e.g. empty string with SkipEmpty)
+  {
+    std::vector<std::string> results =
+        absl::StrSplit("", ',', absl::SkipEmpty());
+    EXPECT_THAT(results, IsEmpty());
+  }
+
+  // Test 1 token (empty string without SkipEmpty)
+  {
+    std::vector<std::string> results = absl::StrSplit("", ',');
+    EXPECT_THAT(results, ElementsAre(""));
+  }
+
+  // Test 1 non-empty token
+  {
+    std::vector<std::string> results = absl::StrSplit("hello", ',');
+    EXPECT_THAT(results, ElementsAre("hello"));
+  }
+
+  // Test 2 tokens
+  {
+    std::vector<std::string> results = absl::StrSplit("a,b", ',');
+    EXPECT_THAT(results, ElementsAre("a", "b"));
+  }
+
+  // Test boundary token counts (15, 16 stack fast path; 17, 32, 33 heap spill
+  // path)
+  for (int num_tokens : {15, 16, 17, 32, 33}) {
+    std::string input_string;
+    std::vector<std::string> expected_tokens;
+    for (int token_index = 0; token_index < num_tokens; ++token_index) {
+      if (token_index > 0) input_string += ",";
+      input_string += std::to_string(token_index);
+      expected_tokens.push_back(std::to_string(token_index));
+    }
+    std::vector<std::string> results = absl::StrSplit(input_string, ',');
+    EXPECT_EQ(results, expected_tokens);
+  }
+
+  // Test with custom allocator across stack and heap spill paths
+  {
+    using CustomVector =
+        std::vector<std::string, CustomVectorAllocator<std::string>>;
+    CustomVector stack_results = absl::StrSplit("one,two,three", ',');
+    EXPECT_THAT(stack_results, ElementsAre("one", "two", "three"));
+
+    std::string input_string;
+    std::vector<std::string> expected_tokens;
+    for (int token_index = 0; token_index < 20; ++token_index) {
+      if (token_index > 0) input_string += ",";
+      input_string += std::to_string(token_index);
+      expected_tokens.push_back(std::to_string(token_index));
+    }
+    CustomVector heap_results = absl::StrSplit(input_string, ',');
+    EXPECT_EQ(heap_results.size(), 20);
+    for (size_t token_index = 0; token_index < 20; ++token_index) {
+      EXPECT_EQ(heap_results[token_index], expected_tokens[token_index]);
+    }
+  }
+}
+
 }  // namespace