Make HTTP header name a variant / union for performance

This saves memory and can speed up performance with
well-known headers.

The change consists of:
- Change the internal type of 'name' to a std::variant
  capable of holding either WellKnownHeader-enum, or a QBA.
- Accordingly, add an equality operator.
- When headers are added (append, insert, replace) then
  use WellKnownHeader as storage type when possible;
  either use the function parameter directly if a WellKnownHeader
  overload was used, or check if the provided string can
  be converted to a WellKnownHeader.
- Convert other functions to use a more performant
  lookup/comparisons.

Fixes: QTBUG-122020
Change-Id: If2452f6edc497547246fb4ddbace384e39c26c5e
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
(cherry picked from commit 0c05d2b43ec5ab29efc3db2718289a5600da754c)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
Juha Vuolle 2024-02-15 08:13:45 +02:00 committed by Qt Cherry-pick Bot
parent 6c82802a95
commit f413e71984

View File

@ -5,6 +5,7 @@
#include <private/qoffsetstringarray_p.h> #include <private/qoffsetstringarray_p.h>
#include <QtCore/qcompare.h>
#include <QtCore/qhash.h> #include <QtCore/qhash.h>
#include <QtCore/qloggingcategory.h> #include <QtCore/qloggingcategory.h>
#include <QtCore/qmap.h> #include <QtCore/qmap.h>
@ -13,6 +14,7 @@
#include <q20algorithm.h> #include <q20algorithm.h>
#include <string_view> #include <string_view>
#include <variant>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
@ -683,13 +685,86 @@ static QByteArray normalizedName(QAnyStringView name)
return name.visit([](auto name){ return fieldToByteArray(name); }).toLower(); return name.visit([](auto name){ return fieldToByteArray(name); }).toLower();
} }
struct HeaderName
{
explicit HeaderName(QHttpHeaders::WellKnownHeader name) : data(name)
{
}
explicit HeaderName(QAnyStringView name)
{
const auto nname = normalizedName(name);
if (auto h = HeaderName::toWellKnownHeader(nname))
data = *h;
else
data = std::move(nname);
}
// Returns an enum corresponding with the 'name' if possible. Uses binary search (O(logN)).
// The function doesn't normalize the data; needs to be done by the caller if needed
static std::optional<QHttpHeaders::WellKnownHeader> toWellKnownHeader(QByteArrayView name) noexcept
{
auto indexesBegin = std::cbegin(orderedHeaderNameIndexes);
auto indexesEnd = std::cend(orderedHeaderNameIndexes);
auto result = std::lower_bound(indexesBegin, indexesEnd, name, ByIndirectHeaderName{});
if (result != indexesEnd && name == headerNames[*result])
return static_cast<QHttpHeaders::WellKnownHeader>(*result);
return std::nullopt;
}
QByteArrayView asView() const noexcept
{
return std::visit([](const auto &arg) -> QByteArrayView {
using T = decltype(arg);
if constexpr (std::is_same_v<T, const QByteArray &>)
return arg;
else if constexpr (std::is_same_v<T, const QHttpHeaders::WellKnownHeader &>)
return headerNames.viewAt(qToUnderlying(arg));
else
static_assert(QtPrivate::type_dependent_false<T>());
}, data);
}
QByteArray asByteArray() const noexcept
{
return std::visit([](const auto &arg) -> QByteArray {
using T = decltype(arg);
if constexpr (std::is_same_v<T, const QByteArray &>) {
return arg;
} else if constexpr (std::is_same_v<T, const QHttpHeaders::WellKnownHeader &>) {
const auto view = headerNames.viewAt(qToUnderlying(arg));
return QByteArray::fromRawData(view.constData(), view.size());
} else {
static_assert(QtPrivate::type_dependent_false<T>());
}
}, data);
}
private:
// Store the data as 'enum' whenever possible; more performant, and comparison relies on that
std::variant<QHttpHeaders::WellKnownHeader, QByteArray> data;
friend bool comparesEqual(const HeaderName &lhs, const HeaderName &rhs) noexcept
{
// Here we compare two std::variants, which will return false if the types don't match.
// That is beneficial here because we avoid unnecessary comparisons; but it also means
// we must always store the data as WellKnownHeader when possible (in other words, if
// we get a string that is mappable to a WellKnownHeader). To guard against accidental
// misuse, the 'data' is private and the constructors must be used.
return lhs.data == rhs.data;
}
Q_DECLARE_EQUALITY_COMPARABLE(HeaderName)
};
// A clarification on case-sensitivity: // A clarification on case-sensitivity:
// - Header *names* are case-insensitive; Content-Type and content-type are considered equal // - Header *names* are case-insensitive; Content-Type and content-type are considered equal
// - Header *values* are case-sensitive // - Header *values* are case-sensitive
// (In addition, the HTTP/2 and HTTP/3 standards mandate that all headers must be lower-cased when // (In addition, the HTTP/2 and HTTP/3 standards mandate that all headers must be lower-cased when
// encoded into transmission) // encoded into transmission)
struct Header { struct Header {
QByteArray name; HeaderName name;
QByteArray value; QByteArray value;
private: private:
@ -699,11 +774,21 @@ private:
} }
}; };
auto headerNameMatches(const HeaderName &name)
{
return [&name](const Header &header) { return header.name == name; };
}
class QHttpHeadersPrivate : public QSharedData class QHttpHeadersPrivate : public QSharedData
{ {
public: public:
QHttpHeadersPrivate() = default; QHttpHeadersPrivate() = default;
// The 'Self' is supplied as parameter to static functions so that
// we can define common methods which 'detach()' the private itself.
using Self = QExplicitlySharedDataPointer<QHttpHeadersPrivate>;
static void removeAll(Self &d, const HeaderName &name);
QList<Header> headers; QList<Header> headers;
}; };
@ -718,6 +803,21 @@ template <> void QExplicitlySharedDataPointer<QHttpHeadersPrivate>::detach()
} }
} }
void QHttpHeadersPrivate::removeAll(Self &d, const HeaderName &name)
{
const auto it = std::find_if(d->headers.cbegin(), d->headers.cend(), headerNameMatches(name));
if (it != d->headers.cend()) {
// Found something to remove, calculate offset so we can proceed from the match-location
const auto matchOffset = it - d->headers.cbegin();
d.detach();
// Rearrange all matches to the end and erase them
d->headers.erase(std::remove_if(d->headers.begin() + matchOffset, d->headers.end(),
headerNameMatches(name)),
d->headers.end());
}
}
/*! /*!
Creates a new QHttpHeaders object. Creates a new QHttpHeaders object.
*/ */
@ -827,7 +927,7 @@ QDebug operator<<(QDebug debug, const QHttpHeaders &headers)
debug << "headers = "; debug << "headers = ";
const char *separator = ""; const char *separator = "";
for (const auto &h : headers.d->headers) { for (const auto &h : headers.d->headers) {
debug << separator << h.name << ':' << h.value; debug << separator << h.name.asView() << ':' << h.value;
separator = " | "; separator = " | ";
} }
} }
@ -985,11 +1085,6 @@ static QByteArray normalizedValue(QAnyStringView value)
return value.visit([](auto value){ return fieldToByteArray(value); }).trimmed(); return value.visit([](auto value){ return fieldToByteArray(value); }).trimmed();
} }
static bool headerNameIs(const Header &header, QAnyStringView name)
{
return header.name == normalizedName(name);
}
/*! /*!
Appends a header entry with \a name and \a value and returns \c true Appends a header entry with \a name and \a value and returns \c true
if successful. if successful.
@ -1003,7 +1098,7 @@ bool QHttpHeaders::append(QAnyStringView name, QAnyStringView value)
return false; return false;
d.detach(); d.detach();
d->headers.push_back({normalizedName(name), normalizedValue(value)}); d->headers.push_back({HeaderName{name}, normalizedValue(value)});
return true; return true;
} }
@ -1016,7 +1111,7 @@ bool QHttpHeaders::append(WellKnownHeader name, QAnyStringView value)
return false; return false;
d.detach(); d.detach();
d->headers.push_back({headerNames[qToUnderlying(name)], normalizedValue(value)}); d->headers.push_back({HeaderName{name}, normalizedValue(value)});
return true; return true;
} }
@ -1035,7 +1130,7 @@ bool QHttpHeaders::insert(qsizetype i, QAnyStringView name, QAnyStringView value
return false; return false;
d.detach(); d.detach();
d->headers.insert(i, {normalizedName(name), normalizedValue(value)}); d->headers.insert(i, {HeaderName{name}, normalizedValue(value)});
return true; return true;
} }
@ -1049,7 +1144,7 @@ bool QHttpHeaders::insert(qsizetype i, WellKnownHeader name, QAnyStringView valu
return false; return false;
d.detach(); d.detach();
d->headers.insert(i, {headerNames[qToUnderlying(name)], normalizedValue(value)}); d->headers.insert(i, {HeaderName{name}, normalizedValue(value)});
return true; return true;
} }
@ -1069,7 +1164,7 @@ bool QHttpHeaders::replace(qsizetype i, QAnyStringView name, QAnyStringView newV
return false; return false;
d.detach(); d.detach();
d->headers.replace(i, {normalizedName(name), normalizedValue(newValue)}); d->headers.replace(i, {HeaderName{name}, normalizedValue(newValue)});
return true; return true;
} }
@ -1083,7 +1178,7 @@ bool QHttpHeaders::replace(qsizetype i, WellKnownHeader name, QAnyStringView new
return false; return false;
d.detach(); d.detach();
d->headers.replace(i, {headerNames[qToUnderlying(name)], normalizedValue(newValue)}); d->headers.replace(i, {HeaderName{name}, normalizedValue(newValue)});
return true; return true;
} }
@ -1094,10 +1189,10 @@ bool QHttpHeaders::replace(qsizetype i, WellKnownHeader name, QAnyStringView new
*/ */
bool QHttpHeaders::contains(QAnyStringView name) const bool QHttpHeaders::contains(QAnyStringView name) const
{ {
if (!d) if (isEmpty())
return false; return false;
return std::any_of(d->headers.cbegin(), d->headers.cend(),
[&name](const Header &header) { return headerNameIs(header, name); }); return std::any_of(d->headers.cbegin(), d->headers.cend(), headerNameMatches(HeaderName{name}));
} }
/*! /*!
@ -1105,7 +1200,10 @@ bool QHttpHeaders::contains(QAnyStringView name) const
*/ */
bool QHttpHeaders::contains(WellKnownHeader name) const bool QHttpHeaders::contains(WellKnownHeader name) const
{ {
return contains(headerNames[qToUnderlying(name)]); if (isEmpty())
return false;
return std::any_of(d->headers.cbegin(), d->headers.cend(), headerNameMatches(HeaderName{name}));
} }
/*! /*!
@ -1115,12 +1213,10 @@ bool QHttpHeaders::contains(WellKnownHeader name) const
*/ */
void QHttpHeaders::removeAll(QAnyStringView name) void QHttpHeaders::removeAll(QAnyStringView name)
{ {
if (contains(name)) { if (isEmpty())
d.detach(); return;
d->headers.removeIf([&name](const Header &header){
return headerNameIs(header, name); return QHttpHeadersPrivate::removeAll(d, HeaderName(name));
});
}
} }
/*! /*!
@ -1128,7 +1224,10 @@ void QHttpHeaders::removeAll(QAnyStringView name)
*/ */
void QHttpHeaders::removeAll(WellKnownHeader name) void QHttpHeaders::removeAll(WellKnownHeader name)
{ {
removeAll(headerNames[qToUnderlying(name)]); if (isEmpty())
return;
return QHttpHeadersPrivate::removeAll(d, HeaderName(name));
} }
/*! /*!
@ -1153,10 +1252,13 @@ void QHttpHeaders::removeAt(qsizetype i)
*/ */
QByteArrayView QHttpHeaders::value(QAnyStringView name, QByteArrayView defaultValue) const noexcept QByteArrayView QHttpHeaders::value(QAnyStringView name, QByteArrayView defaultValue) const noexcept
{ {
if (!d) if (isEmpty())
return defaultValue; return defaultValue;
const HeaderName hname(name);
for (const auto &h : std::as_const(d->headers)) { for (const auto &h : std::as_const(d->headers)) {
if (headerNameIs(h, name)) if (h.name == hname)
return h.value; return h.value;
} }
return defaultValue; return defaultValue;
@ -1167,7 +1269,16 @@ QByteArrayView QHttpHeaders::value(QAnyStringView name, QByteArrayView defaultVa
*/ */
QByteArrayView QHttpHeaders::value(WellKnownHeader name, QByteArrayView defaultValue) const noexcept QByteArrayView QHttpHeaders::value(WellKnownHeader name, QByteArrayView defaultValue) const noexcept
{ {
return value(headerNames[qToUnderlying(name)], defaultValue); if (isEmpty())
return defaultValue;
const HeaderName hname(name);
for (const auto &h : std::as_const(d->headers)) {
if (h.name == hname)
return h.value;
}
return defaultValue;
} }
/*! /*!
@ -1178,14 +1289,17 @@ QByteArrayView QHttpHeaders::value(WellKnownHeader name, QByteArrayView defaultV
*/ */
QList<QByteArray> QHttpHeaders::values(QAnyStringView name) const QList<QByteArray> QHttpHeaders::values(QAnyStringView name) const
{ {
QList<QByteArray> values; QList<QByteArray> result;
if (!d) if (isEmpty())
return values; return result;
const HeaderName hname(name);
for (const auto &h : std::as_const(d->headers)) { for (const auto &h : std::as_const(d->headers)) {
if (headerNameIs(h, name)) if (h.name == hname)
values.append(h.value); result.append(h.value);
} }
return values; return result;
} }
/*! /*!
@ -1193,7 +1307,17 @@ QList<QByteArray> QHttpHeaders::values(QAnyStringView name) const
*/ */
QList<QByteArray> QHttpHeaders::values(WellKnownHeader name) const QList<QByteArray> QHttpHeaders::values(WellKnownHeader name) const
{ {
return values(headerNames[qToUnderlying(name)]); QList<QByteArray> values;
if (isEmpty())
return values;
const HeaderName hname(name);
for (const auto &h : std::as_const(d->headers)) {
if (h.name == hname)
values.append(h.value);
}
return values;
} }
/*! /*!
@ -1219,7 +1343,7 @@ QByteArrayView QHttpHeaders::valueAt(qsizetype i) const noexcept
QLatin1StringView QHttpHeaders::nameAt(qsizetype i) const noexcept QLatin1StringView QHttpHeaders::nameAt(qsizetype i) const noexcept
{ {
verify(i); verify(i);
return QLatin1StringView{d->headers.at(i).name}; return QLatin1StringView{d->headers.at(i).name.asView()};
} }
/*! /*!
@ -1237,14 +1361,18 @@ QLatin1StringView QHttpHeaders::nameAt(qsizetype i) const noexcept
QByteArray QHttpHeaders::combinedValue(QAnyStringView name) const QByteArray QHttpHeaders::combinedValue(QAnyStringView name) const
{ {
QByteArray result; QByteArray result;
if (isEmpty())
return result;
const HeaderName hname(name);
const char* separator = ""; const char* separator = "";
auto valueList = values(name); for (const auto &h : std::as_const(d->headers)) {
for (const auto &v : valueList) { if (h.name == hname) {
result.append(separator); result.append(separator);
result.append(v); result.append(h.value);
separator = ", "; separator = ", ";
} }
}
return result; return result;
} }
@ -1253,7 +1381,20 @@ QByteArray QHttpHeaders::combinedValue(QAnyStringView name) const
*/ */
QByteArray QHttpHeaders::combinedValue(WellKnownHeader name) const QByteArray QHttpHeaders::combinedValue(WellKnownHeader name) const
{ {
return combinedValue(headerNames[qToUnderlying(name)]); QByteArray result;
if (isEmpty())
return result;
const HeaderName hname(name);
const char* separator = "";
for (const auto &h : std::as_const(d->headers)) {
if (h.name == hname) {
result.append(separator);
result.append(h.value);
separator = ", ";
}
}
return result;
} }
/*! /*!
@ -1302,11 +1443,11 @@ QByteArrayView QHttpHeaders::wellKnownHeaderName(WellKnownHeader name) noexcept
QList<std::pair<QByteArray, QByteArray>> QHttpHeaders::toListOfPairs() const QList<std::pair<QByteArray, QByteArray>> QHttpHeaders::toListOfPairs() const
{ {
QList<std::pair<QByteArray, QByteArray>> list; QList<std::pair<QByteArray, QByteArray>> list;
if (!d) if (isEmpty())
return list; return list;
list.reserve(size()); list.reserve(size());
for (const auto & h : std::as_const(d->headers)) for (const auto & h : std::as_const(d->headers))
list.append({h.name, h.value}); list.append({h.name.asByteArray(), h.value});
return list; return list;
} }
@ -1317,10 +1458,10 @@ QList<std::pair<QByteArray, QByteArray>> QHttpHeaders::toListOfPairs() const
QMultiMap<QByteArray, QByteArray> QHttpHeaders::toMultiMap() const QMultiMap<QByteArray, QByteArray> QHttpHeaders::toMultiMap() const
{ {
QMultiMap<QByteArray, QByteArray> map; QMultiMap<QByteArray, QByteArray> map;
if (!d) if (isEmpty())
return map; return map;
for (const auto &h : std::as_const(d->headers)) for (const auto &h : std::as_const(d->headers))
map.insert(h.name, h.value); map.insert(h.name.asByteArray(), h.value);
return map; return map;
} }
@ -1331,11 +1472,11 @@ QMultiMap<QByteArray, QByteArray> QHttpHeaders::toMultiMap() const
QMultiHash<QByteArray, QByteArray> QHttpHeaders::toMultiHash() const QMultiHash<QByteArray, QByteArray> QHttpHeaders::toMultiHash() const
{ {
QMultiHash<QByteArray, QByteArray> hash; QMultiHash<QByteArray, QByteArray> hash;
if (!d) if (isEmpty())
return hash; return hash;
hash.reserve(size()); hash.reserve(size());
for (const auto &h : std::as_const(d->headers)) for (const auto &h : std::as_const(d->headers))
hash.insert(h.name, h.value); hash.insert(h.name.asByteArray(), h.value);
return hash; return hash;
} }