Skip to content

Conversation

@kuhar
Copy link
Member

@kuhar kuhar commented Dec 12, 2025

Calling reserve in a loop can prevent amortized constant time appends when the container doesn't round up the capacity.

@llvmbot
Copy link
Member

llvmbot commented Dec 12, 2025

@llvm/pr-subscribers-llvm-adt

Author: Jakub Kuderski (kuhar)

Changes

Calling reserve in a loop can prevent amortized constant time appends when the container doesn't round up the capacity.


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

2 Files Affected:

  • (modified) llvm/include/llvm/ADT/STLExtras.h (+11-1)
  • (modified) llvm/unittests/ADT/STLExtrasTest.cpp (+35)
diff --git a/llvm/include/llvm/ADT/STLExtras.h b/llvm/include/llvm/ADT/STLExtras.h
index 3c18aae1b519c..409cd7b60fa00 100644
--- a/llvm/include/llvm/ADT/STLExtras.h
+++ b/llvm/include/llvm/ADT/STLExtras.h
@@ -2152,7 +2152,17 @@ void append_range(Container &C, Range &&R) {
 /// Appends all `Values` to container `C`.
 template <typename Container, typename... Args>
 void append_values(Container &C, Args &&...Values) {
-  C.reserve(range_size(C) + sizeof...(Args));
+  if (size_t InitialSize = range_size(C); InitialSize == 0) {
+    // Only reserve if the container is empty. Reserving on a non-empty
+    // container may interfere with the exponential growth strategy, if the
+    // container does not round up the capacity. Consider `append_values` called
+    // repeatedly in a loop: each call would reserve exactly `size + N`, causing
+    // the capacity to grow linearly (e.g., 100 -> 105 -> 110 -> ...) instead of
+    // exponentially (e.g., 100 -> 200 -> ...). Linear growth turns the
+    // amortized O(1) append into O(n) because every few insertions trigger a
+    // reallocation and copy of all elements.
+    C.reserve(InitialSize + sizeof...(Args));
+  }
   // Append all values one by one.
   ((void)C.insert(C.end(), std::forward<Args>(Values)), ...);
 }
diff --git a/llvm/unittests/ADT/STLExtrasTest.cpp b/llvm/unittests/ADT/STLExtrasTest.cpp
index e356f6b540568..ae79b852aecf5 100644
--- a/llvm/unittests/ADT/STLExtrasTest.cpp
+++ b/llvm/unittests/ADT/STLExtrasTest.cpp
@@ -701,6 +701,41 @@ TEST(STLExtrasTest, AppendValues) {
   EXPECT_THAT(Set, UnorderedElementsAre(1, 2, 3));
 }
 
+TEST(STLExtrasTest, AppendValuesReserve) {
+  // A vector wrapper that tracks reserve() calls.
+  struct TrackedVector : std::vector<int> {
+    using std::vector<int>::vector;
+    size_t LastReservedSize = 0;
+    unsigned ReserveCallCount = 0;
+
+    void reserve(size_t N) {
+      LastReservedSize = N;
+      ++ReserveCallCount;
+      std::vector<int>::reserve(N);
+    }
+  };
+
+  // When empty, reserve should be called.
+  TrackedVector Empty;
+  append_values(Empty, 1, 2, 3);
+  EXPECT_EQ(Empty.ReserveCallCount, 1u);
+  EXPECT_EQ(Empty.LastReservedSize, 3u);
+  EXPECT_THAT(Empty, ElementsAre(1, 2, 3));
+
+  // When non-empty, reserve should NOT be called to avoid preventing
+  // exponential growth.
+  TrackedVector NonEmpty = {1, 2};
+  append_values(NonEmpty, 3, 4);
+  EXPECT_EQ(NonEmpty.ReserveCallCount, 0u);
+  EXPECT_THAT(NonEmpty, ElementsAre(1, 2, 3, 4));
+
+  // Appending more values to a now non-empty container should still not
+  // reserve.
+  append_values(Empty, 4, 5);
+  EXPECT_EQ(Empty.ReserveCallCount, 1u); // Still 1 from initial call.
+  EXPECT_THAT(Empty, ElementsAre(1, 2, 3, 4, 5));
+}
+
 TEST(STLExtrasTest, ADLTest) {
   some_namespace::some_struct s{{1, 2, 3, 4, 5}, ""};
   some_namespace::some_struct s2{{2, 4, 6, 8, 10}, ""};

template <typename Container, typename... Args>
void append_values(Container &C, Args &&...Values) {
C.reserve(range_size(C) + sizeof...(Args));
if (size_t InitialSize = range_size(C); InitialSize == 0) {
Copy link
Contributor

@nikic nikic Dec 13, 2025

Choose a reason for hiding this comment

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

Side note: This code should be using llvm::size() and not range_size(). It makes no sense to do a full container scan with std::distance() in this context.

If we have any usages where size() is not available, those should just not attempt to reserve() at all.

Copy link
Member Author

@kuhar kuhar Dec 13, 2025

Choose a reason for hiding this comment

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

I got back to this line of work recently and think we need some fast size function that allows for both random access iterators and containers with .size(), so a combination of std::size (arrays and .size()) and llvm::size (random access only).

Here specifically, I'm not aware of any containers that have .insert() but don't have (fast) .size() (almost every usage is with vector and small vector), so ACK but I'm going to address this separately.

@kuhar kuhar merged commit 2490bb7 into llvm:main Dec 13, 2025
10 checks passed
@llvm-ci
Copy link
Collaborator

llvm-ci commented Dec 13, 2025

LLVM Buildbot has detected a new failure on builder clang-m68k-linux-cross running on suse-gary-m68k-cross while building llvm at step 5 "ninja check 1".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/27/builds/20301

Here is the relevant piece of the build log for the reference
Step 5 (ninja check 1) failure: stage 1 checked (failure)
...
[191/1237] Building CXX object tools/clang/tools/extra/clangd/unittests/CMakeFiles/ClangdTests.dir/TUSchedulerTests.cpp.o
/var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/llvm/clang-tools-extra/clangd/unittests/TUSchedulerTests.cpp: In member function ‘virtual void clang::clangd::{anonymous}::TUSchedulerTests_PublishWithStalePreamble_Test::TestBody()::BlockPreambleThread::onPreambleAST(clang::clangd::PathRef, llvm::StringRef, clang::clangd::CapturedASTCtx, std::shared_ptr<const clang::include_cleaner::PragmaIncludes>)’:
/var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/llvm/clang-tools-extra/clangd/unittests/TUSchedulerTests.cpp:1219:10: warning: suggest explicit braces to avoid ambiguous ‘else’ [-Wdangling-else]
 1219 |       if (BuildBefore)
      |          ^
[192/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/DependencyScanning/DependencyScanningWorkerTest.cpp.o
[193/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/ValueTest.cpp.o
[194/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/MapLatticeTest.cpp.o
[195/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/StaticAnalyzer/AnalyzerOptionsTest.cpp.o
[196/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/IntervalPartitionTest.cpp.o
FAILED: tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/IntervalPartitionTest.cpp.o 
/usr/bin/c++ -DGTEST_HAS_RTTI=0 -DLLVM_BUILD_STATIC -D_DEBUG -D_GLIBCXX_ASSERTIONS -D_GLIBCXX_USE_CXX11_ABI=1 -D_GNU_SOURCE -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS -I/var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/stage1/tools/clang/unittests -I/var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/llvm/clang/unittests -I/var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/llvm/clang/include -I/var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/stage1/tools/clang/include -I/var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/stage1/include -I/var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/llvm/llvm/include -I/var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/llvm/clang/unittests/Tooling -I/var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/llvm/third-party/unittest/googletest/include -I/var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/llvm/third-party/unittest/googlemock/include -fPIC -fno-semantic-interposition -fvisibility-inlines-hidden -Werror=date-time -Wall -Wextra -Wno-unused-parameter -Wwrite-strings -Wcast-qual -Wno-missing-field-initializers -pedantic -Wno-long-long -Wimplicit-fallthrough -Wno-uninitialized -Wno-nonnull -Wno-class-memaccess -Wno-dangling-reference -Wno-redundant-move -Wno-pessimizing-move -Wno-array-bounds -Wno-stringop-overread -Wno-noexcept-type -Wdelete-non-virtual-dtor -Wsuggest-override -Wno-comment -Wno-misleading-indentation -Wctad-maybe-unsupported -fdiagnostics-color -ffunction-sections -fdata-sections -fno-common -Woverloaded-virtual -O3 -DNDEBUG -std=c++17  -Wno-variadic-macros -fno-exceptions -funwind-tables -fno-rtti -UNDEBUG -Wno-suggest-override -MD -MT tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/IntervalPartitionTest.cpp.o -MF tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/IntervalPartitionTest.cpp.o.d -o tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/IntervalPartitionTest.cpp.o -c /var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/llvm/clang/unittests/Analysis/IntervalPartitionTest.cpp
c++: fatal error: Killed signal terminated program cc1plus
compilation terminated.
[197/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/CFGMatchSwitchTest.cpp.o
[198/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/CachedConstAccessorsLatticeTest.cpp.o
[199/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/RecordOpsTest.cpp.o
[200/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/StaticAnalyzer/BlockEntranceCallbackTest.cpp.o
[201/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/StaticAnalyzer/APSIntTypeTest.cpp.o
[202/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/CFGTest.cpp.o
[203/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/SimplifyConstraintsTest.cpp.o
[204/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/ASTOpsTest.cpp.o
[205/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/SignAnalysisTest.cpp.o
[206/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/DeterminismTest.cpp.o
[207/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/LifetimeSafetyTest.cpp.o
/var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/llvm/clang/unittests/Analysis/LifetimeSafetyTest.cpp: In instantiation of ‘bool clang::lifetimes::internal::{anonymous}::AreLiveAtImplMatcherP2<Annotation_type, ConfFilter_type>::gmock_Impl<arg_type>::MatchAndExplain(const arg_type&, testing::MatchResultListener*) const [with arg_type = const clang::lifetimes::internal::{anonymous}::OriginsInfo&; Annotation_type = const char*; ConfFilter_type = clang::lifetimes::internal::{anonymous}::LivenessKindFilter]’:
/var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/llvm/clang/unittests/Analysis/LifetimeSafetyTest.cpp:304:1:   required from here
  304 | MATCHER_P2(AreLiveAtImpl, Annotation, ConfFilter, "") {
/var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/llvm/clang/unittests/Analysis/LifetimeSafetyTest.cpp:314:19: warning: loop variable ‘<structured bindings>’ creates a copy from type ‘const std::pair<clang::lifetimes::internal::utils::ID<clang::lifetimes::internal::OriginTag>, clang::lifetimes::internal::LivenessKind>’ [-Wrange-loop-construct]
  314 |   for (const auto [OID, ActualConfidence] : ActualLiveSetOpt.value()) {
      |                   ^~~~~~~~~~~~~~~~~~~~~~~
/var/lib/buildbot/workers/suse-gary-m68k-cross/clang-m68k-linux-cross/llvm/clang/unittests/Analysis/LifetimeSafetyTest.cpp:314:19: note: use reference type to prevent copying
  314 |   for (const auto [OID, ActualConfidence] : ActualLiveSetOpt.value()) {
      |                   ^~~~~~~~~~~~~~~~~~~~~~~
      |                   &
[208/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/ChromiumCheckModelTest.cpp.o
[209/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/CFGDominatorTree.cpp.o
[210/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/WatchedLiteralsSolverTest.cpp.o
[211/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/SmartPointerAccessorCachingTest.cpp.o
[212/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/DebugSupportTest.cpp.o
[213/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/TestingSupport.cpp.o
[214/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/LoggerTest.cpp.o
[215/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/MatchSwitchTest.cpp.o
[216/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/DataflowEnvironmentTest.cpp.o
[217/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/TransferBranchTest.cpp.o
[218/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/UncheckedStatusOrAccessModelTest.cpp.o
[219/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/ExprMutationAnalyzerTest.cpp.o
[220/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/TestingSupportTest.cpp.o
[221/1237] Building CXX object tools/clang/unittests/CMakeFiles/AllClangUnitTests.dir/Analysis/FlowSensitive/SingleVarConstantPropagationTest.cpp.o

anonymouspc pushed a commit to anonymouspc/llvm that referenced this pull request Dec 15, 2025
…2109)

Calling reserve in a loop can prevent amortized constant time appends
when the container doesn't round up the capacity.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants