Skip to content

Commit e467b97

Browse files
committed
Handle word size, add benchmark, return construction error
1 parent 36d34f7 commit e467b97

3 files changed

Lines changed: 147 additions & 26 deletions

File tree

mem/buffer_pool.go

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package mem
2020

2121
import (
22+
"fmt"
2223
"math/bits"
2324
"slices"
2425
"sort"
@@ -51,10 +52,17 @@ var defaultBufferPoolSizeExponents = []uint8{
5152
20, // 1MB
5253
}
5354

54-
var defaultBufferPool BufferPool
55+
var (
56+
defaultBufferPool BufferPool
57+
uintSize = bits.UintSize // use a variable for mocking during tests.
58+
)
5559

5660
func init() {
57-
defaultBufferPool = NewBinaryTieredBufferPool(defaultBufferPoolSizeExponents...)
61+
var err error
62+
defaultBufferPool, err = NewBinaryTieredBufferPool(defaultBufferPoolSizeExponents...)
63+
if err != nil {
64+
panic(fmt.Sprintf("Failed to create default buffer pool: %v", err))
65+
}
5866

5967
internal.SetDefaultBufferPoolForTesting = func(pool BufferPool) {
6068
defaultBufferPool = pool
@@ -127,37 +135,33 @@ type binaryTieredBufferPool struct {
127135
maxPoolCap int // Optimization: Cache max capacity
128136
}
129137

130-
// NewBinaryTieredBufferPool returns a BufferPool implementation that uses
131-
// multiple underlying pools of the given pool sizes. The pool sizes must be
132-
// powers of 2. This enables O(1) lookup when getting or putting a buffer.
138+
// NewBinaryTieredBufferPool returns a BufferPool backed by multiple sub-pools.
139+
// This structure enables O(1) lookup time for Get and Put operations.
133140
//
134-
// Note that the argument passed to this functions are the powers of 2 of the
135-
// capacity of the buffers in the pool, not the capacities of the buffers
136-
// themselves. For example, if you wanted a pool that had buffers with a capacity
137-
// of 16kb, you would pass 14 as the argument to this function.
138-
func NewBinaryTieredBufferPool(powerOfTwoExponents ...uint8) BufferPool {
141+
// The arguments provided are the exponents for the buffer capacities (powers
142+
// of 2), not the raw byte sizes. For example, to create a pool of 16KB buffers
143+
// (2^14 bytes), pass 14 as the argument.
144+
func NewBinaryTieredBufferPool(powerOfTwoExponents ...uint8) (BufferPool, error) {
139145
slices.Sort(powerOfTwoExponents)
140146

141-
// Determine the maximum exponent we need to support.
142-
// bits.Len64(math.MaxUint64) is 63.
143-
const maxExponent = 63
147+
// Determine the maximum exponent we need to support. This depends on the
148+
// word size (32-bit vs 64-bit).
149+
maxExponent := uintSize - 1
144150
indexOfNextLargestBit := slices.Repeat([]int{-1}, maxExponent+1)
145151
indexOfPreviousLargestBit := slices.Repeat([]int{-1}, maxExponent+1)
146152

147-
maxCap := 0
153+
maxTier := 0
148154
pools := make([]*sizedBufferPool, 0, len(powerOfTwoExponents))
149155

150156
for i, exp := range powerOfTwoExponents {
151-
// Allocating slices of size > 2^maxExponent isn't possible on 64-bit
152-
// machines.
153-
if exp > maxExponent {
154-
continue
155-
}
156-
capSize := 1 << exp
157-
pools = append(pools, newSizedBufferPool(capSize))
158-
if capSize > maxCap {
159-
maxCap = capSize
157+
// Allocating slices of size > 2^maxExponent isn't possible on
158+
// maxExponent-bit machines.
159+
if int(exp) > maxExponent {
160+
return nil, fmt.Errorf("allocating slice of size 2^%d is not possible", exp)
160161
}
162+
tierSize := 1 << exp
163+
pools = append(pools, newSizedBufferPool(tierSize))
164+
maxTier = max(maxTier, tierSize)
161165

162166
// Map the exact power of 2 to this pool index.
163167
indexOfNextLargestBit[exp] = i
@@ -184,8 +188,8 @@ func NewBinaryTieredBufferPool(powerOfTwoExponents ...uint8) BufferPool {
184188
exponentToNextLargestPoolMap: indexOfNextLargestBit,
185189
exponentToPreviousLargestPoolMap: indexOfPreviousLargestBit,
186190
sizedPools: pools,
187-
maxPoolCap: maxCap,
188-
}
191+
maxPoolCap: maxTier,
192+
}, nil
189193
}
190194

191195
func (b *binaryTieredBufferPool) Get(size int) *[]byte {

mem/buffer_pool_internal_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
*
3+
* Copyright 2026 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package mem
20+
21+
import (
22+
"testing"
23+
)
24+
25+
func TestNewBinaryTieredBufferPool_WordSize(t *testing.T) {
26+
origUintSize := uintSize
27+
defer func() { uintSize = origUintSize }()
28+
29+
tests := []struct {
30+
name string
31+
wordSize int
32+
exponents []uint8
33+
wantErr bool
34+
}{
35+
{
36+
name: "32-bit valid exponent",
37+
wordSize: 32,
38+
exponents: []uint8{31},
39+
wantErr: false,
40+
},
41+
{
42+
name: "32-bit invalid exponent",
43+
wordSize: 32,
44+
exponents: []uint8{32},
45+
wantErr: true,
46+
},
47+
{
48+
name: "64-bit valid exponent",
49+
wordSize: 64,
50+
exponents: []uint8{63},
51+
wantErr: false,
52+
},
53+
{
54+
name: "64-bit invalid exponent",
55+
wordSize: 64,
56+
exponents: []uint8{64},
57+
wantErr: true,
58+
},
59+
}
60+
61+
for _, tt := range tests {
62+
t.Run(tt.name, func(t *testing.T) {
63+
uintSize = tt.wordSize
64+
pool, err := NewBinaryTieredBufferPool(tt.exponents...)
65+
if (err != nil) != tt.wantErr {
66+
t.Errorf("NewBinaryTieredBufferPool() error = %t, wantErr %t", err, tt.wantErr)
67+
return
68+
}
69+
if err == nil {
70+
bp := pool.(*binaryTieredBufferPool)
71+
if len(bp.exponentToNextLargestPoolMap) != tt.wordSize {
72+
t.Errorf("exponentToNextLargestPoolMap length = %d, want %d", len(bp.exponentToNextLargestPoolMap), tt.wordSize)
73+
}
74+
if len(bp.exponentToPreviousLargestPoolMap) != tt.wordSize {
75+
t.Errorf("exponentToPreviousLargestPoolMap length = %d, want %d", len(bp.exponentToPreviousLargestPoolMap), tt.wordSize)
76+
}
77+
}
78+
})
79+
}
80+
}
81+
82+
// BenchmarkTieredPool benchmarks the performance of the tiered buffer pool
83+
// implementations, specifically focusing on the overhead of selecting the
84+
// correct bucket for a given size.
85+
func BenchmarkTieredPool(b *testing.B) {
86+
defaultBufferPoolSizes := make([]int, len(defaultBufferPoolSizeExponents))
87+
for i, exp := range defaultBufferPoolSizeExponents {
88+
defaultBufferPoolSizes[i] = 1 << exp
89+
}
90+
b.Run("pool=Tiered", func(b *testing.B) {
91+
p := NewTieredBufferPool(defaultBufferPoolSizes...).(*tieredBufferPool)
92+
for b.Loop() {
93+
for size := range 1 << 19 {
94+
// One for get, one for put.
95+
_ = p.getPool(size)
96+
_ = p.getPool(size)
97+
}
98+
}
99+
})
100+
101+
b.Run("pool=BinaryTiered", func(b *testing.B) {
102+
pool, err := NewBinaryTieredBufferPool(defaultBufferPoolSizeExponents...)
103+
p := pool.(*binaryTieredBufferPool)
104+
if err != nil {
105+
b.Fatalf("Faield to create buffer pool: %v", err)
106+
}
107+
for b.Loop() {
108+
for size := range 1 << 19 {
109+
_ = p.poolForGet(size)
110+
_ = p.poolForPut(size)
111+
}
112+
}
113+
})
114+
}

mem/buffer_pool_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,10 @@ func TestBinaryBufferPool(t *testing.T) {
131131

132132
for _, tc := range testCases {
133133
t.Run(fmt.Sprintf("requestSize=%d", tc.requestSize), func(t *testing.T) {
134-
pool := mem.NewBinaryTieredBufferPool(poolSizes...)
134+
pool, err := mem.NewBinaryTieredBufferPool(poolSizes...)
135+
if err != nil {
136+
t.Fatalf("Failed to create buffer pool: %v", err)
137+
}
135138
buf := pool.Get(tc.requestSize)
136139
if cap(*buf) != tc.wantCapacity {
137140
t.Errorf("Get(%d) returned buffer with capacity: %d, want %d", tc.requestSize, cap(*buf), tc.wantCapacity)

0 commit comments

Comments
 (0)