Skip to content

Commit 30c614f

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 30c614f

File tree

2 files changed

+181
-0
lines changed

2 files changed

+181
-0
lines changed

cralloc/scratch_buffer.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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+
// AllocUnsafe returns a byte slice of length n and arbitrary capacity which can
28+
// be used until the next call to AllocUnsafe/AllocZero/Append.
29+
//
30+
// WARNING: the slice contains arbitrary data.
31+
//
32+
// This method is marked Unsafe because the allowed lifetime of the returned
33+
// slice is limited.
34+
//
35+
// If the receiver is nil, always allocates a new slice.
36+
func (sb *ScratchBuffer) AllocUnsafe(n int) []byte {
37+
if sb == nil {
38+
return make([]byte, n)
39+
}
40+
s := unsafe.Slice((*byte)(sb.p), sb.capacity)
41+
if sb.capacity >= n {
42+
return s[:n]
43+
}
44+
// Adapted from slices.Grow().
45+
s = append(s[:0], make([]byte, n)...)
46+
sb.p = unsafe.Pointer(&s[0])
47+
sb.capacity = cap(s)
48+
return s
49+
}
50+
51+
// AllocZeroUnsafe returns a byte slice of length n and arbitrary capacity which
52+
// can be used until the next call to AllocUnsafe/AllocZero/Append. The slice is
53+
// zeroed out.
54+
//
55+
// WARNING: the slice contains arbitrary data between the length and the
56+
// capacity.
57+
//
58+
// This method is marked Unsafe because the allowed lifetime of the returned
59+
// slice is limited.
60+
//
61+
// If the receiver is nil, always allocates a new slice.
62+
func (sb *ScratchBuffer) AllocZeroUnsafe(n int) []byte {
63+
if sb == nil {
64+
return make([]byte, n)
65+
}
66+
s := unsafe.Slice((*byte)(sb.p), sb.capacity)
67+
if sb.capacity >= n {
68+
s = s[:n]
69+
clear(s)
70+
return s
71+
}
72+
// Adapted from slices.Grow(). We do not want to simply use make([]byte, n)
73+
// because we want the scratch buffer to grow according to the append()
74+
// heuristics. Otherwise, an allocation pattern of slowly increasing sizes
75+
// would cause an allocation each time.
76+
s = append(s[:0], make([]byte, n)...)
77+
sb.p = unsafe.Pointer(&s[0])
78+
sb.capacity = cap(s)
79+
return s
80+
}
81+
82+
// Append is like the built-in append(), but it also updates the scratch buffer
83+
// so that any newly allocated buffer can be reused.
84+
//
85+
// Append can be used with buffers not allocated through the scratch buffer (in
86+
// which case the scratch buffer is not updated).
87+
func (sb *ScratchBuffer) Append(buf []byte, values ...byte) []byte {
88+
res := append(buf, values...)
89+
if sb != nil && unsafe.SliceData(buf) == (*byte)(sb.p) && unsafe.SliceData(res) != (*byte)(sb.p) {
90+
sb.p = unsafe.Pointer(unsafe.SliceData(res))
91+
sb.capacity = cap(res)
92+
}
93+
return res
94+
}
95+
96+
// Capacity returns the current capacity.
97+
func (sb *ScratchBuffer) Capacity() int {
98+
if sb == nil {
99+
return 0
100+
}
101+
return sb.capacity
102+
}
103+
104+
// Reset clears the buffer. This can be useful if we want to avoid retaining a
105+
// very large buffer.
106+
func (sb *ScratchBuffer) Reset() {
107+
if sb != nil {
108+
*sb = ScratchBuffer{}
109+
}
110+
}

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).AllocUnsafe(100)
26+
require.Equal(t, len(s), 100)
27+
var sb ScratchBuffer
28+
s = sb.AllocUnsafe(100)
29+
require.Equal(t, len(s), 100)
30+
c := cap(s)
31+
s = sb.AllocUnsafe(50)
32+
require.Equal(t, len(s), 50)
33+
require.Equal(t, cap(s), c)
34+
s = sb.AllocUnsafe(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.AllocZeroUnsafe(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.AllocUnsafe(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)