QList: re-use the prepend buffer, if any, on assign()

Task-number: QTBUG-106196
Change-Id: I62d8610529cab528ae1b114d29707133b4fc28dc
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
This commit is contained in:
Marc Mutz 2023-05-10 18:09:57 +02:00
parent 06830bd78d
commit 782ccc6de5
2 changed files with 138 additions and 14 deletions

View File

@ -8,6 +8,7 @@
#include <QtCore/qcontainertools_impl.h>
#include <QtCore/q20functional.h>
#include <QtCore/q20memory.h>
QT_BEGIN_NAMESPACE
@ -317,9 +318,7 @@ public:
if constexpr (IsFwdIt) {
const qsizetype n = std::distance(first, last);
// Use of freeSpaceAtBegin() isn't, yet, implemented.
const auto usableCapacity = constAllocatedCapacity() - freeSpaceAtBegin();
if (needsDetach() || n > usableCapacity) {
if (needsDetach() || n > constAllocatedCapacity()) {
QArrayDataPointer allocated(Data::allocate(detachCapacity(n)));
swap(allocated);
}
@ -329,8 +328,48 @@ public:
// We don't want to copy data that we know we'll overwrite
}
auto dst = begin();
auto offset = freeSpaceAtBegin();
const auto capacityBegin = begin() - offset;
const auto prependBufferEnd = begin();
if constexpr (!std::is_nothrow_constructible_v<T, decltype(proj(*first))>) {
// If construction can throw, and we have freeSpaceAtBegin(),
// it's easiest to just clear the container and start fresh.
// The alternative would be to keep track of two active, disjoint ranges.
if (offset) {
(*this)->truncate(0);
setBegin(capacityBegin);
offset = 0;
}
}
auto dst = capacityBegin;
const auto dend = end();
if (offset) { // avoids dead stores
setBegin(capacityBegin); // undo prepend optimization
// By construction, the following loop is nothrow!
// (otherwise, we can't reach here)
// Assumes InputIterator operations don't throw.
// (but we can't statically assert that, as these operations
// have preconditons, so typically aren't noexcept)
while (true) {
if (dst == prependBufferEnd) { // ran out of prepend buffer space
size += offset;
// we now have a contiguous buffer, continue with the main loop:
break;
}
if (first == last) { // ran out of elements to assign
std::destroy(prependBufferEnd, dend);
size = dst - begin();
return;
}
q20::construct_at(dst, proj(*first)); // construct element in prepend buffer
++dst;
++first;
}
}
while (true) {
if (first == last) { // ran out of elements to assign
std::destroy(dst, dend);

View File

@ -234,6 +234,12 @@ private slots:
void assignInt() const { assign<int>(); }
void assignMovable() const { assign<Movable>(); }
void assignCustom() const { assign<Custom>(); }
void assignUsesPrependBuffer_int_data() { assignUsesPrependBuffer_data(); }
void assignUsesPrependBuffer_int() const { assignUsesPrependBuffer<int>(); }
void assignUsesPrependBuffer_Movable_data() { assignUsesPrependBuffer_data(); }
void assignUsesPrependBuffer_Movable() const { assignUsesPrependBuffer<Movable>(); }
void assignUsesPrependBuffer_Custom_data() { assignUsesPrependBuffer_data(); }
void assignUsesPrependBuffer_Custom() const { assignUsesPrependBuffer<Custom>(); }
void at() const;
void capacityInt() const { capacity<int>(); }
void capacityMovable() const { capacity<Movable>(); }
@ -400,6 +406,8 @@ private:
template<typename T> void add() const;
template<typename T> void append() const;
template<typename T> void assign() const;
void assignUsesPrependBuffer_data() const;
template<typename T> void assignUsesPrependBuffer() const;
template<typename T> void assignFromInitializerList() const;
template<typename T> void capacity() const;
template<typename T> void clear() const;
@ -796,21 +804,98 @@ void tst_QList::assign() const
QVERIFY(!myvecCopy.isSharedWith(myvec));
QCOMPARE(myvecCopy, QList<T>() << T_FOO << T_FOO);
}
}
inline namespace Scenarios {
Q_NAMESPACE
enum ListState {
UnsharedList,
SharedList,
};
Q_ENUM_NS(ListState)
enum RelationWithPrependBuffer {
FitsIntoFreeSpaceAtBegin,
FitsFreeSpaceAtBeginExactly,
ExceedsFreeSpaceAtBegin,
FitsFreeSpaceAtBeginPlusSizeExactly,
FullCapacity,
};
Q_ENUM_NS(RelationWithPrependBuffer)
} // namespace Scenarios
void tst_QList::assignUsesPrependBuffer_data() const
{
QTest::addColumn<ListState>("listState");
QTest::addColumn<RelationWithPrependBuffer>("relationWithPrependBuffer");
const auto sme = QMetaEnum::fromType<ListState>();
const auto rme = QMetaEnum::fromType<RelationWithPrependBuffer>();
for (int i = 0, s = sme.value(i); s != -1; s = sme.value(++i)) {
for (int j = 0, r = rme.value(j); r != -1; r = rme.value(++j)) {
QTest::addRow("%s-%s", sme.key(i), rme.key(j))
<< ListState(s) << RelationWithPrependBuffer(r);
}
}
}
template <typename T>
void tst_QList::assignUsesPrependBuffer() const
{
QFETCH(const ListState, listState);
QFETCH(const RelationWithPrependBuffer, relationWithPrependBuffer);
const auto capBegin = [](const QList<T> &l) {
return l.begin() - l.d.freeSpaceAtBegin();
};
const auto capEnd = [](const QList<T> &l) {
return l.end() + l.d.freeSpaceAtEnd();
};
TST_QLIST_CHECK_LEAKS(T)
{
// Test the prepend optimization.
QList<T> withFreeSpaceAtBegin(16, T_FOO);
// try at most 100 times to create freeSpaceAtBegin():
for (int i = 0; i < 100 && !withFreeSpaceAtBegin.d.freeSpaceAtBegin(); ++i)
for (int i = 0; i < 100 && withFreeSpaceAtBegin.d.freeSpaceAtBegin() < 2; ++i)
withFreeSpaceAtBegin.prepend(T_FOO);
QCOMPARE_GT(withFreeSpaceAtBegin.d.freeSpaceAtBegin(), 0);
const auto oldData = withFreeSpaceAtBegin.constData();
std::vector<T> v(withFreeSpaceAtBegin.capacity(), T_BAR);
withFreeSpaceAtBegin.assign(v.begin(), v.end());
QCOMPARE_EQ(withFreeSpaceAtBegin.d.freeSpaceAtBegin(), 0);
// TODO: Check for equality after the prepend optimization
// the following test checks that we didn't reallocate, but re-used the prepend buffer
QEXPECT_FAIL("","Use of freeSpaceAtBegin() isn't, yet, implemented", Continue);
QVERIFY(QtPrivate::q_points_into_range(oldData, withFreeSpaceAtBegin));
QCOMPARE_GT(withFreeSpaceAtBegin.d.freeSpaceAtBegin(), 1);
auto c = [&] {
switch (listState) {
case UnsharedList: return std::move(withFreeSpaceAtBegin);
case SharedList: return withFreeSpaceAtBegin;
}
Q_UNREACHABLE_RETURN(withFreeSpaceAtBegin);
}();
const auto n = [&] () -> qsizetype {
switch (relationWithPrependBuffer) {
case FitsIntoFreeSpaceAtBegin:
return qsizetype(1);
case FitsFreeSpaceAtBeginExactly:
return c.d.freeSpaceAtBegin();
case ExceedsFreeSpaceAtBegin:
return c.d.freeSpaceAtBegin() + 1;
case FitsFreeSpaceAtBeginPlusSizeExactly:
return c.d.freeSpaceAtBegin() + c.size();
case FullCapacity:
return c.capacity();
};
Q_UNREACHABLE_RETURN(0);
}();
const auto oldCapBegin = capBegin(c);
const auto oldCapEnd = capEnd(c);
const std::vector v(n, T_BAR);
c.assign(v.begin(), v.end());
QCOMPARE_EQ(c.d.freeSpaceAtBegin(), 0); // we used the prepend-buffer
if (listState != SharedList) {
// check that we didn't reallocate
QCOMPARE_EQ(capBegin(c), oldCapBegin);
QCOMPARE_EQ(capEnd(c), oldCapEnd);
}
}
}