Index: net/quic/quic_buffer_pool.h |
diff --git a/net/quic/quic_buffer_pool.h b/net/quic/quic_buffer_pool.h |
new file mode 100644 |
index 0000000000000000000000000000000000000000..85d64c5c752a4185be545a190479359612e5ae1e |
--- /dev/null |
+++ b/net/quic/quic_buffer_pool.h |
@@ -0,0 +1,486 @@ |
+// Copyright 2016 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+// A free list for character buffers, optimized to avoid TLB misses. |
+// |
+// This seeks to solve 2 problems: |
+// 1. Free lists are broadly beneficial to most malloc implementations that QUIC |
+// relies on, but especially TCMalloc. Certain QUIC allocations |
+// disproportionately fill up the TCMalloc caches, which leads to TCMalloc |
+// running in a less efficient manner than usual because its other buckets |
+// end up starved. |
+// 2. TCMalloc will release unused memory back to the system periodically. |
+// However, it does so out of contiguous allocations, which can lead to the |
+// physical memory becoming fragmented between the processes running. |
+// |
+// An ordinary FreeList solves the first problem, essentially by artificially |
+// inflating a particular TCMalloc bucket without impacting TCMalloc directly. |
+// However, it does nothing to solve the second problem. |
+// |
+// This free list instead uses huge blocks of memory (so-called "Regions"). Each |
+// block stores the block's free list and the blocks buffer pool. The huge block |
+// is opportunistically acquired as huge pages (a 2MB page) to avoid physical |
+// memory fragmentation, and to decrease the likelihood of TLB misses. |
+// Additionally, the region pointers are stored in the same storage as the |
+// buffer pool itself, which lends itself to avoiding a pointer dereference |
+// (again, further polluting the TLBs), at the expense of having a compiled-in |
+// maximum number of regions. |
+// |
+// Fundamentally, the QuicBufferPool supercedes a new char[] and delete[] pair. |
+// There is an additional New(size_t, bool), which makes the use of a |
+// QuicBufferPool flag-flippable. |
+// |
+// QuicBufferPool is not thread-safe. Using it without external locking will |
+// cause pool corruption. |
+// |
+// The memory returned by QuicBufferPool is always aligned to at least 16 bytes, |
+// so it can be used with SSE instructions without additional work. |
+// |
+// QuicBufferPool is errno-transparent; it makes system calls, but errno will |
+// always be restored to the original value after them. |
+// |
+// QuicBufferPool does not release memory until the whole object is destroyed. |
+// |
+#ifndef NET_QUIC_QUIC_BUFFER_POOL_H_ |
+#define NET_QUIC_QUIC_BUFFER_POOL_H_ |
+ |
+#include <algorithm> |
+#include <array> |
+#include <cstddef> |
+#include <limits> |
+#include <new> |
+#include <type_traits> |
+ |
+#include "base/logging.h" |
+#include "base/memory/aligned_memory.h" |
+#include "net/quic/quic_bug_tracker.h" |
+#include "net/quic/quic_protocol.h" |
+ |
+#define PREDICT_FALSE(x) x |
+#define PREDICT_TRUE(x) x |
+ |
+#define PROT_READ 0x04 |
+#define PROT_WRITE 0x02 |
+#define MAP_PRIVATE 0x02 |
+#define MAP_ANONYMOUS 0x20 |
+ |
+namespace net { |
+ |
+template <size_t MaxBufferSize, |
+ size_t MaxNumRegions, |
+ size_t RegionSize = 8 * 1024 * 1024> |
+class QuicBufferPool : public QuicBufferAllocator { |
+ public: |
+ QuicBufferPool() = default; |
+ explicit QuicBufferPool(base::LinkerInitialized linker_initialized); |
+ char* New(size_t size) override; |
+ char* New(size_t size, bool flag_enable) override; |
+ void Delete(char* buf) override; |
+ void MarkAllocatorIdle() override; |
+ |
+ private: |
+ friend class QuicBufferPoolTest; |
+ |
+ // The minimum alignment for objects in a region. |
+ static const size_t kMinAlignment = 16; |
+ |
+ static const size_t kAlignedMaxBufferSize = |
+ ((MaxBufferSize + kMinAlignment - 1) / kMinAlignment) * kMinAlignment; |
+ static_assert((RegionSize % 4096) == 0, "Regions must be page aligned."); |
+ static_assert((kAlignedMaxBufferSize % kMinAlignment) == 0, |
+ "Alignment calculation failure: aligned buffer size is not " |
+ "aligned to the minimum alignment."); |
+ static_assert(kAlignedMaxBufferSize >= MaxBufferSize, |
+ "Alignment calculation failure: aligned buffer size smaller " |
+ "than the max buffer size."); |
+ static_assert(MaxBufferSize + kMinAlignment > kAlignedMaxBufferSize, |
+ "Alignment calculation failure: aligned buffer size is larger " |
+ "than it needs to be."); |
+ |
+ // The actual size of an allocation within the region. |
+ static const size_t kAllocSize = kAlignedMaxBufferSize; |
+ |
+ // The usable region size includes kMinAlignment to ensure there's room to |
+ // align up after the free list. |
+ static const size_t kUsableRegionSize = RegionSize - kMinAlignment; |
+ |
+ // The number of buffers that can fit in the usable memory space. |
+ static const size_t kNumBuffersPerRegion = |
+ kUsableRegionSize / (kAllocSize + sizeof(char*)); |
+ |
+ // A region of memory that buffers can be allocated from. |
+ class Region { |
+ public: |
+ Region() = default; |
+ explicit Region(base::LinkerInitialized linker_initialized); |
+ ~Region(); |
+ |
+ // Allocates a region of memory and initializes the free list within that |
+ // memory. Has no effect if the region has already been initialized. If this |
+ // method fails, Initialize can be called again to try again later. |
+ bool Initialize(); |
+ |
+ // Releases the memory backing a region. |
+ void Release(); |
+ |
+ // Returns a new buffer of |kAllocSize| bytes. |
+ // Returns nullptr if there are no available slots. |
+ char* New(); |
+ |
+ // Releases a buffer. Must be called with memory within the region. |
+ void Delete(char* buf); |
+ |
+ // Returns true if the Region contains |buf|. |
+ bool Contains(char* buf) const; |
+ |
+ // Returns true if the Region has no outstanding allocations. |
+ bool Empty() const; |
+ |
+ // Swaps one region with another region. |
+ void Swap(Region* other); |
+ |
+ private: |
+ // The offset of the free list within the region. |
+ static const size_t kFreeListOffset = 0; |
+ |
+ // The offset of the first buffer within the region. |
+ static const size_t kBufferStartOffset = |
+ RegionSize - (kNumBuffersPerRegion * kAllocSize); |
+ |
+ static_assert( |
+ (kFreeListOffset % sizeof(char*)) == 0, |
+ "kFreeListOffset must be aligned to a free list entry boundary."); |
+ static_assert(kAlignedMaxBufferSize < kUsableRegionSize, |
+ "MaxBufferSize is too large to fit in a 2MB page."); |
+ static_assert( |
+ MaxBufferSize > sizeof(char*), |
+ "MaxBufferSize must be at least the size of a freelist entry."); |
+ static_assert(kNumBuffersPerRegion > 0, "Pool will not hold any buffers."); |
+ static_assert( |
+ kBufferStartOffset >= |
+ kFreeListOffset + (sizeof(char*) * kNumBuffersPerRegion), |
+ "Region offset calculation failure; freelist overlaps with buffers."); |
+ static_assert(kNumBuffersPerRegion < |
+ static_cast<size_t>(std::numeric_limits<int>::max()), |
+ "Region buffer calculation failure; more buffers can be " |
+ "stored in a region than are indexable by an int."); |
+ static_assert(static_cast<int>(kNumBuffersPerRegion * MaxNumRegions) == |
+ kNumBuffersPerRegion * MaxNumRegions, |
+ "The number of possible allocations in this pool will " |
+ "overflow an int."); |
+ |
+ // Returns a pointer to the linear free list. |
+ char** free_list() { |
+ return reinterpret_cast<char**>(base_ + kFreeListOffset); |
+ } |
+ |
+ // POSIX mmap, except errno is preserved. |
+ // clang-format off |
+ static void* MmapPreserveErrno(void* addr, |
+ size_t length, |
+ int prot, |
+ int flags, |
+ int fd, |
+ off_t offset); |
+ // clang-format on |
+ |
+ // POSIX munmap, except errno is preserved. |
+ static void MunmapPreserveErrno(void* addr, size_t length); |
+ |
+ char* base_ = nullptr; |
+ unsigned int next_free_ = 0; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(Region); |
+ }; |
+ |
+ // Returns heap-allocated memory. Only fails if malloc fails. |
+ static char* FallbackNew(size_t size); |
+ |
+ // Releases |buf| to the heap. |
+ static void FallbackDelete(char* buf); |
+ |
+ // The current number of buffers in use across all regions. |
+ unsigned int current_allocations_ = 0; |
+ |
+ // The maximum region in regions_ that was successfully initialized. |
+ unsigned int max_region_ = 0; |
+ std::array<Region, MaxNumRegions> regions_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(QuicBufferPool); |
+}; |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::QuicBufferPool( |
+ base::LinkerInitialized /* linker_initialized */) {} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+char* QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::New( |
+ size_t size) { |
+ if (PREDICT_FALSE(size > kAlignedMaxBufferSize)) { |
+ QUIC_BUG << "Request for buffer of size " << size |
+ << " greater than pool buffer size " << kAlignedMaxBufferSize; |
+ return FallbackNew(size); |
+ } |
+ |
+ // Lazy-initialize the first region if we need to. |
+ if (PREDICT_FALSE(max_region_ == 0)) { |
+ if (PREDICT_FALSE(!regions_[0].Initialize())) { |
+ QUIC_BUG << "Failed to initialize first region in buffer pool."; |
+ return FallbackNew(size); |
+ } |
+ max_region_ = 1; |
+ } |
+ |
+ // Find the first Region that can allocate some memory. Prioritize the lower |
+ // order regions so higher order regions can be released. |
+ for (unsigned int current_region = 0; current_region < max_region_; |
+ ++current_region) { |
+ Region& region = regions_[current_region]; |
+ char* const buf = region.New(); |
+ if (buf != nullptr) { |
+ ++current_allocations_; |
+ return buf; |
+ } |
+ } |
+ |
+ // If all of the regions are full and we can add more regions, try to add a |
+ // new one. Note that we never release regions until QuicBufferPool |
+ // destruction. |
+ if (PREDICT_TRUE(max_region_ < MaxNumRegions)) { |
+ Region& new_region = regions_[max_region_]; |
+ if (PREDICT_FALSE(!new_region.Initialize())) { |
+ QUIC_BUG << "Failed to initialize new region in buffer pool."; |
+ return FallbackNew(size); |
+ } |
+ // If this region was just initialized, it's guaranteed to be able to return |
+ // some memory. |
+ char* const buf = new_region.New(); |
+ if (PREDICT_TRUE(buf != nullptr)) { |
+ ++current_allocations_; |
+ ++max_region_; |
+ return buf; |
+ } else { |
+ QUIC_BUG << "Freshly initialized Region failed to allocate memory"; |
+ } |
+ } |
+ |
+ QUIC_BUG << "Ran out of room in QuicBufferPool."; |
+ return FallbackNew(size); |
+} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+char* QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::New( |
+ size_t size, |
+ bool flag_enable) { |
+ if (flag_enable) { |
+ return New(size); |
+ } else { |
+ return FallbackNew(size); |
+ } |
+} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+void QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Delete( |
+ char* buf) { |
+ if (buf == nullptr) { |
+ return; |
+ } |
+ for (unsigned int i = 0; i < max_region_; ++i) { |
+ Region* region = ®ions_[i]; |
+ if (region->Contains(buf)) { |
+ region->Delete(buf); |
+ // If the region is empty, move it to the back of the list of regions. |
+ if (region->Empty()) { |
+ DCHECK_GE(max_region_, 1u); |
+ region->Swap(®ions_[max_region_ - 1]); |
+ } |
+ --current_allocations_; |
+ if (max_region_ > 1 && regions_[max_region_ - 1].Empty() && |
+ current_allocations_ < ((max_region_ * kNumBuffersPerRegion) - |
+ (kNumBuffersPerRegion >> 1))) { |
+ // We have 1.5 Regions worth of memory free, and the last region is |
+ // completely empty. Let's release the last Region back to the OS; it |
+ // seems like we don't need it right now. Note that this will never |
+ // release regions_[0]. |
+ regions_[--max_region_].Release(); |
+ } |
+ return; |
+ } |
+ } |
+ FallbackDelete(buf); |
+} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+void QuicBufferPool<MaxBufferSize, |
+ MaxNumRegions, |
+ RegionSize>::MarkAllocatorIdle() { |
+ if (current_allocations_ == 0 && max_region_ == 1) { |
+ regions_[0].Release(); |
+ max_region_ = 0; |
+ } |
+} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Region::Region( |
+ base::LinkerInitialized /* linker_initialized */) {} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Region::~Region() { |
+ // If the region was allocated but all of the buffers in the region haven't |
+ // been freed yet, defer freeing until the last buffer is released. |
+ if (base_ != nullptr && next_free_ != 0) { |
+ QUIC_BUG << "Freeing " << this << " with " << next_free_ |
+ << " unfreed allocations."; |
+ MunmapPreserveErrno(base_, RegionSize); |
+ } |
+} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+bool QuicBufferPool<MaxBufferSize, |
+ MaxNumRegions, |
+ RegionSize>::Region::Initialize() { |
+ if (base_ != nullptr) { |
+ return true; |
+ } |
+ |
+ const int prot_flags = PROT_READ | PROT_WRITE; |
+ const int map_flags = MAP_PRIVATE | MAP_ANONYMOUS; |
+ // char* buf = static_cast<char*>(MAP_FAILED); |
+ char* buf = nullptr; |
+#ifdef MAP_HUGETLB |
+ // Try to get 2MB pages first. |
+ // clang-format off |
+ buf = static_cast<char*>(MmapPreserveErrno( |
+ static_cast<void*>(nullptr), |
+ RegionSize, |
+ prot_flags, |
+ map_flags | MAP_HUGETLB, |
+ -1, |
+ 0)); |
+#endif |
+ if (buf == nullptr) { |
+ // Otherwise we have to use 4K pages. |
+ buf = static_cast<char*>(MmapPreserveErrno( |
+ static_cast<void*>(nullptr), |
+ RegionSize, |
+ prot_flags, |
+ map_flags, |
+ -1, |
+ 0)); |
+ } |
+ // clang-format on |
+ |
+ if (PREDICT_FALSE(buf == nullptr)) { |
+ return false; |
+ } |
+ |
+ base_ = buf; |
+ |
+ // Walk the memory in linear order; the free list comes first, then comes the |
+ // memory. |
+ for (size_t i = 0; i < kNumBuffersPerRegion; ++i) { |
+ free_list()[i] = base_ + kBufferStartOffset + (kAllocSize * i); |
+ } |
+ |
+ return true; |
+} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+void QuicBufferPool<MaxBufferSize, |
+ MaxNumRegions, |
+ RegionSize>::Region::Release() { |
+ DCHECK(base_ != nullptr); |
+ DCHECK_EQ(0u, next_free_); |
+ if (base_ == nullptr) { |
+ return; |
+ } |
+ |
+ // We'll continue holding onto virtual address space, but the physical backing |
+ // is released. This is faster than munmap, and we have plenty of virtual |
+ // address space to burn. |
+ MunmapPreserveErrno(base_, RegionSize); |
+ base_ = nullptr; |
+ next_free_ = 0; |
+} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+char* QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Region::New() { |
+ if (next_free_ < kNumBuffersPerRegion) { |
+ return free_list()[next_free_++]; |
+ } else { |
+ return nullptr; |
+ } |
+} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+void QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Region::Delete( |
+ char* buf) { |
+ DCHECK_GT(next_free_, 0u); |
+ free_list()[--next_free_] = buf; |
+} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+bool QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Region::Contains( |
+ char* buf) const { |
+ return (base_ <= buf) && (buf + MaxBufferSize <= base_ + RegionSize); |
+} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+bool QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Region::Empty() |
+ const { |
+ return next_free_ == 0; |
+} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+void QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Region::Swap( |
+ Region* other) { |
+ if (this == other) { |
+ return; |
+ } |
+ using std::swap; |
+ swap(next_free_, other->next_free_); |
+ swap(base_, other->base_); |
+} |
+ |
+// clang-format off |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+void* QuicBufferPool<MaxBufferSize, MaxNumRegions, |
+ RegionSize>::Region::MmapPreserveErrno(void* addr, |
+ size_t length, |
+ int prot, |
+ int flags, |
+ int fd, |
+ off_t offset) { |
+ // clang-format on |
+ const int old_errno = errno; |
+ void* ret = base::AlignedAlloc(length, kMinAlignment); |
+ errno = old_errno; |
+ return ret; |
+} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+void QuicBufferPool<MaxBufferSize, |
+ MaxNumRegions, |
+ RegionSize>::Region::MunmapPreserveErrno(void* addr, |
+ size_t length) { |
+ const int old_errno = errno; |
+ base::AlignedFree(addr); |
+ errno = old_errno; |
+} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+char* QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::FallbackNew( |
+ size_t size) { |
+ return new char[size]; |
+} |
+ |
+template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
+void QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::FallbackDelete( |
+ char* buf) { |
+ delete[] buf; |
+} |
+ |
+} // namespace net |
+ |
+#endif // NET_QUIC_QUIC_BUFFER_POOL_H_ |