aboutsummaryrefslogtreecommitdiff
path: root/llvm/unittests/ADT/SmallVectorTest.cpp
diff options
context:
space:
mode:
authorDuncan P. N. Exon Smith <dexonsmith@apple.com>2021-01-15 13:53:02 -0800
committerDuncan P. N. Exon Smith <dexonsmith@apple.com>2021-01-15 14:27:48 -0800
commitceaf0110ff5e0c2de1f03d65d13703d34d0d5737 (patch)
treea259e61a6b0031387d21b57a03664649a5ad5bf9 /llvm/unittests/ADT/SmallVectorTest.cpp
parentceb3cdccd0fb597659147e0f538fdee91414541e (diff)
downloadllvm-ceaf0110ff5e0c2de1f03d65d13703d34d0d5737.zip
llvm-ceaf0110ff5e0c2de1f03d65d13703d34d0d5737.tar.gz
llvm-ceaf0110ff5e0c2de1f03d65d13703d34d0d5737.tar.bz2
Revert "Revert "ADT: Fix reference invalidation in SmallVector...""
This reverts commit 33be50daa9ce1074c3b423a4ab27c70c0722113a, effectively reapplying: - 260a856c2abcef49c7cb3bdcd999701db3e2af38 - 3043e5a5c33c4c871f4a1dfd621a8839f9a1f0b3 - 49142991a685bd427d7e877c29c77371dfb7634c ... with a fix to skip a call to `SmallVector::isReferenceToStorage()` when we know the parameter had been taken by value for small, POD-like `T`. See https://reviews.llvm.org/D93779 for the discussion on the revert. At a high-level, these commits fix reference invalidation in SmallVector's push_back, append, insert (one or N), and resize operations. For more details, please see the original commit messages. This commit fixes a bug that crept into `SmallVectorTemplateCommon::reserveForAndGetAddress()` during the review process after performance analysis was done. That function is now called `reserveForParamAndGetAddress()`, clarifying that it only works for parameter values. It uses that knowledge to bypass `SmallVector::isReferenceToStorage()` when `TakesParamByValue`. This is `constexpr` and avoids adding overhead for "small enough", trivially copyable `T`. Performance could potentially be tuned further by increasing the threshold for `TakesParamByValue`, which is currently defined as: ``` bool TakesParamByValue = sizeof(T) <= 2 * sizeof(void *); ``` in the POD-like version of SmallVectorTemplateBase (else, `false`). Differential Revision: https://reviews.llvm.org/D94800
Diffstat (limited to 'llvm/unittests/ADT/SmallVectorTest.cpp')
-rw-r--r--llvm/unittests/ADT/SmallVectorTest.cpp152
1 files changed, 124 insertions, 28 deletions
diff --git a/llvm/unittests/ADT/SmallVectorTest.cpp b/llvm/unittests/ADT/SmallVectorTest.cpp
index d97ab57..1f6c7db 100644
--- a/llvm/unittests/ADT/SmallVectorTest.cpp
+++ b/llvm/unittests/ADT/SmallVectorTest.cpp
@@ -53,6 +53,7 @@ public:
Constructable(Constructable && src) : constructed(true) {
value = src.value;
+ src.value = 0;
++numConstructorCalls;
++numMoveConstructorCalls;
}
@@ -74,6 +75,7 @@ public:
Constructable & operator=(Constructable && src) {
EXPECT_TRUE(constructed);
value = src.value;
+ src.value = 0;
++numAssignmentCalls;
++numMoveAssignmentCalls;
return *this;
@@ -1056,11 +1058,16 @@ protected:
return N;
}
+ template <class T> static bool isValueType() {
+ return std::is_same<T, typename VectorT::value_type>::value;
+ }
+
void SetUp() override {
SmallVectorTestBase::SetUp();
// Fill up the small size so that insertions move the elements.
- V.append({0, 0, 0});
+ for (int I = 0, E = NumBuiltinElts(V); I != E; ++I)
+ V.emplace_back(I + 1);
}
};
@@ -1074,39 +1081,84 @@ TYPED_TEST_CASE(SmallVectorReferenceInvalidationTest,
SmallVectorReferenceInvalidationTestTypes);
TYPED_TEST(SmallVectorReferenceInvalidationTest, PushBack) {
+ // Note: setup adds [1, 2, ...] to V until it's at capacity in small mode.
auto &V = this->V;
- (void)V;
-#if !defined(NDEBUG) && GTEST_HAS_DEATH_TEST
- EXPECT_DEATH(V.push_back(V.back()), this->AssertionMessage);
-#endif
+ int N = this->NumBuiltinElts(V);
+
+ // Push back a reference to last element when growing from small storage.
+ V.push_back(V.back());
+ EXPECT_EQ(N, V.back());
+
+ // Check that the old value is still there (not moved away).
+ EXPECT_EQ(N, V[V.size() - 2]);
+
+ // Fill storage again.
+ V.back() = V.size();
+ while (V.size() < V.capacity())
+ V.push_back(V.size() + 1);
+
+ // Push back a reference to last element when growing from large storage.
+ V.push_back(V.back());
+ EXPECT_EQ(int(V.size()) - 1, V.back());
}
TYPED_TEST(SmallVectorReferenceInvalidationTest, PushBackMoved) {
+ // Note: setup adds [1, 2, ...] to V until it's at capacity in small mode.
auto &V = this->V;
- (void)V;
-#if !defined(NDEBUG) && GTEST_HAS_DEATH_TEST
- EXPECT_DEATH(V.push_back(std::move(V.back())), this->AssertionMessage);
-#endif
+ int N = this->NumBuiltinElts(V);
+
+ // Push back a reference to last element when growing from small storage.
+ V.push_back(std::move(V.back()));
+ EXPECT_EQ(N, V.back());
+ if (this->template isValueType<Constructable>()) {
+ // Check that the value was moved (not copied).
+ EXPECT_EQ(0, V[V.size() - 2]);
+ }
+
+ // Fill storage again.
+ V.back() = V.size();
+ while (V.size() < V.capacity())
+ V.push_back(V.size() + 1);
+
+ // Push back a reference to last element when growing from large storage.
+ V.push_back(std::move(V.back()));
+
+ // Check the values.
+ EXPECT_EQ(int(V.size()) - 1, V.back());
+ if (this->template isValueType<Constructable>()) {
+ // Check the value got moved out.
+ EXPECT_EQ(0, V[V.size() - 2]);
+ }
}
TYPED_TEST(SmallVectorReferenceInvalidationTest, Resize) {
auto &V = this->V;
(void)V;
int N = this->NumBuiltinElts(V);
-#if !defined(NDEBUG) && GTEST_HAS_DEATH_TEST
- EXPECT_DEATH(V.resize(N + 1, V.back()), this->AssertionMessage);
-#endif
-
- // No assertion when shrinking, since the parameter isn't accessed.
- V.resize(N - 1, V.back());
+ V.resize(N + 1, V.back());
+ EXPECT_EQ(N, V.back());
+
+ // Resize to add enough elements that V will grow again. If reference
+ // invalidation breaks in the future, sanitizers should be able to catch a
+ // use-after-free here.
+ V.resize(V.capacity() + 1, V.front());
+ EXPECT_EQ(1, V.back());
}
TYPED_TEST(SmallVectorReferenceInvalidationTest, Append) {
auto &V = this->V;
(void)V;
-#if !defined(NDEBUG) && GTEST_HAS_DEATH_TEST
- EXPECT_DEATH(V.append(1, V.back()), this->AssertionMessage);
-#endif
+ V.append(1, V.back());
+ int N = this->NumBuiltinElts(V);
+ EXPECT_EQ(N, V[N - 1]);
+
+ // Append enough more elements that V will grow again. This tests growing
+ // when already in large mode.
+ //
+ // If reference invalidation breaks in the future, sanitizers should be able
+ // to catch a use-after-free here.
+ V.append(V.capacity() - V.size() + 1, V.front());
+ EXPECT_EQ(1, V.back());
}
TYPED_TEST(SmallVectorReferenceInvalidationTest, AppendRange) {
@@ -1150,28 +1202,72 @@ TYPED_TEST(SmallVectorReferenceInvalidationTest, AssignRange) {
}
TYPED_TEST(SmallVectorReferenceInvalidationTest, Insert) {
+ // Note: setup adds [1, 2, ...] to V until it's at capacity in small mode.
auto &V = this->V;
(void)V;
-#if !defined(NDEBUG) && GTEST_HAS_DEATH_TEST
- EXPECT_DEATH(V.insert(V.begin(), V.back()), this->AssertionMessage);
-#endif
+
+ // Insert a reference to the back (not at end() or else insert delegates to
+ // push_back()), growing out of small mode. Confirm the value was copied out
+ // (moving out Constructable sets it to 0).
+ V.insert(V.begin(), V.back());
+ EXPECT_EQ(int(V.size() - 1), V.front());
+ EXPECT_EQ(int(V.size() - 1), V.back());
+
+ // Fill up the vector again.
+ while (V.size() < V.capacity())
+ V.push_back(V.size() + 1);
+
+ // Grow again from large storage to large storage.
+ V.insert(V.begin(), V.back());
+ EXPECT_EQ(int(V.size() - 1), V.front());
+ EXPECT_EQ(int(V.size() - 1), V.back());
}
TYPED_TEST(SmallVectorReferenceInvalidationTest, InsertMoved) {
+ // Note: setup adds [1, 2, ...] to V until it's at capacity in small mode.
auto &V = this->V;
(void)V;
-#if !defined(NDEBUG) && GTEST_HAS_DEATH_TEST
- EXPECT_DEATH(V.insert(V.begin(), std::move(V.back())),
- this->AssertionMessage);
-#endif
+
+ // Insert a reference to the back (not at end() or else insert delegates to
+ // push_back()), growing out of small mode. Confirm the value was copied out
+ // (moving out Constructable sets it to 0).
+ V.insert(V.begin(), std::move(V.back()));
+ EXPECT_EQ(int(V.size() - 1), V.front());
+ if (this->template isValueType<Constructable>()) {
+ // Check the value got moved out.
+ EXPECT_EQ(0, V.back());
+ }
+
+ // Fill up the vector again.
+ while (V.size() < V.capacity())
+ V.push_back(V.size() + 1);
+
+ // Grow again from large storage to large storage.
+ V.insert(V.begin(), std::move(V.back()));
+ EXPECT_EQ(int(V.size() - 1), V.front());
+ if (this->template isValueType<Constructable>()) {
+ // Check the value got moved out.
+ EXPECT_EQ(0, V.back());
+ }
}
TYPED_TEST(SmallVectorReferenceInvalidationTest, InsertN) {
auto &V = this->V;
(void)V;
-#if !defined(NDEBUG) && GTEST_HAS_DEATH_TEST
- EXPECT_DEATH(V.insert(V.begin(), 2, V.back()), this->AssertionMessage);
-#endif
+
+ // Cover NumToInsert <= this->end() - I.
+ V.insert(V.begin() + 1, 1, V.back());
+ int N = this->NumBuiltinElts(V);
+ EXPECT_EQ(N, V[1]);
+
+ // Cover NumToInsert > this->end() - I, inserting enough elements that V will
+ // also grow again; V.capacity() will be more elements than necessary but
+ // it's a simple way to cover both conditions.
+ //
+ // If reference invalidation breaks in the future, sanitizers should be able
+ // to catch a use-after-free here.
+ V.insert(V.begin(), V.capacity(), V.front());
+ EXPECT_EQ(1, V.front());
}
TYPED_TEST(SmallVectorReferenceInvalidationTest, InsertRange) {