-
Notifications
You must be signed in to change notification settings - Fork 4.7k
mem: Add faster tiered buffer pool #8775
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
1f26e66
e7b7c57
36d34f7
e467b97
f32f229
f9f1a1d
2d102c7
3b88c26
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ | |
| package mem | ||
|
|
||
| import ( | ||
| "math/bits" | ||
| "sort" | ||
| "sync" | ||
|
|
||
|
|
@@ -38,20 +39,21 @@ type BufferPool interface { | |
| Put(*[]byte) | ||
| } | ||
|
|
||
| const goPageSize = 4 << 10 // 4KiB. N.B. this must be a power of 2. | ||
| const goPageSizeExponent = 12 | ||
| const goPageSize = 1 << goPageSizeExponent // 4KiB. N.B. this must be a power of 2. | ||
|
|
||
| var defaultBufferPoolSizes = []int{ | ||
| 256, | ||
| goPageSize, | ||
| 16 << 10, // 16KB (max HTTP/2 frame size used by gRPC) | ||
| 32 << 10, // 32KB (default buffer size for io.Copy) | ||
| 1 << 20, // 1MB | ||
| var defaultBufferPoolSizeExponents = []int{ | ||
| 8, | ||
| goPageSizeExponent, | ||
| 14, // 16KB (max HTTP/2 frame size used by gRPC) | ||
| 15, // 32KB (default buffer size for io.Copy) | ||
| 20, // 1MB | ||
| } | ||
|
|
||
| var defaultBufferPool BufferPool | ||
|
|
||
| func init() { | ||
| defaultBufferPool = NewTieredBufferPool(defaultBufferPoolSizes...) | ||
| defaultBufferPool = NewBinaryTieredBufferPool(defaultBufferPoolSizeExponents...) | ||
|
|
||
| internal.SetDefaultBufferPoolForTesting = func(pool BufferPool) { | ||
| defaultBufferPool = pool | ||
|
|
@@ -109,6 +111,132 @@ func (p *tieredBufferPool) getPool(size int) BufferPool { | |
| return p.sizedPools[poolIdx] | ||
| } | ||
|
|
||
| type binaryTieredBufferPool struct { | ||
| indexOfNextLargestBit []int | ||
| indexOfPreviousLargestBit []int | ||
| sizedPools []*sizedBufferPool | ||
| fallbackPool simpleBufferPool | ||
| maxPoolCap int // Optimization: Cache max capacity | ||
| } | ||
|
|
||
| // NewBinaryTieredBufferPool returns a BufferPool implementation that uses | ||
| // multiple underlying pools of the given pool sizes. The pool sizes must be | ||
| // powers of 2. This enables O(1) lookup when getting or putting a buffer. | ||
|
easwars marked this conversation as resolved.
Outdated
|
||
| // | ||
| // Note that the argument passed to this functions are the powers of 2 of the | ||
| // capacity of the buffers in the pool, not the capacities of the buffers | ||
| // themselves. For example, if you wanted a pool that had buffers with a capacity | ||
| // of 16kb, you would pass 14 as the argument to this function. | ||
| func NewBinaryTieredBufferPool(powerOfTwoExponents ...int) BufferPool { | ||
| sort.Ints(powerOfTwoExponents) | ||
|
|
||
| // Determine the maximum exponent we need to support. | ||
| // bits.Len64(math.MaxUint64) is 63. | ||
| const maxExponent = 63 | ||
|
easwars marked this conversation as resolved.
Outdated
|
||
| indexOfNextLargestBit := make([]int, maxExponent+1) | ||
| indexOfPreviousLargestBit := make([]int, maxExponent+1) | ||
|
|
||
| // Initialize with sentinel values | ||
| for i := range indexOfNextLargestBit { | ||
| indexOfNextLargestBit[i] = -1 | ||
| indexOfPreviousLargestBit[i] = -1 | ||
| } | ||
|
easwars marked this conversation as resolved.
Outdated
|
||
|
|
||
| maxCap := 0 | ||
| pools := make([]*sizedBufferPool, 0, len(powerOfTwoExponents)) | ||
|
|
||
| for i, exp := range powerOfTwoExponents { | ||
| if exp > maxExponent || exp < 0 { | ||
| continue | ||
| } | ||
|
easwars marked this conversation as resolved.
Outdated
|
||
| capSize := 1 << exp | ||
| pools = append(pools, newSizedBufferPool(capSize)) | ||
| if capSize > maxCap { | ||
| maxCap = capSize | ||
| } | ||
|
|
||
| // Map the exact power of 2 to this pool index. | ||
| indexOfNextLargestBit[exp] = i | ||
| indexOfPreviousLargestBit[exp] = i | ||
| } | ||
|
|
||
| // Fill gaps for Get() (Next Largest) | ||
| // We iterate backwards. If current is empty, take the value from the right (larger). | ||
| for i := maxExponent - 1; i >= 0; i-- { | ||
| if indexOfNextLargestBit[i] == -1 { | ||
| indexOfNextLargestBit[i] = indexOfNextLargestBit[i+1] | ||
| } | ||
| } | ||
|
|
||
| // Fill gaps for Put() (Previous Largest) | ||
| // We iterate forwards. If current is empty, take the value from the left (smaller). | ||
| for i := 1; i <= maxExponent; i++ { | ||
| if indexOfPreviousLargestBit[i] == -1 { | ||
| indexOfPreviousLargestBit[i] = indexOfPreviousLargestBit[i-1] | ||
| } | ||
| } | ||
|
|
||
| return &binaryTieredBufferPool{ | ||
| indexOfNextLargestBit: indexOfNextLargestBit, | ||
| indexOfPreviousLargestBit: indexOfPreviousLargestBit, | ||
| sizedPools: pools, | ||
| maxPoolCap: maxCap, | ||
| } | ||
| } | ||
|
|
||
| func (b *binaryTieredBufferPool) Get(size int) *[]byte { | ||
| return b.poolForGet(size).Get(size) | ||
| } | ||
|
|
||
| func (b *binaryTieredBufferPool) poolForGet(size int) BufferPool { | ||
| if size == 0 || size > b.maxPoolCap { | ||
| return &b.fallbackPool | ||
| } | ||
|
|
||
| // Calculate the exponent of the smallest power of 2 >= size. | ||
| // We subtract 1 from size to handle exact powers of 2 correctly. | ||
| // | ||
| // Examples: | ||
| // size=16 (0b10000) -> size-1=15 (0b01111) -> bits.Len=4 -> Pool for 2^4 | ||
| // size=17 (0b10001) -> size-1=16 (0b10000) -> bits.Len=5 -> Pool for 2^5 | ||
| querySize := uint(size - 1) | ||
| poolIdx := b.indexOfNextLargestBit[bits.Len(querySize)] | ||
|
|
||
| return b.sizedPools[poolIdx] | ||
| } | ||
|
|
||
| func (b *binaryTieredBufferPool) Put(buf *[]byte) { | ||
| b.poolForPut(cap(*buf)).Put(buf) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a comment here capturing the subtle but important fact that we are passing the capacity of the buffer, and not the size of the buffer to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a comment. |
||
| } | ||
|
|
||
| func (b *binaryTieredBufferPool) poolForPut(bCap int) BufferPool { | ||
| if bCap == 0 { | ||
| return NopBufferPool{} | ||
| } | ||
| if bCap > b.maxPoolCap { | ||
| return &b.fallbackPool | ||
| } | ||
| // Find the pool with the largest capacity <= bCap. | ||
| // | ||
| // We calculate the exponent of the largest power of 2 <= bCap. | ||
| // bits.Len(x) returns the minimum number of bits required to represent x; | ||
| // i.e. the number of bits up to and including the most significant bit. | ||
| // Subtracting 1 gives the 0-based index of the most significant bit, | ||
| // which is the exponent of the largest power of 2 <= bCap. | ||
| // | ||
| // Examples: | ||
| // cap=16 (0b10000) -> Len=5 -> 5-1=4 -> 2^4 | ||
| // cap=15 (0b01111) -> Len=4 -> 4-1=3 -> 2^3 | ||
| largestPowerOfTwo := bits.Len(uint(bCap)) - 1 | ||
| poolIdx := b.indexOfPreviousLargestBit[largestPowerOfTwo] | ||
| // The buffer is smaller than the smallest power of 2, discard it. | ||
| if poolIdx == -1 { | ||
| // Buffer is smaller than our smallest pool bucket. | ||
| return NopBufferPool{} | ||
| } | ||
| return b.sizedPools[poolIdx] | ||
| } | ||
|
|
||
| // sizedBufferPool is a BufferPool implementation that is optimized for specific | ||
| // buffer sizes. For example, HTTP/2 frames within gRPC have a default max size | ||
| // of 16kb and a sizedBufferPool can be configured to only return buffers with a | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.