Skip to content

Commit 2ad1782

Browse files
committed
cralloc: add ScratchBuffer
Add a helper type for reusing a buffer. This simplifies the typical pattern of passing and returning a slice. It is also useful for cases where a function might return either a slice from the scratch buffer or some other aliased slice (for example, `EncodeMVCCValueForExport` in cockroachdb has to return an extra bool so the caller can know if it should update the scratch buffer).
1 parent 0794c59 commit 2ad1782

File tree

2 files changed

+171
-0
lines changed

2 files changed

+171
-0
lines changed

cralloc/scratch_buffer.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
// implied. See the License for the specific language governing
13+
// permissions and limitations under the License.
14+
15+
package cralloc
16+
17+
import "unsafe"
18+
19+
// ScratchBuffer is a helper for the common pattern of reusing a byte buffer to
20+
// reduce slice allocations. To use, replace `make([]byte, n)` with
21+
// `sb.Alloc(n)`.
22+
type ScratchBuffer struct {
23+
p unsafe.Pointer
24+
capacity int
25+
}
26+
27+
// Alloc returns a byte slice of length n and arbitrary capacity which can be
28+
// used until the next call to Alloc.
29+
//
30+
// WARNING: the slice contains arbitrary data.
31+
//
32+
// If the receiver is nil, always allocates a new slice.
33+
func (sb *ScratchBuffer) Alloc(n int) []byte {
34+
if sb == nil {
35+
return make([]byte, n)
36+
}
37+
s := unsafe.Slice((*byte)(sb.p), sb.capacity)
38+
if sb.capacity >= n {
39+
return s[:n]
40+
}
41+
// Adapted from slices.Grow().
42+
s = append(s[:0], make([]byte, n)...)
43+
sb.p = unsafe.Pointer(&s[0])
44+
sb.capacity = cap(s)
45+
return s
46+
}
47+
48+
// AllocZero returns a byte slice of length n and arbitrary capacity which can
49+
// be used until the next call to Alloc. The slice is zeroed out.
50+
//
51+
// WARNING: the slice contains arbitrary data between the length and the
52+
// capacity.
53+
//
54+
// If the receiver is nil, always allocates a new slice.
55+
func (sb *ScratchBuffer) AllocZero(n int) []byte {
56+
if sb == nil {
57+
return make([]byte, n)
58+
}
59+
s := unsafe.Slice((*byte)(sb.p), sb.capacity)
60+
if sb.capacity >= n {
61+
s = s[:n]
62+
clear(s)
63+
return s
64+
}
65+
// Adapted from slices.Grow().
66+
s = append(s[:0], make([]byte, n)...)
67+
sb.p = unsafe.Pointer(&s[0])
68+
sb.capacity = cap(s)
69+
return s
70+
}
71+
72+
// Append is like the built-in append(), but it also updates the scratch buffer
73+
// so that any newly allocated buffer can be reused.
74+
//
75+
// Append can be used with buffers not allocated through the scratch buffer (in
76+
// which case the scratch buffer is not updated).
77+
func (sb *ScratchBuffer) Append(buf []byte, values ...byte) []byte {
78+
res := append(buf, values...)
79+
if sb != nil && unsafe.SliceData(buf) == (*byte)(sb.p) && unsafe.SliceData(res) != (*byte)(sb.p) {
80+
sb.p = unsafe.Pointer(unsafe.SliceData(res))
81+
sb.capacity = cap(res)
82+
}
83+
return res
84+
}
85+
86+
// Capacity returns the current capacity.
87+
func (sb *ScratchBuffer) Capacity() int {
88+
if sb == nil {
89+
return 0
90+
}
91+
return sb.capacity
92+
}
93+
94+
// Reset clears the buffer. This can be useful if we want to avoid retaining a
95+
// very large buffer.
96+
func (sb *ScratchBuffer) Reset() {
97+
if sb != nil {
98+
*sb = ScratchBuffer{}
99+
}
100+
}

cralloc/scratch_buffer_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
// implied. See the License for the specific language governing
13+
// permissions and limitations under the License.
14+
15+
package cralloc
16+
17+
import (
18+
"math/rand/v2"
19+
"testing"
20+
21+
"github.com/cockroachdb/crlib/testutils/require"
22+
)
23+
24+
func TestScratchBuffer(t *testing.T) {
25+
s := (*ScratchBuffer)(nil).Alloc(100)
26+
require.Equal(t, len(s), 100)
27+
var sb ScratchBuffer
28+
s = sb.Alloc(100)
29+
require.Equal(t, len(s), 100)
30+
c := cap(s)
31+
s = sb.Alloc(50)
32+
require.Equal(t, len(s), 50)
33+
require.Equal(t, cap(s), c)
34+
s = sb.Alloc(101)
35+
require.Equal(t, len(s), 101)
36+
require.GT(t, cap(s), 101)
37+
38+
t.Run("AllocZero", func(t *testing.T) {
39+
for range 100 {
40+
var sb ScratchBuffer
41+
maxN := 1 + rand.IntN(1000)
42+
for range 20 {
43+
n := rand.IntN(maxN)
44+
b := sb.AllocZero(n)
45+
for i := range b {
46+
require.Equal(t, b[i], 0)
47+
}
48+
// Trash the entire buffer.
49+
b = b[:cap(b)]
50+
for i := range b {
51+
b[i] = 0xcc
52+
}
53+
}
54+
}
55+
})
56+
57+
t.Run("Append", func(t *testing.T) {
58+
var sb ScratchBuffer
59+
b := sb.Alloc(100)
60+
b = sb.Append(b, make([]byte, 1000)...)
61+
require.Equal(t, len(b), 1100)
62+
// Ensure the capacity has grown.
63+
require.GE(t, sb.Capacity(), 1100)
64+
65+
// Append an unrelated slice.
66+
b = sb.Append(make([]byte, 1100), make([]byte, 10000)...)
67+
require.Equal(t, len(b), 11100)
68+
// Ensure the capacity did not grow.
69+
require.LT(t, sb.Capacity(), 10000)
70+
})
71+
}

0 commit comments

Comments
 (0)