Skip to content

Commit bcdd1ce

Browse files
committed
add buffer pooling and smaller performance fixes
Benchmark iteration (rough medians, -benchmem -count=5) Benchmark Before (~ns/op) After (~ns/op) CompressInodes_Small ~9.8µs ~8.2µs CompressInodes_Large ~13.4µs ~11.4µs CompressInodes_OverPool ~41.5µs ~32.6µs DecompressPage ~14.7µs ~8.7µs Signed-off-by: Thomas Jungblut <tjungblu@redhat.com>
1 parent 45df6bf commit bcdd1ce

2 files changed

Lines changed: 122 additions & 17 deletions

File tree

internal/common/compression.go

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,44 @@ import (
1414
// the decompressor to know exactly how many bytes to read.
1515
const compressedDataLenSize = 4
1616

17+
// maxPooledCompressionBytes is the maximum scratch size for which we use
18+
// pooled buffers in CompressInodes. Larger serializations allocate as needed.
19+
// maxPooledCompressionBytes is set as a multiple of the default bbolt page size (4096),
20+
// so pooled buffers efficiently match or exceed typical page-based serialization.
21+
var maxPooledCompressionBytes = 16 * DefaultPageSize
22+
1723
var (
1824
zstdEncoderOnce sync.Once
1925
zstdDecoderOnce sync.Once
2026
zstdEncoder *zstd.Encoder
2127
zstdDecoder *zstd.Decoder
28+
// compressionBufPool holds buffers for serialization + EncodeAll destination.
29+
// Stored as *[]byte so Put is pointer-like and avoids allocs (SA6002).
30+
// Buffers have cap 2*maxPooledCompressionBytes so we use [0:scratchSize] for
31+
// serialization and [scratchSize:scratchSize] for EncodeAll(dst) (nil cap so
32+
// the encoder allocates its own output; sharing the backing array with src
33+
// as append target regressed benchmarks on typical page sizes).
34+
compressionBufPool = sync.Pool{
35+
New: func() interface{} {
36+
b := make([]byte, 0, 2*maxPooledCompressionBytes)
37+
return &b
38+
},
39+
}
40+
// decompressBufPool holds buffers passed to DecodeAll. Stored as *[]byte (SA6002).
41+
decompressBufPool = sync.Pool{
42+
New: func() interface{} {
43+
b := make([]byte, 0, maxPooledCompressionBytes)
44+
return &b
45+
},
46+
}
2247
)
2348

2449
func getZstdEncoder() *zstd.Encoder {
2550
zstdEncoderOnce.Do(func() {
2651
var err error
2752
zstdEncoder, err = zstd.NewWriter(nil,
2853
zstd.WithEncoderLevel(zstd.SpeedDefault),
54+
zstd.WithEncoderCRC(true),
2955
zstd.WithEncoderConcurrency(1),
3056
)
3157
if err != nil {
@@ -60,37 +86,44 @@ func getZstdDecoder() *zstd.Decoder {
6086
// On success, the caller should allocate ceil(len(result)/pageSize) pages
6187
// and copy the result into the allocated page buffer.
6288
func CompressInodes(inodes Inodes, isLeaf bool, pageSize int) []byte {
63-
// First, figure out how large the uncompressed page would be.
64-
uncompressedSize := int(PageHeaderSize)
65-
var elemSize uintptr
89+
var ph Page
6690
if isLeaf {
67-
elemSize = LeafPageElementSize
91+
ph.SetFlags(LeafPageFlag)
6892
} else {
69-
elemSize = BranchPageElementSize
70-
}
71-
for i := 0; i < len(inodes); i++ {
72-
uncompressedSize += int(elemSize) + len(inodes[i].Key()) + len(inodes[i].Value())
93+
ph.SetFlags(BranchPageFlag)
7394
}
95+
ph.SetCount(uint16(len(inodes)))
96+
uncompressedSize := int(UsedSpaceInPage(inodes, &ph))
7497
uncompressedPages := (uncompressedSize + pageSize - 1) / pageSize
7598

7699
// Serialize the inodes into a scratch buffer large enough for the
77100
// uncompressed page(s). We need a real page layout so that
78101
// WriteInodeToPage can use unsafe pointer arithmetic.
79102
scratchSize := uncompressedPages * pageSize
80-
scratch := make([]byte, scratchSize)
103+
var scratch []byte
104+
var encodeDst []byte
105+
if scratchSize <= maxPooledCompressionBytes {
106+
if ptr := compressionBufPool.Get().(*[]byte); cap(*ptr) >= 2*scratchSize {
107+
pooled := *ptr
108+
scratch = pooled[:scratchSize]
109+
encodeDst = pooled[scratchSize:scratchSize]
110+
defer func() { compressionBufPool.Put(ptr) }()
111+
}
112+
}
113+
if scratch == nil {
114+
scratch = make([]byte, scratchSize)
115+
encodeDst = nil
116+
}
81117
p := (*Page)(unsafe.Pointer(&scratch[0]))
82118
if isLeaf {
83119
p.SetFlags(LeafPageFlag)
84120
} else {
85121
p.SetFlags(BranchPageFlag)
86122
}
87123
p.SetCount(uint16(len(inodes)))
88-
WriteInodeToPage(inodes, p)
89-
90-
// Compress the data portion (everything after the page header).
91-
dataSize := scratchSize - int(PageHeaderSize)
92-
data := scratch[PageHeaderSize:]
93-
compressed := getZstdEncoder().EncodeAll(data[:dataSize], nil)
124+
used := int(WriteInodeToPage(inodes, p))
125+
data := scratch[PageHeaderSize:used]
126+
compressed := getZstdEncoder().EncodeAll(data, encodeDst)
94127

95128
// Total compressed page size: header + 4-byte length + compressed data.
96129
compressedTotalSize := int(PageHeaderSize) + compressedDataLenSize + len(compressed)
@@ -139,9 +172,11 @@ func DecompressPage(p *Page, pageSize int) (*Page, []byte, error) {
139172
// Get the compressed data.
140173
compressedData := UnsafeByteSlice(unsafe.Pointer(p), PageHeaderSize+uintptr(compressedDataLenSize), 0, compressedLen)
141174

142-
// Decompress the data.
143-
decompressed, err := getZstdDecoder().DecodeAll(compressedData, nil)
175+
// Decompress the data, reusing a pooled buffer when possible to avoid allocs.
176+
ptr := decompressBufPool.Get().(*[]byte)
177+
decompressed, err := getZstdDecoder().DecodeAll(compressedData, (*ptr)[:0])
144178
if err != nil {
179+
decompressBufPool.Put(ptr)
145180
return nil, nil, fmt.Errorf("zstd DecodeAll on page %d: %w", p.Id(), err)
146181
}
147182

@@ -158,6 +193,7 @@ func DecompressPage(p *Page, pageSize int) (*Page, []byte, error) {
158193

159194
// Copy the decompressed data after the header.
160195
copy(buf[PageHeaderSize:], decompressed)
196+
decompressBufPool.Put(ptr)
161197

162198
// Clear the compressed flag. Overflow is preserved from the original
163199
// page header — it reflects the on-disk allocation, not the

internal/common/compression_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ import (
55
"unsafe"
66
)
77

8+
// makeCompressibleInodes builds n leaf inodes with compressible values (same byte repeated).
9+
func makeCompressibleInodes(n int, valSize int) Inodes {
10+
inodes := make(Inodes, n)
11+
for i := range inodes {
12+
inodes[i].SetKey([]byte{byte(i + 1)})
13+
val := make([]byte, valSize)
14+
for j := range val {
15+
val[j] = byte(i)
16+
}
17+
inodes[i].SetValue(val)
18+
}
19+
return inodes
20+
}
21+
822
func TestCompressDecompressInodes(t *testing.T) {
923
const pageSize = 4096
1024
const numInodes = 30
@@ -219,3 +233,58 @@ func TestFastCheck_CompressedPage(t *testing.T) {
219233
p.SetFlags(BranchPageFlag | CompressedPageFlag)
220234
p.FastCheck(42)
221235
}
236+
237+
// Benchmarks for compression/decompression. Run with:
238+
// go test -bench=BenchmarkCompress -benchmem ./internal/common/
239+
// go test -bench=BenchmarkDecompress -benchmem ./internal/common/
240+
// Compare allocs and ns/op before/after optimization changes.
241+
242+
func BenchmarkCompressInodes_Small(b *testing.B) {
243+
const pageSize = 4096
244+
inodes := makeCompressibleInodes(30, 500)
245+
b.ResetTimer()
246+
b.ReportAllocs()
247+
for i := 0; i < b.N; i++ {
248+
_ = CompressInodes(inodes, true, pageSize)
249+
}
250+
}
251+
252+
func BenchmarkCompressInodes_Large(b *testing.B) {
253+
const pageSize = 4096
254+
// ~50 inodes × (1 + 500) bytes → multi-page, still within pooled 64KB
255+
inodes := makeCompressibleInodes(50, 500)
256+
b.ResetTimer()
257+
b.ReportAllocs()
258+
for i := 0; i < b.N; i++ {
259+
_ = CompressInodes(inodes, true, pageSize)
260+
}
261+
}
262+
263+
func BenchmarkCompressInodes_OverPool(b *testing.B) {
264+
const pageSize = 4096
265+
// Exceeds maxPooledCompressionBytes (64KB) so we use non-pooled path
266+
inodes := makeCompressibleInodes(100, 800)
267+
b.ResetTimer()
268+
b.ReportAllocs()
269+
for i := 0; i < b.N; i++ {
270+
_ = CompressInodes(inodes, true, pageSize)
271+
}
272+
}
273+
274+
func BenchmarkDecompressPage(b *testing.B) {
275+
const pageSize = 4096
276+
inodes := makeCompressibleInodes(30, 500)
277+
compressed := CompressInodes(inodes, true, pageSize)
278+
if compressed == nil {
279+
b.Fatal("compression returned nil")
280+
}
281+
cp := (*Page)(unsafe.Pointer(&compressed[0]))
282+
b.ResetTimer()
283+
b.ReportAllocs()
284+
for i := 0; i < b.N; i++ {
285+
_, _, err := DecompressPage(cp, pageSize)
286+
if err != nil {
287+
b.Fatal(err)
288+
}
289+
}
290+
}

0 commit comments

Comments
 (0)