[LLVMdev] SmallString + raw_svector_ostream combination should be more efficient

Yaron Keren yaron.keren at gmail.com
Fri May 1 21:41:17 PDT 2015


+update diff


2015-05-02 7:38 GMT+03:00 Yaron Keren <yaron.keren at gmail.com>:

> I outlined (is that the right
> word?) raw_char_ostream::grow, raw_char_ostream::write (both) into
> raw_ostream.cpp with less than 10% difference in performance.
>
> Profiling reveals that the real culprit is the code line
>
>  OS.reserve(OS.size() + 64);
>
> called from raw_svector_ostream::write_impl called from
> raw_svector_ostream::~raw_svector_ostream.
> The issue is already documented:
>
> raw_svector_ostream::~raw_svector_ostream() {
>   // FIXME: Prevent resizing during this flush().
>   flush();
> }
>
> And a solution is provided in raw_svector_ostream::init():
>
>   // Set up the initial external buffer. We make sure that the buffer has
> at
>   // least 128 bytes free; raw_ostream itself only requires 64, but we
> want to
>   // make sure that we don't grow the buffer unnecessarily on destruction
> (when
>   // the data is flushed). See the FIXME below.
>   OS.reserve(OS.size() + 128);
>
> This solution may be worse than the problem. In total:
>
> * If the SmallString was less than 128 bytes init() will always(!) heap
> allocate.
> * If it's more or equal to 128 bytes but upon destruction less than 64
> bytes are left unused it will heap allocate for no reason.
>
> A careful programmer who did size the SmallString to match its use + small
> reserve will find either of these heap allocations surprising, negating the
> SmallString advantage and then paying memcpy cost.
>
> I filed http://llvm.org/pr23395 about this. To temporarly avoid this
> issue, in addition to the outline methods change I commented the OS.reserve
> line in flush(). (This change of course requires further review.)
>
> With reserve being out of the way, the gap now is smaller but still
> significant: raw_char_ostream is about 20% faster than
> raw_svector_ostream in Release=optimized configuration.
>
>
> 2015-05-02 4:39 GMT+03:00 Sean Silva <chisophugis at gmail.com>:
>
>>
>> Could you dig into why:
>> +  raw_ostream &write(unsigned char C) override {
>> +    grow(1);
>> +    *OutBufCur++ = C;
>> +    return *this;
>> +  }
>>
>> Is 3 times as fast as raw_svector_ostream? I don't see a good reason why
>> that should be any faster than:
>>
>>   raw_ostream &operator<<(char C) {
>>     if (OutBufCur >= OutBufEnd)
>>       return write(C);
>>     *OutBufCur++ = C;
>>     return *this;
>>   }
>>
>>
>> You might just be seeing the difference between having write inlined vs.
>> non-inlined, in which case your patch might be a complicated way to pull
>> the likely cases of some raw_ostream methods into the header.
>>
>>
>> -- Sean Silva
>>
>
> On Thu, Apr 30, 2015 at 8:02 PM, Yaron Keren <yaron.keren at gmail.com>
> wrote:
>
>> Yes, we should do without virtual flush. The problem was raw_char_ostream
>> should not be flushed - it's always current - so I overloaded flush to a
>> no-op. A cleaner solution is attached, adding a DirectBuffer mode to the
>> raw_ostream.
>>
>> Also added a simple performance comparison project
>> between raw_char_ostream and raw_svector_ostream. On Window 7 x64
>> machine, raw_char_ostream was three times faster than raw_svector_ostream
>> when the provided buffer size is large enough and two times as fast with
>> one dynamic allocation resize.
>>
>>
>> 2015-04-30 22:48 GMT+03:00 Rafael EspĂ­ndola <rafael.espindola at gmail.com>:
>>
>>> I don't think we should make flush virtual. Why do you need to do it?
>>> Can't you set up the base class to write to use the tail of the memory
>>> region as the buffer?
>>>
>>>
>>>
>>> On 24 April 2015 at 06:46, Yaron Keren <yaron.keren at gmail.com> wrote:
>>> > Hi,
>>> >
>>> > Is this what you're thinking about?
>>> > The code is not tested yet, I'd like to know if the overall direction
>>> and
>>> > design are correct.
>>> >
>>> > Yaron
>>> >
>>> >
>>> > 2015-04-21 0:10 GMT+03:00 Yaron Keren <yaron.keren at gmail.com>:
>>> >>
>>> >> Sean, thanks for reminding this, Alp did commit a class derived from
>>> >> raw_svector_ostream conatining an internal SmallString he called
>>> >> small_string_ostream. The commit was reverted after a day due to a
>>> >> disagreement about the commit approval and apparently abandoned.
>>> >>
>>> >>
>>> >>
>>> http://lists.cs.uiuc.edu/pipermail/llvm-commits/Week-of-Mon-20140623/223393.html
>>> >>
>>> >>
>>> http://lists.cs.uiuc.edu/pipermail/llvm-commits/Week-of-Mon-20140623/223557.html
>>> >>
>>> >>
>>> http://lists.cs.uiuc.edu/pipermail/llvm-commits/Week-of-Mon-20140630/223986.html
>>> >>
>>> >> The class Alp comitted did solve the possible mismatch between the
>>> >> SmallString and the stream by making the SmallString private to the
>>> class.
>>> >> This however did not treat the root problem, the duplication of the
>>> >> information about the buffer between SmallString and the stream.
>>> >>
>>> >> I can make performance measurements but even if the performance
>>> difference
>>> >> is neglible (and looking at all the code paths and conditionals of
>>> >> non-inlined functions at raw_ostream.cpp that's hard to believe), the
>>> >> current SmallString-raw_svector_ostream is simply wrong.
>>> >>
>>> >> Personally, after the previous "Alp" discussion I found and fixed
>>> several
>>> >> such issues in my out-of-tree code only to make new similar new error
>>> >> earlier this year, which I caught only months after, when Rafael
>>> committed
>>> >> the pwrite which reminded me the issue. Due ot the information
>>> duplication
>>> >> Rafael commit also had a bug and Mehdi reports similar experience.
>>> Back then
>>> >> Alp reported similar problems he found in LLVM code which were
>>> hopefully
>>> >> fixed otherwise.
>>> >>
>>> >> In addition to the information duplication, the design is quite
>>> confusing
>>> >> to use
>>> >>
>>> >> - Should one use the stream.str() function or the SmallString itself?
>>> >> - flush or str?
>>> >> - How do you properly clear the combination for reuse (that was my
>>> >> mistake, I forgot to resync after OS.clear())?
>>> >>
>>> >> It's safe to say this design pattern is very easy to get wrong one
>>> way or
>>> >> another, we got burned by it multiple times and it should be replaced.
>>> >>
>>> >> Alp suggested a derived class containing its own SmallString. That's a
>>> >> simple and effective approach to avoid the bugs mentioned but does
>>> not fix
>>> >> the memory or runtime issues. Small as they may be, why have them at a
>>> >> fundemental data structure?
>>> >>
>>> >> I was thinking about going further avoiding all duplication with a
>>> >> templated class derived with its own internal buffer storage.
>>> >>
>>> >> Rafael suggested a more modular approach, a derived adpater class
>>> adapter
>>> >> to a *simple* buffer (or nullptr for fully-dynamic operation) which
>>> also
>>> >> won't duplicate any information the buffer is dumb and has no state.
>>> >>
>>> >> That solution sounds simple, efficient and safe to use. The
>>> implementation
>>> >> would be actually simpler then raw_svector_ostream since all the
>>> >> coordination logic is not required anymore.
>>> >>
>>> >>
>>> >> 2015-04-20 22:17 GMT+03:00 Sean Silva <chisophugis at gmail.com>:
>>> >>>
>>> >>>
>>> >>>
>>> >>> On Sun, Apr 19, 2015 at 7:40 AM, Yaron Keren <yaron.keren at gmail.com>
>>> >>> wrote:
>>> >>>>
>>> >>>> A very common code pattern in LLVM is
>>> >>>>
>>> >>>>  SmallString<128> S;
>>> >>>>  raw_svector_ostream OS(S);
>>> >>>>  OS<< ...
>>> >>>>  Use OS.str()
>>> >>>>
>>> >>>> While raw_svector_ostream is smart to share the text buffer itself,
>>> it's
>>> >>>> inefficient keeping two sets of pointers to the same buffer:
>>> >>>>
>>> >>>>  In SmallString: void *BeginX, *EndX, *CapacityX
>>> >>>>  In raw_ostream: char *OutBufStart, *OutBufEnd, *OutBufCur
>>> >>>>
>>> >>>> Moreover, at runtime the two sets of pointers need to be coordinated
>>> >>>> between the SmallString and raw_svector_ostream using
>>> >>>> raw_svector_ostream::init, raw_svector_ostream::pwrite,
>>> >>>> raw_svector_ostream::resync and raw_svector_ostream::write_impl.
>>> >>>> All these functions have non-inlined implementations in
>>> raw_ostream.cpp.
>>> >>>>
>>> >>>> Finally, this may cause subtle bugs if S is modified without calling
>>> >>>> OS::resync(). This is too easy to do by mistake.
>>> >>>>
>>> >>>> In this frequent case usage the client does not really care about S
>>> >>>> being a SmallString with its many useful string helper function.
>>> It's just
>>> >>>> boilerplate code for raw_svector_ostream. But it does cost three
>>> extra
>>> >>>> pointers, some runtime performance and possible bugs.
>>> >>>
>>> >>>
>>> >>> I agree the bugs are real (Alp proposed something a while back
>>> regarding
>>> >>> this?), but you will need to provide measurements to justify the
>>> cost in
>>> >>> runtime performance. One technique I have used in the past to
>>> measure these
>>> >>> sorts of things I call "stuffing": take the operation that you want
>>> to
>>> >>> measure, then essentially change the logic so that you pay the cost
>>> 2 times,
>>> >>> 3 times, etc. You can then look at the trend in performance as N
>>> varies and
>>> >>> extrapolate back to the case where N = 0 (i.e. you don't pay the
>>> cost).
>>> >>>
>>> >>> For example, in one situation where I used this method it was to
>>> measure
>>> >>> the cost of stat'ing files (sys::fs::status) across a holistic
>>> build, using
>>> >>> only "time" on the command line (it was on Windows and I didn't have
>>> any
>>> >>> tools like DTrace available that can directly measure this). In
>>> order to do
>>> >>> this, I changed sys::fs::status to call stat N times instead of 1,
>>> and
>>> >>> measured with N=1 N=2 N=3 etc. The result was that the difference
>>> between
>>> >>> the N and N+1 versions was about 1-2% across N=1..10 (or whatever I
>>> >>> measured). In order to negate caching and other confounding effects,
>>> it is
>>> >>> important to try different distributions of stats; e.g. the extra
>>> stats are
>>> >>> on the same file as the "real" stat vs. the extra stats are on
>>> nonexistent
>>> >>> files in the same directory as the "real" file vs. parent
>>> directories of the
>>> >>> "real" file; if these match up fairly well (they did), then you have
>>> some
>>> >>> indication that the "stuffing" is measuring what you want to measure.
>>> >>>
>>> >>> So e.g. if you think the cost of 3 extra pointers is significant,
>>> then
>>> >>> "stuff" the struct with 3, 6, 9, ... extra pointers and measure the
>>> >>> difference in performance (e.g. measure the time building a real
>>> project).
>>> >>>
>>> >>> -- Sean Silva
>>> >>>
>>> >>>>
>>> >>>>
>>> >>>> To solve all three issues, would it make sense to have
>>> >>>> raw_ostream-derived container with a its own SmallString like
>>> templated-size
>>> >>>> built-in buffer?
>>> >>>>
>>> >>>
>>> >
>>>
>>
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20150502/cf76e5ee/attachment.html>
-------------- next part --------------
Index: include/llvm/Support/raw_ostream.h
===================================================================
--- include/llvm/Support/raw_ostream.h	(revision 236304)
+++ include/llvm/Support/raw_ostream.h	(working copy)
@@ -52,17 +52,19 @@
   ///               OutBufEnd - OutBufStart >= 1).
   ///
   /// If buffered, then the raw_ostream owns the buffer if (BufferMode ==
-  /// InternalBuffer); otherwise the buffer has been set via SetBuffer and is
-  /// managed by the subclass.
+  /// InternalBuffer or BufferMode == DirectBuffer); otherwise the buffer has
+  /// been set via SetBuffer and is managed by the subclass.
   ///
   /// If a subclass installs an external buffer using SetBuffer then it can wait
   /// for a \see write_impl() call to handle the data which has been put into
   /// this buffer.
+protected:
   char *OutBufStart, *OutBufEnd, *OutBufCur;
 
   enum BufferKind {
     Unbuffered = 0,
     InternalBuffer,
+    DirectBuffer,
     ExternalBuffer
   } BufferMode;
 
@@ -132,7 +134,7 @@
   //===--------------------------------------------------------------------===//
 
   void flush() {
-    if (OutBufCur != OutBufStart)
+    if (BufferMode != DirectBuffer && OutBufCur != OutBufStart)
       flush_nonempty();
   }
 
@@ -208,8 +210,8 @@
   /// satisfy std::isprint into an escape sequence.
   raw_ostream &write_escaped(StringRef Str, bool UseHexEscapes = false);
 
-  raw_ostream &write(unsigned char C);
-  raw_ostream &write(const char *Ptr, size_t Size);
+  virtual raw_ostream &write(unsigned char C);
+  virtual raw_ostream &write(const char *Ptr, size_t Size);
 
   // Formatted output, see the format() function in Support/Format.h.
   raw_ostream &operator<<(const format_object_base &Fmt);
@@ -297,13 +299,13 @@
   /// unbuffered.
   const char *getBufferStart() const { return OutBufStart; }
 
+  /// Install the given buffer and mode.
+  void SetBufferAndMode(char *BufferStart, size_t Size, BufferKind Mode);
+
   //===--------------------------------------------------------------------===//
   // Private Interface
   //===--------------------------------------------------------------------===//
 private:
-  /// Install the given buffer and mode.
-  void SetBufferAndMode(char *BufferStart, size_t Size, BufferKind Mode);
-
   /// Flush the current buffer, which is known to be non-empty. This outputs the
   /// currently buffered data and resets the buffer to empty.
   void flush_nonempty();
@@ -481,6 +483,33 @@
   }
 };
 
+/// A raw_ostream that writes to a char array. This is a simple adaptor class.
+/// This class does not encounter output errors.
+class raw_char_ostream : public raw_pwrite_stream {
+  void write_impl(const char *Ptr, size_t Size) override{};
+  void pwrite_impl(const char *Ptr, size_t Size, uint64_t Offset) override {
+    memcpy(OutBufStart + Offset, Ptr, Size);
+  }
+  uint64_t current_pos() const override { return 0; }
+  void grow(size_t Size);
+
+public:
+  explicit raw_char_ostream(char *Buffer, unsigned BufferSize) {
+    if (Buffer) {
+      SetBufferAndMode(Buffer, BufferSize, ExternalBuffer);
+    } else {
+      if (!BufferSize)
+        BufferSize = preferred_buffer_size();
+      SetBufferAndMode(new char[BufferSize], BufferSize, DirectBuffer);
+    }
+  }
+  ~raw_char_ostream() override { OutBufCur = OutBufStart; }
+  raw_ostream &write(unsigned char C) override;
+  raw_ostream &write(const char *Ptr, size_t Size) override;
+  /// Returns the buffer.
+  StringRef str() { return StringRef(OutBufStart, GetNumBytesInBuffer()); }
+};
+
 /// A raw_ostream that writes to an SmallVector or SmallString.  This is a
 /// simple adaptor class. This class does not encounter output errors.
 class raw_svector_ostream : public raw_pwrite_stream {
Index: lib/Support/raw_ostream.cpp
===================================================================
--- lib/Support/raw_ostream.cpp	(revision 236304)
+++ lib/Support/raw_ostream.cpp	(working copy)
@@ -65,7 +65,7 @@
   assert(OutBufCur == OutBufStart &&
          "raw_ostream destructor called with non-empty buffer!");
 
-  if (BufferMode == InternalBuffer)
+  if (BufferMode == InternalBuffer || BufferMode == DirectBuffer)
     delete [] OutBufStart;
 }
 
@@ -95,7 +95,7 @@
   // child buffer management logic will be in write_impl).
   assert(GetNumBytesInBuffer() == 0 && "Current buffer is non-empty!");
 
-  if (BufferMode == InternalBuffer)
+  if (BufferMode == InternalBuffer || BufferMode == DirectBuffer)
     delete [] OutBufStart;
   OutBufStart = BufferStart;
   OutBufEnd = OutBufStart+Size;
@@ -751,6 +751,29 @@
   OS.append(Ptr, Size);
 }
 
+void raw_char_ostream::grow(size_t Size) {
+  size_t CurBytesAvailable = OutBufEnd - OutBufCur;
+  if (Size > CurBytesAvailable) {
+    size_t CurPos = OutBufCur - OutBufStart;
+    OutBufCur = OutBufStart;
+    size_t NewSize = size_t(NextPowerOf2(CurPos + Size + 2));
+    SetBufferAndMode(new char[NewSize], NewSize, DirectBuffer);
+    OutBufCur = OutBufStart + CurPos;
+  }
+}
+
+raw_ostream &raw_char_ostream::write(unsigned char C) {
+  grow(1);
+  *OutBufCur++ = C;
+  return *this;
+}
+raw_ostream &raw_char_ostream::write(const char *Ptr, size_t Size) {
+  grow(Size);
+  memcpy(OutBufCur, Ptr, Size);
+  OutBufCur += Size;
+  return *this;
+}
+
 //===----------------------------------------------------------------------===//
 //  raw_svector_ostream
 //===----------------------------------------------------------------------===//
@@ -810,6 +833,7 @@
     OS.append(Ptr, Ptr + Size);
   }
 
+  // TESTME: comment out reserve call.
   OS.reserve(OS.size() + 64);
   SetBuffer(OS.end(), OS.capacity() - OS.size());
 }
Index: projects/raw_char_ostream_speed/CMakeLists.txt
===================================================================
--- projects/raw_char_ostream_speed/CMakeLists.txt	(revision 0)
+++ projects/raw_char_ostream_speed/CMakeLists.txt	(working copy)
@@ -0,0 +1,7 @@
+set(LLVM_LINK_COMPONENTS
+  Support
+)
+
+add_llvm_tool(raw_char_ostream
+  raw_char_ostream_speed.cpp
+)
Index: projects/raw_char_ostream_speed/raw_char_ostream_speed.cpp
===================================================================
--- projects/raw_char_ostream_speed/raw_char_ostream_speed.cpp	(revision 0)
+++ projects/raw_char_ostream_speed/raw_char_ostream_speed.cpp	(working copy)
@@ -0,0 +1,45 @@
+#include "llvm/ADT/SmallString.h"
+#include "llvm/Support/raw_ostream.h"
+
+#include <ctime>
+
+using namespace llvm;
+
+namespace {
+const StringRef FuncName =
+    "thadddddddddsrttttttttttttttttttttttttjouhojkirjdjdsjoiuosiasddjisjsrof";
+
+void writeStream(raw_ostream &OS) {
+  OS << "_Z" << FuncName.size() << FuncName << 'v';
+}
+
+const unsigned BufferSize = 128;
+void test_raw_char_ostream() {
+  char Buffer[BufferSize];
+  raw_char_ostream OS(Buffer, sizeof(Buffer));
+  writeStream(OS);
+}
+
+void test_raw_svector_ostream() {
+  SmallString<BufferSize> Buffer;
+  raw_svector_ostream OS(Buffer);
+  writeStream(OS);
+  // Make sure Buffer was not reallocated.
+  assert(Buffer.capacity() == BufferSize);
+}
+
+void testIt(void (*F)()) {
+  std::clock_t StartTime = std::clock();
+  for (unsigned i = 1e7; i; --i)
+    F();
+  std::clock_t EndTime = std::clock();
+  unsigned ms = unsigned(double(EndTime - StartTime) * 1000.0 / CLOCKS_PER_SEC);
+  outs() << ms << "ms\n";
+}
+}
+
+int main() {
+  testIt(test_raw_char_ostream);
+  testIt(test_raw_svector_ostream);
+  return 0;
+}


More information about the llvm-dev mailing list