Commit 70d6b0b6 authored by Chip Turner's avatar Chip Turner Committed by JoelMarcey

Improve performance of stringPrintf and related functions

Summary:
It turned out at least one optimization we were doing for
stringPrintf (using the tail of the input buffer) was causing a
performance degradation in some cases -- if the string was already
pre-sized, our resize() call would end up memset'ing the tail.  In some
instances, this could cause significant performance penalties, such as
when multiple stringAppendf calls were made.

So, this diff removes the "optimization" around using the tail of the input
buffer and instead uses a standalone stack buffer.  If vsnprintf deems
that buffer too small, a heap buffer is instead used.

As there is no std::string method that resizes the string to fill the
underlying buffer without setting the values to a default, and as it is
not legal to write past the end of the data buffer (even if capacity()
says there is enough), let's just abandon that optimization.  It turns
out this doesn't have a major performance loss for most cases since,
with this diff, most small strings will fit on-stack and then hopefully
in the string's tail anyway.

Test Plan: runtests

Reviewed By: simpkins@fb.com

Subscribers: trunkagent, net-systems@, lins, anca, folly-diffs@

FB internal diff: D1733774

Tasks: 5735468

Signature: t1:1733774:1418323943:ec47007c9756aca6cf0466bce92722ac4c7e89b2
parent a33734e4
...@@ -31,48 +31,57 @@ namespace folly { ...@@ -31,48 +31,57 @@ namespace folly {
namespace { namespace {
inline void stringPrintfImpl(std::string& output, const char* format, int stringAppendfImplHelper(char* buf,
va_list args) { size_t bufsize,
// Tru to the space at the end of output for our output buffer. const char* format,
// Find out write point then inflate its size temporarily to its va_list args) {
// capacity; we will later shrink it to the size needed to represent
// the formatted string. If this buffer isn't large enough, we do a
// resize and try again.
const auto write_point = output.size();
auto remaining = output.capacity() - write_point;
output.resize(output.capacity());
va_list args_copy; va_list args_copy;
va_copy(args_copy, args); va_copy(args_copy, args);
int bytes_used = vsnprintf(&output[write_point], remaining, format, int bytes_used = vsnprintf(buf, bufsize, format, args_copy);
args_copy);
va_end(args_copy); va_end(args_copy);
return bytes_used;
}
void stringAppendfImpl(std::string& output, const char* format, va_list args) {
// Very simple; first, try to avoid an allocation by using an inline
// buffer. If that fails to hold the output string, allocate one on
// the heap, use it instead.
//
// It is hard to guess the proper size of this buffer; some
// heuristics could be based on the number of format characters, or
// static analysis of a codebase. Or, we can just pick a number
// that seems big enough for simple cases (say, one line of text on
// a terminal) without being large enough to be concerning as a
// stack variable.
std::array<char, 128> inline_buffer;
int bytes_used = stringAppendfImplHelper(
inline_buffer.data(), inline_buffer.size(), format, args);
if (bytes_used < 0) { if (bytes_used < 0) {
throw std::runtime_error( throw std::runtime_error(to<std::string>(
to<std::string>("Invalid format string; snprintf returned negative " "Invalid format string; snprintf returned negative "
"with format string: ", format)); "with format string: ",
} else if (size_t(bytes_used) < remaining) { format));
// There was enough room, just shrink and return. }
output.resize(write_point + bytes_used);
} else { if (static_cast<size_t>(bytes_used) < inline_buffer.size()) {
output.resize(write_point + bytes_used + 1); output.append(inline_buffer.data(), bytes_used);
remaining = bytes_used + 1; return;
va_list args_copy;
va_copy(args_copy, args);
bytes_used = vsnprintf(&output[write_point], remaining, format,
args_copy);
va_end(args_copy);
if (size_t(bytes_used) + 1 != remaining) {
throw std::runtime_error(
to<std::string>("vsnprint retry did not manage to work "
"with format string: ", format));
}
output.resize(write_point + bytes_used);
} }
// Couldn't fit. Heap allocate a buffer, oh well.
std::unique_ptr<char[]> heap_buffer(new char[bytes_used + 1]);
int final_bytes_used =
stringAppendfImplHelper(heap_buffer.get(), bytes_used + 1, format, args);
// The second call should require the same length, which is 1 less
// than the buffer size (we don't keep the trailing \0 byte in our
// output string).
CHECK(bytes_used == final_bytes_used);
output.append(heap_buffer.get(), bytes_used);
} }
} // anon namespace } // anon namespace
std::string stringPrintf(const char* format, ...) { std::string stringPrintf(const char* format, ...) {
va_list ap; va_list ap;
...@@ -84,19 +93,8 @@ std::string stringPrintf(const char* format, ...) { ...@@ -84,19 +93,8 @@ std::string stringPrintf(const char* format, ...) {
} }
std::string stringVPrintf(const char* format, va_list ap) { std::string stringVPrintf(const char* format, va_list ap) {
// snprintf will tell us how large the output buffer should be, but std::string ret;
// we then have to call it a second time, which is costly. By stringAppendfImpl(ret, format, ap);
// guestimating the final size, we avoid the double snprintf in many
// cases, resulting in a performance win. We use this constructor
// of std::string to avoid a double allocation, though it does pad
// the resulting string with nul bytes. Our guestimation is twice
// the format string size, or 32 bytes, whichever is larger. This
// is a hueristic that doesn't affect correctness but attempts to be
// reasonably fast for the most common cases.
std::string ret(std::max(size_t(32), strlen(format) * 2), '\0');
ret.resize(0);
stringPrintfImpl(ret, format, ap);
return ret; return ret;
} }
...@@ -114,7 +112,7 @@ std::string& stringAppendf(std::string* output, const char* format, ...) { ...@@ -114,7 +112,7 @@ std::string& stringAppendf(std::string* output, const char* format, ...) {
std::string& stringVAppendf(std::string* output, std::string& stringVAppendf(std::string* output,
const char* format, const char* format,
va_list ap) { va_list ap) {
stringPrintfImpl(*output, format, ap); stringAppendfImpl(*output, format, ap);
return *output; return *output;
} }
...@@ -129,7 +127,7 @@ void stringPrintf(std::string* output, const char* format, ...) { ...@@ -129,7 +127,7 @@ void stringPrintf(std::string* output, const char* format, ...) {
void stringVPrintf(std::string* output, const char* format, va_list ap) { void stringVPrintf(std::string* output, const char* format, va_list ap) {
output->clear(); output->clear();
stringPrintfImpl(*output, format, ap); stringAppendfImpl(*output, format, ap);
}; };
namespace { namespace {
......
...@@ -112,10 +112,14 @@ TEST(StringPrintf, VPrintf) { ...@@ -112,10 +112,14 @@ TEST(StringPrintf, VPrintf) {
} }
TEST(StringPrintf, VariousSizes) { TEST(StringPrintf, VariousSizes) {
// Test a wide variety of output sizes // Test a wide variety of output sizes, making sure to cross the
for (int i = 0; i < 100; ++i) { // vsnprintf buffer boundary implementation detail.
for (int i = 0; i < 4096; ++i) {
string expected(i + 1, 'a'); string expected(i + 1, 'a');
EXPECT_EQ("X" + expected + "X", stringPrintf("X%sX", expected.c_str())); expected = "X" + expected + "X";
string result = stringPrintf("%s", expected.c_str());
EXPECT_EQ(expected.size(), result.size());
EXPECT_EQ(expected, result);
} }
EXPECT_EQ("abc12345678910111213141516171819202122232425xyz", EXPECT_EQ("abc12345678910111213141516171819202122232425xyz",
......
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