Commit c3f47cdc authored by Andrei Alexandrescu's avatar Andrei Alexandrescu Committed by Tudor Bosman

Faster repeated append (particularly for short strings)

Summary:
https://phabricator.fb.com/D544159 reveals a large performance gap between
fbstring and std::string for repeated appends of short strings, which I
consider a relatively urgent matter (as much of our code uses such patterns).

This diff attempts to fix the issue in a principled manner by first appending
the first character with exponential reallocation, after which the rest of the
characters are appended normally.

With the proposed fix the benchmarks are much faster than the previous fbstring
and also than std::string (numbers to follow in comments).

Test Plan: unittested and benchmarked

Reviewed By: soren@fb.com

FB internal diff: D545416
parent 442e1c0c
...@@ -87,6 +87,7 @@ ...@@ -87,6 +87,7 @@
#include <cassert> #include <cassert>
#include "folly/Traits.h" #include "folly/Traits.h"
#include "folly/Likely.h"
#include "folly/Malloc.h" #include "folly/Malloc.h"
#include "folly/Hash.h" #include "folly/Hash.h"
...@@ -143,15 +144,17 @@ inline void pod_fill(Pod* b, Pod* e, T c) { ...@@ -143,15 +144,17 @@ inline void pod_fill(Pod* b, Pod* e, T c) {
/* /*
* Lightly structured memcpy, simplifies copying PODs and introduces * Lightly structured memcpy, simplifies copying PODs and introduces
* some asserts * some asserts. Unfortunately using this function may cause
* measurable overhead (presumably because it adjusts from a begin/end
* convention to a pointer/size convention, so it does some extra
* arithmetic even though the caller might have done the inverse
* adaptation outside).
*/ */
template <class Pod> template <class Pod>
inline Pod* pod_copy(const Pod* b, const Pod* e, Pod* d) { inline void pod_copy(const Pod* b, const Pod* e, Pod* d) {
assert(e >= b); assert(e >= b);
assert(d >= e || d + (e - b) <= b); assert(d >= e || d + (e - b) <= b);
const size_t s = e - b; memcpy(d, b, (e - b) * sizeof(Pod));
std::memcpy(d, b, s * sizeof(*b));
return d + s;
} }
/* /*
...@@ -210,9 +213,10 @@ public: ...@@ -210,9 +213,10 @@ public:
void shrink(size_t delta); void shrink(size_t delta);
// Expands the string by delta characters (i.e. after this call // Expands the string by delta characters (i.e. after this call
// size() will report the old size() plus delta) but without // size() will report the old size() plus delta) but without
// initializing the expanded region. The caller is expected to fill // initializing the expanded region. Returns a pointer to the memory
// the expanded area appropriately. // to be initialized (the beginning of the expanded portion). The
void expand_noinit(size_t delta); // caller is expected to fill the expanded area appropriately.
Char* expand_noinit(size_t delta);
// Expands the string by one character and sets the last character // Expands the string by one character and sets the last character
// to c. // to c.
void push_back(Char c); void push_back(Char c);
...@@ -606,31 +610,33 @@ public: ...@@ -606,31 +610,33 @@ public:
assert(capacity() >= minCapacity); assert(capacity() >= minCapacity);
} }
void expand_noinit(const size_t delta) { Char * expand_noinit(const size_t delta) {
// Strategy is simple: make room, then change size // Strategy is simple: make room, then change size
assert(capacity() >= size()); assert(capacity() >= size());
size_t sz, newSz, cp; size_t sz, newSz;
if (category() == isSmall) { if (category() == isSmall) {
sz = smallSize(); sz = smallSize();
newSz = sz + delta; newSz = sz + delta;
if (newSz <= maxSmallSize) { if (newSz <= maxSmallSize) {
setSmallSize(newSz); setSmallSize(newSz);
writeTerminator(); writeTerminator();
return; return small_ + sz;
} }
cp = maxSmallSize; reserve(newSz);
} else { } else {
sz = ml_.size_; sz = ml_.size_;
newSz = sz + delta; newSz = ml_.size_ + delta;
cp = capacity(); if (newSz > ml_.capacity()) {
reserve(newSz);
}
} }
if (newSz > cp) reserve(newSz);
assert(capacity() >= newSz); assert(capacity() >= newSz);
// Category can't be small - we took care of that above // Category can't be small - we took care of that above
assert(category() == isMedium || category() == isLarge); assert(category() == isMedium || category() == isLarge);
ml_.size_ = newSz; ml_.size_ = newSz;
writeTerminator(); writeTerminator();
assert(size() == newSz); assert(size() == newSz);
return ml_.data_ + sz;
} }
void push_back(Char c) { void push_back(Char c) {
...@@ -644,7 +650,7 @@ public: ...@@ -644,7 +650,7 @@ public:
writeTerminator(); writeTerminator();
return; return;
} }
reserve(maxSmallSize * 3 / 2); reserve(maxSmallSize * 2);
} else { } else {
sz = ml_.size_; sz = ml_.size_;
cp = ml_.capacity(); cp = ml_.capacity();
...@@ -854,8 +860,10 @@ public: ...@@ -854,8 +860,10 @@ public:
assert(delta <= size()); assert(delta <= size());
backend_.resize(size() - delta); backend_.resize(size() - delta);
} }
void expand_noinit(size_t delta) { Char * expand_noinit(size_t delta) {
auto const sz = size();
backend_.resize(size() + delta); backend_.resize(size() + delta);
return backend_.data() + sz;
} }
void push_back(Char c) { void push_back(Char c) {
backend_.push_back(c); backend_.push_back(c);
...@@ -1001,8 +1009,7 @@ public: ...@@ -1001,8 +1009,7 @@ public:
} }
basic_fbstring(size_type n, value_type c, const A& a = A()) { basic_fbstring(size_type n, value_type c, const A& a = A()) {
store_.expand_noinit(n); auto const data = store_.expand_noinit(n);
auto const data = store_.mutable_data();
fbstring_detail::pod_fill(data, data + n, c); fbstring_detail::pod_fill(data, data + n, c);
store_.writeTerminator(); store_.writeTerminator();
} }
...@@ -1235,23 +1242,39 @@ public: ...@@ -1235,23 +1242,39 @@ public:
return append(str.data() + pos, n); return append(str.data() + pos, n);
} }
basic_fbstring& append(const value_type* s, const size_type n) { basic_fbstring& append(const value_type* s, size_type n) {
#ifndef NDEBUG #ifndef NDEBUG
auto oldSize = size();
#endif
Invariant checker(*this); Invariant checker(*this);
(void) checker; (void) checker;
static std::less_equal<const value_type*> le; #endif
if (le(data(), s) && !le(data() + size(), s)) {// aliasing if (UNLIKELY(!n)) {
assert(le(s + n, data() + size())); // Unlikely but must be done
const size_type offset = s - data(); return *this;
store_.reserve(size() + n); }
auto const oldSize = size();
auto const oldData = data();
// Check for aliasing (rare). We could use "<=" here but in theory
// those do not work for pointers unless the pointers point to
// elements in the same array. For that reason we use
// std::less_equal, which is guaranteed to offer a total order
// over pointers. See discussion at http://goo.gl/Cy2ya for more
// info.
static const std::less_equal<const value_type*> le;
if (UNLIKELY(le(oldData, s) && !le(oldData + oldSize, s))) {
assert(le(s + n, oldData + oldSize));
const size_type offset = s - oldData;
store_.reserve(oldSize + n);
// Restore the source // Restore the source
s = data() + offset; s = data() + offset;
} }
store_.expand_noinit(n); // Warning! Repeated appends with short strings may actually incur
fbstring_detail::pod_copy(s, s + n, end() - n); // practically quadratic performance. Avoid that by pushing back
store_.writeTerminator(); // the first character (which ensures exponential growth) and then
// appending the rest normally. Worst case the append may incur a
// second allocation but that will be rare.
push_back(*s++);
--n;
memcpy(store_.expand_noinit(n), s, n * sizeof(value_type));
assert(size() == oldSize + n); assert(size() == oldSize + n);
return *this; return *this;
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment