Commit cb527272 authored by Adam Simpkins's avatar Adam Simpkins Committed by Jordan DeLong

change the mechanism for "internal" buffer storage

Summary:
This removes kFlagExt, and instead implements the internal buffer
optimization using operator new and delete.

IOBuf::createCombined() will allocate a single buffer for the IOBuf,
SharedInfo, and the actual data itself.  Each heap allocated IOBuf now
contains a set of flags indicating if the memory segment indicating when
it can actually be deleted.  The delete operator won't actually free the
storage until both the IOBuf and the data buffer are unused (the
SharedInfo object always becomes unused at the same time as the data
buffer).

This has a couple advantages over the old mechanism:

- Other IOBufs can continue to use and share the data storage space even
after the original IOBuf associated with it is deleted.  With the old
internal buffer mechanism, internal buffers could never be shared.

- This simplifies several parts of the code, since kFlagExt no longer
exists.  Code that previously required special handling for internal
buffers no longer needs to be aware of them.

One downside is that this can result in wasted space in some cases if
the original IOBuf is changed to point at a different buffer, since the
space for the data buffer cannot be freed until the IOBuf itself is also
destroyed.  The old internal buffer mechanism also had this problem,
which we mitigated simply by disallowing internal buffers for larger
than ~200 bytes.  With the new mechanism we currently allocate an
internal buffer for up to 1024 bytes by default, but we also allow
callers to explicitly decide if they want an internal buffer or not.

Test Plan:
Added some new unit tests for the combined buffer behavior.  Also ran
all of the existing IOBuf unit tests, with and without ASAN.  (ASAN
performs additional memory checking, but also changes IOBuf's behavior
slightly as usingJEMalloc() is false with ASAN.)

Reviewed By: davejwatson@fb.com

FB internal diff: D1072336

@override-unit-failures
parent ec61097a
This diff is collapsed.
...@@ -222,6 +222,27 @@ class IOBuf { ...@@ -222,6 +222,27 @@ class IOBuf {
*/ */
static std::unique_ptr<IOBuf> create(uint32_t capacity); static std::unique_ptr<IOBuf> create(uint32_t capacity);
/**
* Create a new IOBuf, using a single memory allocation to allocate space
* for both the IOBuf object and the data storage space.
*
* This saves one memory allocation. However, it can be wasteful if you
* later need to grow the buffer using reserve(). If the buffer needs to be
* reallocated, the space originally allocated will not be freed() until the
* IOBuf object itself is also freed. (It can also be slightly wasteful in
* some cases where you clone this IOBuf and then free the original IOBuf.)
*/
static std::unique_ptr<IOBuf> createCombined(uint32_t capacity);
/**
* Create a new IOBuf, using separate memory allocations for the IOBuf object
* for the IOBuf and the data storage space.
*
* This requires two memory allocations, but saves space in the long run
* if you know that you will need to reallocate the data buffer later.
*/
static std::unique_ptr<IOBuf> createSeparate(uint32_t capacity);
/** /**
* Allocate a new IOBuf chain with the requested total capacity, allocating * Allocate a new IOBuf chain with the requested total capacity, allocating
* no more than maxBufCapacity to each buffer. * no more than maxBufCapacity to each buffer.
...@@ -455,7 +476,7 @@ class IOBuf { ...@@ -455,7 +476,7 @@ class IOBuf {
* get a pointer to the start of the data within the buffer. * get a pointer to the start of the data within the buffer.
*/ */
const uint8_t* buffer() const { const uint8_t* buffer() const {
return (flags_ & kFlagExt) ? ext_.buf : int_.buf; return buf_;
} }
/** /**
...@@ -465,7 +486,7 @@ class IOBuf { ...@@ -465,7 +486,7 @@ class IOBuf {
* actually safe to write to the buffer. * actually safe to write to the buffer.
*/ */
uint8_t* writableBuffer() { uint8_t* writableBuffer() {
return (flags_ & kFlagExt) ? ext_.buf : int_.buf; return buf_;
} }
/** /**
...@@ -476,9 +497,7 @@ class IOBuf { ...@@ -476,9 +497,7 @@ class IOBuf {
* get a pointer to the end of the data within the buffer. * get a pointer to the end of the data within the buffer.
*/ */
const uint8_t* bufferEnd() const { const uint8_t* bufferEnd() const {
return (flags_ & kFlagExt) ? return buf_ + capacity_;
ext_.buf + ext_.capacity :
int_.buf + kMaxInternalDataSize;
} }
/** /**
...@@ -488,7 +507,7 @@ class IOBuf { ...@@ -488,7 +507,7 @@ class IOBuf {
* method to get the length of the actual valid data in this IOBuf. * method to get the length of the actual valid data in this IOBuf.
*/ */
uint32_t capacity() const { uint32_t capacity() const {
return (flags_ & kFlagExt) ? ext_.capacity : kMaxInternalDataSize; return capacity_;
} }
/** /**
...@@ -830,15 +849,11 @@ class IOBuf { ...@@ -830,15 +849,11 @@ class IOBuf {
return true; return true;
} }
// an internal buffer wouldn't have kFlagMaybeShared or kFlagUserOwned // kFlagMaybeShared is set, so we need to check the reference count.
// so we would have returned false already. The only remaining case // (Checking the reference count requires an atomic operation, which is why
// is an external buffer which may be shared, so we need to read // we prefer to only check kFlagMaybeShared if possible.)
// the reference count. DCHECK(flags_ & kFlagMaybeShared);
assert((flags_ & (kFlagExt | kFlagMaybeShared)) == bool shared = sharedInfo_->refcount.load(std::memory_order_acquire) > 1;
(kFlagExt | kFlagMaybeShared));
bool shared =
ext_.sharedInfo->refcount.load(std::memory_order_acquire) > 1;
if (!shared) { if (!shared) {
// we're the last one left // we're the last one left
flags_ &= ~kFlagMaybeShared; flags_ &= ~kFlagMaybeShared;
...@@ -973,10 +988,12 @@ class IOBuf { ...@@ -973,10 +988,12 @@ class IOBuf {
*/ */
folly::fbvector<struct iovec> getIov() const; folly::fbvector<struct iovec> getIov() const;
// Overridden operator new and delete. /*
// These directly use malloc() and free() to allocate the space for IOBuf * Overridden operator new and delete.
// objects. This is needed since IOBuf::create() manually uses malloc when * These perform specialized memory management to help support
// allocating IOBuf objects with an internal buffer. * createCombined(), which allocates IOBuf objects together with the buffer
* data.
*/
void* operator new(size_t size); void* operator new(size_t size);
void* operator new(size_t size, void* ptr); void* operator new(size_t size, void* ptr);
void operator delete(void* ptr); void operator delete(void* ptr);
...@@ -1000,21 +1017,20 @@ class IOBuf { ...@@ -1000,21 +1017,20 @@ class IOBuf {
private: private:
enum FlagsEnum : uint32_t { enum FlagsEnum : uint32_t {
kFlagExt = 0x1, kFlagUserOwned = 0x1,
kFlagUserOwned = 0x2, kFlagFreeSharedInfo = 0x2,
kFlagFreeSharedInfo = 0x4, kFlagMaybeShared = 0x4,
kFlagMaybeShared = 0x8,
}; };
// Values for the ExternalBuf type field. // Values for the type_ field.
// We currently don't really use this for anything, other than to have it // We currently don't really use this for anything, other than to have it
// around for debugging purposes. We store it at the moment just because we // around for debugging purposes. We store it at the moment just because we
// have the 4 extra bytes in the ExternalBuf struct that would just be // have the 4 extra bytes that would just be padding otherwise.
// padding otherwise.
enum ExtBufTypeEnum { enum ExtBufTypeEnum {
kExtAllocated = 0, kExtAllocated = 0,
kExtUserSupplied = 1, kExtUserSupplied = 1,
kExtUserOwned = 2, kExtUserOwned = 2,
kCombinedAlloc = 3,
}; };
struct SharedInfo { struct SharedInfo {
...@@ -1027,33 +1043,15 @@ class IOBuf { ...@@ -1027,33 +1043,15 @@ class IOBuf {
void* userData; void* userData;
std::atomic<uint32_t> refcount; std::atomic<uint32_t> refcount;
}; };
struct ExternalBuf { // Helper structs for use by operator new and delete
uint32_t capacity; struct HeapPrefix;
uint32_t type; struct HeapStorage;
uint8_t* buf; struct HeapFullStorage;
// SharedInfo may be NULL if kFlagUserOwned is set. It is non-NULL
// in all other cases.
SharedInfo* sharedInfo;
};
struct InternalBuf {
uint8_t buf[] __attribute__((aligned));
};
// The maximum size for an IOBuf object, including any internal data buffer
static const uint32_t kMaxIOBufSize = 256;
static const uint32_t kMaxInternalDataSize;
// Forbidden copy constructor and assignment opererator // Forbidden copy constructor and assignment opererator
IOBuf(IOBuf const &); IOBuf(IOBuf const &);
IOBuf& operator=(IOBuf const &); IOBuf& operator=(IOBuf const &);
/**
* Create a new IOBuf with internal data.
*
* end is a pointer to the end of the IOBuf's internal data buffer.
*/
explicit IOBuf(uint8_t* end);
/** /**
* Create a new IOBuf pointing to an external buffer. * Create a new IOBuf pointing to an external buffer.
* *
...@@ -1088,6 +1086,8 @@ class IOBuf { ...@@ -1088,6 +1086,8 @@ class IOBuf {
uint8_t** bufReturn, uint8_t** bufReturn,
SharedInfo** infoReturn, SharedInfo** infoReturn,
uint32_t* capacityReturn); uint32_t* capacityReturn);
static void releaseStorage(HeapStorage* storage, uint16_t freeFlags);
static void freeInternalBuf(void* buf, void* userData);
/* /*
* Member variables * Member variables
...@@ -1110,13 +1110,14 @@ class IOBuf { ...@@ -1110,13 +1110,14 @@ class IOBuf {
* This may refer to any subsection of the actual buffer capacity. * This may refer to any subsection of the actual buffer capacity.
*/ */
uint8_t* data_; uint8_t* data_;
uint8_t* buf_;
uint32_t length_; uint32_t length_;
uint32_t capacity_;
mutable uint32_t flags_; mutable uint32_t flags_;
uint32_t type_;
union { // SharedInfo may be NULL if kFlagUserOwned is set. It is non-NULL
ExternalBuf ext_; // in all other cases.
InternalBuf int_; SharedInfo* sharedInfo_;
};
struct DeleterBase { struct DeleterBase {
virtual ~DeleterBase() { } virtual ~DeleterBase() { }
......
...@@ -163,6 +163,61 @@ TEST(IOBuf, WrapBuffer) { ...@@ -163,6 +163,61 @@ TEST(IOBuf, WrapBuffer) {
EXPECT_EQ(size2, iobuf2->capacity()); EXPECT_EQ(size2, iobuf2->capacity());
} }
TEST(IOBuf, CreateCombined) {
// Create a combined IOBuf, then destroy it.
// The data buffer and IOBuf both become unused as part of the destruction
{
auto buf = IOBuf::createCombined(256);
EXPECT_FALSE(buf->isShared());
}
// Create a combined IOBuf, clone from it, and then destroy the original
// IOBuf. The data buffer cannot be deleted until the clone is also
// destroyed.
{
auto bufA = IOBuf::createCombined(256);
EXPECT_FALSE(bufA->isShared());
auto bufB = bufA->clone();
EXPECT_TRUE(bufA->isShared());
EXPECT_TRUE(bufB->isShared());
bufA.reset();
EXPECT_FALSE(bufB->isShared());
}
// Create a combined IOBuf, then call reserve() to get a larger buffer.
// The IOBuf no longer points to the combined data buffer, but the
// overall memory segment cannot be deleted until the IOBuf is also
// destroyed.
{
auto buf = IOBuf::createCombined(256);
buf->reserve(0, buf->capacity() + 100);
}
// Create a combined IOBuf, clone from it, then call unshare() on the original
// buffer. This creates a situation where bufB is pointing at the combined
// buffer associated with bufA, but bufA is now using a different buffer.
auto testSwap = [](bool resetAFirst) {
auto bufA = IOBuf::createCombined(256);
EXPECT_FALSE(bufA->isShared());
auto bufB = bufA->clone();
EXPECT_TRUE(bufA->isShared());
EXPECT_TRUE(bufB->isShared());
bufA->unshare();
EXPECT_FALSE(bufA->isShared());
EXPECT_FALSE(bufB->isShared());
if (resetAFirst) {
bufA.reset();
bufB.reset();
} else {
bufB.reset();
bufA.reset();
}
};
testSwap(true);
testSwap(false);
}
void fillBuf(uint8_t* buf, uint32_t length, boost::mt19937& gen) { void fillBuf(uint8_t* buf, uint32_t length, boost::mt19937& gen) {
for (uint32_t n = 0; n < length; ++n) { for (uint32_t n = 0; n < length; ++n) {
buf[n] = static_cast<uint8_t>(gen() & 0xff); buf[n] = static_cast<uint8_t>(gen() & 0xff);
...@@ -392,8 +447,7 @@ TEST(IOBuf, Chaining) { ...@@ -392,8 +447,7 @@ TEST(IOBuf, Chaining) {
EXPECT_TRUE(iob1->isShared()); EXPECT_TRUE(iob1->isShared());
EXPECT_TRUE(iob1->isSharedOne()); EXPECT_TRUE(iob1->isSharedOne());
// since iob2 has a small internal buffer, it will never be shared EXPECT_TRUE(iob2ptr->isSharedOne());
EXPECT_FALSE(iob2ptr->isSharedOne());
EXPECT_TRUE(iob3ptr->isSharedOne()); EXPECT_TRUE(iob3ptr->isSharedOne());
EXPECT_TRUE(iob4ptr->isSharedOne()); EXPECT_TRUE(iob4ptr->isSharedOne());
EXPECT_TRUE(iob5ptr->isSharedOne()); EXPECT_TRUE(iob5ptr->isSharedOne());
......
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