OLD | NEW |
(Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 // A free list for character buffers, optimized to avoid TLB misses. |
| 6 // |
| 7 // This seeks to solve 2 problems: |
| 8 // 1. Free lists are broadly beneficial to most malloc implementations that QUIC |
| 9 // relies on, but especially TCMalloc. Certain QUIC allocations |
| 10 // disproportionately fill up the TCMalloc caches, which leads to TCMalloc |
| 11 // running in a less efficient manner than usual because its other buckets |
| 12 // end up starved. |
| 13 // 2. TCMalloc will release unused memory back to the system periodically. |
| 14 // However, it does so out of contiguous allocations, which can lead to the |
| 15 // physical memory becoming fragmented between the processes running. |
| 16 // |
| 17 // An ordinary FreeList solves the first problem, essentially by artificially |
| 18 // inflating a particular TCMalloc bucket without impacting TCMalloc directly. |
| 19 // However, it does nothing to solve the second problem. |
| 20 // |
| 21 // This free list instead uses huge blocks of memory (so-called "Regions"). Each |
| 22 // block stores the block's free list and the blocks buffer pool. The huge block |
| 23 // is opportunistically acquired as huge pages (a 2MB page) to avoid physical |
| 24 // memory fragmentation, and to decrease the likelihood of TLB misses. |
| 25 // Additionally, the region pointers are stored in the same storage as the |
| 26 // buffer pool itself, which lends itself to avoiding a pointer dereference |
| 27 // (again, further polluting the TLBs), at the expense of having a compiled-in |
| 28 // maximum number of regions. |
| 29 // |
| 30 // Fundamentally, the QuicBufferPool supercedes a new char[] and delete[] pair. |
| 31 // There is an additional New(size_t, bool), which makes the use of a |
| 32 // QuicBufferPool flag-flippable. |
| 33 // |
| 34 // QuicBufferPool is not thread-safe. Using it without external locking will |
| 35 // cause pool corruption. |
| 36 // |
| 37 // The memory returned by QuicBufferPool is always aligned to at least 16 bytes, |
| 38 // so it can be used with SSE instructions without additional work. |
| 39 // |
| 40 // QuicBufferPool is errno-transparent; it makes system calls, but errno will |
| 41 // always be restored to the original value after them. |
| 42 // |
| 43 // QuicBufferPool does not release memory until the whole object is destroyed. |
| 44 // |
| 45 #ifndef NET_QUIC_QUIC_BUFFER_POOL_H_ |
| 46 #define NET_QUIC_QUIC_BUFFER_POOL_H_ |
| 47 |
| 48 #include <algorithm> |
| 49 #include <array> |
| 50 #include <cstddef> |
| 51 #include <limits> |
| 52 #include <new> |
| 53 #include <type_traits> |
| 54 |
| 55 #include "base/logging.h" |
| 56 #include "base/memory/aligned_memory.h" |
| 57 #include "net/quic/quic_bug_tracker.h" |
| 58 #include "net/quic/quic_protocol.h" |
| 59 |
| 60 #define PREDICT_FALSE(x) x |
| 61 #define PREDICT_TRUE(x) x |
| 62 |
| 63 #define PROT_READ 0x04 |
| 64 #define PROT_WRITE 0x02 |
| 65 #define MAP_PRIVATE 0x02 |
| 66 #define MAP_ANONYMOUS 0x20 |
| 67 |
| 68 namespace net { |
| 69 |
| 70 template <size_t MaxBufferSize, |
| 71 size_t MaxNumRegions, |
| 72 size_t RegionSize = 8 * 1024 * 1024> |
| 73 class QuicBufferPool : public QuicBufferAllocator { |
| 74 public: |
| 75 QuicBufferPool() = default; |
| 76 explicit QuicBufferPool(base::LinkerInitialized linker_initialized); |
| 77 char* New(size_t size) override; |
| 78 char* New(size_t size, bool flag_enable) override; |
| 79 void Delete(char* buf) override; |
| 80 void MarkAllocatorIdle() override; |
| 81 |
| 82 private: |
| 83 friend class QuicBufferPoolTest; |
| 84 |
| 85 // The minimum alignment for objects in a region. |
| 86 static const size_t kMinAlignment = 16; |
| 87 |
| 88 static const size_t kAlignedMaxBufferSize = |
| 89 ((MaxBufferSize + kMinAlignment - 1) / kMinAlignment) * kMinAlignment; |
| 90 static_assert((RegionSize % 4096) == 0, "Regions must be page aligned."); |
| 91 static_assert((kAlignedMaxBufferSize % kMinAlignment) == 0, |
| 92 "Alignment calculation failure: aligned buffer size is not " |
| 93 "aligned to the minimum alignment."); |
| 94 static_assert(kAlignedMaxBufferSize >= MaxBufferSize, |
| 95 "Alignment calculation failure: aligned buffer size smaller " |
| 96 "than the max buffer size."); |
| 97 static_assert(MaxBufferSize + kMinAlignment > kAlignedMaxBufferSize, |
| 98 "Alignment calculation failure: aligned buffer size is larger " |
| 99 "than it needs to be."); |
| 100 |
| 101 // The actual size of an allocation within the region. |
| 102 static const size_t kAllocSize = kAlignedMaxBufferSize; |
| 103 |
| 104 // The usable region size includes kMinAlignment to ensure there's room to |
| 105 // align up after the free list. |
| 106 static const size_t kUsableRegionSize = RegionSize - kMinAlignment; |
| 107 |
| 108 // The number of buffers that can fit in the usable memory space. |
| 109 static const size_t kNumBuffersPerRegion = |
| 110 kUsableRegionSize / (kAllocSize + sizeof(char*)); |
| 111 |
| 112 // A region of memory that buffers can be allocated from. |
| 113 class Region { |
| 114 public: |
| 115 Region() = default; |
| 116 explicit Region(base::LinkerInitialized linker_initialized); |
| 117 ~Region(); |
| 118 |
| 119 // Allocates a region of memory and initializes the free list within that |
| 120 // memory. Has no effect if the region has already been initialized. If this |
| 121 // method fails, Initialize can be called again to try again later. |
| 122 bool Initialize(); |
| 123 |
| 124 // Releases the memory backing a region. |
| 125 void Release(); |
| 126 |
| 127 // Returns a new buffer of |kAllocSize| bytes. |
| 128 // Returns nullptr if there are no available slots. |
| 129 char* New(); |
| 130 |
| 131 // Releases a buffer. Must be called with memory within the region. |
| 132 void Delete(char* buf); |
| 133 |
| 134 // Returns true if the Region contains |buf|. |
| 135 bool Contains(char* buf) const; |
| 136 |
| 137 // Returns true if the Region has no outstanding allocations. |
| 138 bool Empty() const; |
| 139 |
| 140 // Swaps one region with another region. |
| 141 void Swap(Region* other); |
| 142 |
| 143 private: |
| 144 // The offset of the free list within the region. |
| 145 static const size_t kFreeListOffset = 0; |
| 146 |
| 147 // The offset of the first buffer within the region. |
| 148 static const size_t kBufferStartOffset = |
| 149 RegionSize - (kNumBuffersPerRegion * kAllocSize); |
| 150 |
| 151 static_assert( |
| 152 (kFreeListOffset % sizeof(char*)) == 0, |
| 153 "kFreeListOffset must be aligned to a free list entry boundary."); |
| 154 static_assert(kAlignedMaxBufferSize < kUsableRegionSize, |
| 155 "MaxBufferSize is too large to fit in a 2MB page."); |
| 156 static_assert( |
| 157 MaxBufferSize > sizeof(char*), |
| 158 "MaxBufferSize must be at least the size of a freelist entry."); |
| 159 static_assert(kNumBuffersPerRegion > 0, "Pool will not hold any buffers."); |
| 160 static_assert( |
| 161 kBufferStartOffset >= |
| 162 kFreeListOffset + (sizeof(char*) * kNumBuffersPerRegion), |
| 163 "Region offset calculation failure; freelist overlaps with buffers."); |
| 164 static_assert(kNumBuffersPerRegion < |
| 165 static_cast<size_t>(std::numeric_limits<int>::max()), |
| 166 "Region buffer calculation failure; more buffers can be " |
| 167 "stored in a region than are indexable by an int."); |
| 168 static_assert(static_cast<int>(kNumBuffersPerRegion * MaxNumRegions) == |
| 169 kNumBuffersPerRegion * MaxNumRegions, |
| 170 "The number of possible allocations in this pool will " |
| 171 "overflow an int."); |
| 172 |
| 173 // Returns a pointer to the linear free list. |
| 174 char** free_list() { |
| 175 return reinterpret_cast<char**>(base_ + kFreeListOffset); |
| 176 } |
| 177 |
| 178 // POSIX mmap, except errno is preserved. |
| 179 // clang-format off |
| 180 static void* MmapPreserveErrno(void* addr, |
| 181 size_t length, |
| 182 int prot, |
| 183 int flags, |
| 184 int fd, |
| 185 off_t offset); |
| 186 // clang-format on |
| 187 |
| 188 // POSIX munmap, except errno is preserved. |
| 189 static void MunmapPreserveErrno(void* addr, size_t length); |
| 190 |
| 191 char* base_ = nullptr; |
| 192 unsigned int next_free_ = 0; |
| 193 |
| 194 DISALLOW_COPY_AND_ASSIGN(Region); |
| 195 }; |
| 196 |
| 197 // Returns heap-allocated memory. Only fails if malloc fails. |
| 198 static char* FallbackNew(size_t size); |
| 199 |
| 200 // Releases |buf| to the heap. |
| 201 static void FallbackDelete(char* buf); |
| 202 |
| 203 // The current number of buffers in use across all regions. |
| 204 unsigned int current_allocations_ = 0; |
| 205 |
| 206 // The maximum region in regions_ that was successfully initialized. |
| 207 unsigned int max_region_ = 0; |
| 208 std::array<Region, MaxNumRegions> regions_; |
| 209 |
| 210 DISALLOW_COPY_AND_ASSIGN(QuicBufferPool); |
| 211 }; |
| 212 |
| 213 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 214 QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::QuicBufferPool( |
| 215 base::LinkerInitialized /* linker_initialized */) {} |
| 216 |
| 217 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 218 char* QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::New( |
| 219 size_t size) { |
| 220 if (PREDICT_FALSE(size > kAlignedMaxBufferSize)) { |
| 221 QUIC_BUG << "Request for buffer of size " << size |
| 222 << " greater than pool buffer size " << kAlignedMaxBufferSize; |
| 223 return FallbackNew(size); |
| 224 } |
| 225 |
| 226 // Lazy-initialize the first region if we need to. |
| 227 if (PREDICT_FALSE(max_region_ == 0)) { |
| 228 if (PREDICT_FALSE(!regions_[0].Initialize())) { |
| 229 QUIC_BUG << "Failed to initialize first region in buffer pool."; |
| 230 return FallbackNew(size); |
| 231 } |
| 232 max_region_ = 1; |
| 233 } |
| 234 |
| 235 // Find the first Region that can allocate some memory. Prioritize the lower |
| 236 // order regions so higher order regions can be released. |
| 237 for (unsigned int current_region = 0; current_region < max_region_; |
| 238 ++current_region) { |
| 239 Region& region = regions_[current_region]; |
| 240 char* const buf = region.New(); |
| 241 if (buf != nullptr) { |
| 242 ++current_allocations_; |
| 243 return buf; |
| 244 } |
| 245 } |
| 246 |
| 247 // If all of the regions are full and we can add more regions, try to add a |
| 248 // new one. Note that we never release regions until QuicBufferPool |
| 249 // destruction. |
| 250 if (PREDICT_TRUE(max_region_ < MaxNumRegions)) { |
| 251 Region& new_region = regions_[max_region_]; |
| 252 if (PREDICT_FALSE(!new_region.Initialize())) { |
| 253 QUIC_BUG << "Failed to initialize new region in buffer pool."; |
| 254 return FallbackNew(size); |
| 255 } |
| 256 // If this region was just initialized, it's guaranteed to be able to return |
| 257 // some memory. |
| 258 char* const buf = new_region.New(); |
| 259 if (PREDICT_TRUE(buf != nullptr)) { |
| 260 ++current_allocations_; |
| 261 ++max_region_; |
| 262 return buf; |
| 263 } else { |
| 264 QUIC_BUG << "Freshly initialized Region failed to allocate memory"; |
| 265 } |
| 266 } |
| 267 |
| 268 QUIC_BUG << "Ran out of room in QuicBufferPool."; |
| 269 return FallbackNew(size); |
| 270 } |
| 271 |
| 272 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 273 char* QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::New( |
| 274 size_t size, |
| 275 bool flag_enable) { |
| 276 if (flag_enable) { |
| 277 return New(size); |
| 278 } else { |
| 279 return FallbackNew(size); |
| 280 } |
| 281 } |
| 282 |
| 283 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 284 void QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Delete( |
| 285 char* buf) { |
| 286 if (buf == nullptr) { |
| 287 return; |
| 288 } |
| 289 for (unsigned int i = 0; i < max_region_; ++i) { |
| 290 Region* region = ®ions_[i]; |
| 291 if (region->Contains(buf)) { |
| 292 region->Delete(buf); |
| 293 // If the region is empty, move it to the back of the list of regions. |
| 294 if (region->Empty()) { |
| 295 DCHECK_GE(max_region_, 1u); |
| 296 region->Swap(®ions_[max_region_ - 1]); |
| 297 } |
| 298 --current_allocations_; |
| 299 if (max_region_ > 1 && regions_[max_region_ - 1].Empty() && |
| 300 current_allocations_ < ((max_region_ * kNumBuffersPerRegion) - |
| 301 (kNumBuffersPerRegion >> 1))) { |
| 302 // We have 1.5 Regions worth of memory free, and the last region is |
| 303 // completely empty. Let's release the last Region back to the OS; it |
| 304 // seems like we don't need it right now. Note that this will never |
| 305 // release regions_[0]. |
| 306 regions_[--max_region_].Release(); |
| 307 } |
| 308 return; |
| 309 } |
| 310 } |
| 311 FallbackDelete(buf); |
| 312 } |
| 313 |
| 314 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 315 void QuicBufferPool<MaxBufferSize, |
| 316 MaxNumRegions, |
| 317 RegionSize>::MarkAllocatorIdle() { |
| 318 if (current_allocations_ == 0 && max_region_ == 1) { |
| 319 regions_[0].Release(); |
| 320 max_region_ = 0; |
| 321 } |
| 322 } |
| 323 |
| 324 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 325 QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Region::Region( |
| 326 base::LinkerInitialized /* linker_initialized */) {} |
| 327 |
| 328 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 329 QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Region::~Region() { |
| 330 // If the region was allocated but all of the buffers in the region haven't |
| 331 // been freed yet, defer freeing until the last buffer is released. |
| 332 if (base_ != nullptr && next_free_ != 0) { |
| 333 QUIC_BUG << "Freeing " << this << " with " << next_free_ |
| 334 << " unfreed allocations."; |
| 335 MunmapPreserveErrno(base_, RegionSize); |
| 336 } |
| 337 } |
| 338 |
| 339 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 340 bool QuicBufferPool<MaxBufferSize, |
| 341 MaxNumRegions, |
| 342 RegionSize>::Region::Initialize() { |
| 343 if (base_ != nullptr) { |
| 344 return true; |
| 345 } |
| 346 |
| 347 const int prot_flags = PROT_READ | PROT_WRITE; |
| 348 const int map_flags = MAP_PRIVATE | MAP_ANONYMOUS; |
| 349 // char* buf = static_cast<char*>(MAP_FAILED); |
| 350 char* buf = nullptr; |
| 351 #ifdef MAP_HUGETLB |
| 352 // Try to get 2MB pages first. |
| 353 // clang-format off |
| 354 buf = static_cast<char*>(MmapPreserveErrno( |
| 355 static_cast<void*>(nullptr), |
| 356 RegionSize, |
| 357 prot_flags, |
| 358 map_flags | MAP_HUGETLB, |
| 359 -1, |
| 360 0)); |
| 361 #endif |
| 362 if (buf == nullptr) { |
| 363 // Otherwise we have to use 4K pages. |
| 364 buf = static_cast<char*>(MmapPreserveErrno( |
| 365 static_cast<void*>(nullptr), |
| 366 RegionSize, |
| 367 prot_flags, |
| 368 map_flags, |
| 369 -1, |
| 370 0)); |
| 371 } |
| 372 // clang-format on |
| 373 |
| 374 if (PREDICT_FALSE(buf == nullptr)) { |
| 375 return false; |
| 376 } |
| 377 |
| 378 base_ = buf; |
| 379 |
| 380 // Walk the memory in linear order; the free list comes first, then comes the |
| 381 // memory. |
| 382 for (size_t i = 0; i < kNumBuffersPerRegion; ++i) { |
| 383 free_list()[i] = base_ + kBufferStartOffset + (kAllocSize * i); |
| 384 } |
| 385 |
| 386 return true; |
| 387 } |
| 388 |
| 389 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 390 void QuicBufferPool<MaxBufferSize, |
| 391 MaxNumRegions, |
| 392 RegionSize>::Region::Release() { |
| 393 DCHECK(base_ != nullptr); |
| 394 DCHECK_EQ(0u, next_free_); |
| 395 if (base_ == nullptr) { |
| 396 return; |
| 397 } |
| 398 |
| 399 // We'll continue holding onto virtual address space, but the physical backing |
| 400 // is released. This is faster than munmap, and we have plenty of virtual |
| 401 // address space to burn. |
| 402 MunmapPreserveErrno(base_, RegionSize); |
| 403 base_ = nullptr; |
| 404 next_free_ = 0; |
| 405 } |
| 406 |
| 407 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 408 char* QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Region::New() { |
| 409 if (next_free_ < kNumBuffersPerRegion) { |
| 410 return free_list()[next_free_++]; |
| 411 } else { |
| 412 return nullptr; |
| 413 } |
| 414 } |
| 415 |
| 416 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 417 void QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Region::Delete( |
| 418 char* buf) { |
| 419 DCHECK_GT(next_free_, 0u); |
| 420 free_list()[--next_free_] = buf; |
| 421 } |
| 422 |
| 423 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 424 bool QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Region::Contains( |
| 425 char* buf) const { |
| 426 return (base_ <= buf) && (buf + MaxBufferSize <= base_ + RegionSize); |
| 427 } |
| 428 |
| 429 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 430 bool QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Region::Empty() |
| 431 const { |
| 432 return next_free_ == 0; |
| 433 } |
| 434 |
| 435 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 436 void QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::Region::Swap( |
| 437 Region* other) { |
| 438 if (this == other) { |
| 439 return; |
| 440 } |
| 441 using std::swap; |
| 442 swap(next_free_, other->next_free_); |
| 443 swap(base_, other->base_); |
| 444 } |
| 445 |
| 446 // clang-format off |
| 447 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 448 void* QuicBufferPool<MaxBufferSize, MaxNumRegions, |
| 449 RegionSize>::Region::MmapPreserveErrno(void* addr, |
| 450 size_t length, |
| 451 int prot, |
| 452 int flags, |
| 453 int fd, |
| 454 off_t offset) { |
| 455 // clang-format on |
| 456 const int old_errno = errno; |
| 457 void* ret = base::AlignedAlloc(length, kMinAlignment); |
| 458 errno = old_errno; |
| 459 return ret; |
| 460 } |
| 461 |
| 462 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 463 void QuicBufferPool<MaxBufferSize, |
| 464 MaxNumRegions, |
| 465 RegionSize>::Region::MunmapPreserveErrno(void* addr, |
| 466 size_t length) { |
| 467 const int old_errno = errno; |
| 468 base::AlignedFree(addr); |
| 469 errno = old_errno; |
| 470 } |
| 471 |
| 472 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 473 char* QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::FallbackNew( |
| 474 size_t size) { |
| 475 return new char[size]; |
| 476 } |
| 477 |
| 478 template <size_t MaxBufferSize, size_t MaxNumRegions, size_t RegionSize> |
| 479 void QuicBufferPool<MaxBufferSize, MaxNumRegions, RegionSize>::FallbackDelete( |
| 480 char* buf) { |
| 481 delete[] buf; |
| 482 } |
| 483 |
| 484 } // namespace net |
| 485 |
| 486 #endif // NET_QUIC_QUIC_BUFFER_POOL_H_ |
OLD | NEW |