Skip to content

Commit ec37a27

Browse files
committed
runtime: Mildly over-allocate when growing slices
This refactors runtime.sliceAppend() and runtime.sliceGrow() so, not only sliceAppend() uses sliceGrow() if needed, sliceGrow() only mildly over-allocates a new buffer using a sequence similar to what CPython uses for list objects. Signed-off-by: L. Pereira <[email protected]>
1 parent 880e940 commit ec37a27

File tree

2 files changed

+22
-41
lines changed

2 files changed

+22
-41
lines changed

src/runtime/slice.go

Lines changed: 20 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,19 @@ import (
88

99
// Builtin append(src, elements...) function: append elements to src and return
1010
// the modified (possibly expanded) slice.
11-
func sliceAppend(srcBuf, elemsBuf unsafe.Pointer, srcLen, srcCap, elemsLen uintptr, elemSize uintptr) (unsafe.Pointer, uintptr, uintptr) {
12-
if elemsLen == 0 {
13-
// Nothing to append, return the input slice.
14-
return srcBuf, srcLen, srcCap
15-
}
11+
func sliceAppend(srcBuf, elemsBuf unsafe.Pointer, srcLen, srcCap, elemsLen, elemSize uintptr) (unsafe.Pointer, uintptr, uintptr) {
12+
newLen := srcLen + elemsLen
1613

17-
if srcLen+elemsLen > srcCap {
18-
// Slice does not fit, allocate a new buffer that's large enough.
19-
srcCap = srcCap * 2
20-
if srcCap == 0 { // e.g. zero slice
21-
srcCap = 1
22-
}
23-
for srcLen+elemsLen > srcCap {
24-
// This algorithm may be made more memory-efficient: don't multiply
25-
// by two but by 1.5 or something. As far as I can see, that's
26-
// allowed by the Go language specification (but may be observed by
27-
// programs).
28-
srcCap *= 2
29-
}
30-
buf := alloc(srcCap*elemSize, nil)
14+
if elemsLen != 0 {
15+
// Allocate a new slice with capacity for elemsLen more elements, if necessary;
16+
// otherwise, reuse the passed slice.
17+
srcBuf, srcLen, srcCap = sliceGrow(srcBuf, srcLen, srcCap, newLen, elemSize)
3118

32-
// Copy the old slice to the new slice.
33-
if srcLen != 0 {
34-
memmove(buf, srcBuf, srcLen*elemSize)
35-
}
36-
srcBuf = buf
19+
// Append the new elements in-place.
20+
memmove(unsafe.Add(srcBuf, srcLen*elemSize), elemsBuf, elemsLen*elemSize)
3721
}
3822

39-
// The slice fits (after possibly allocating a new one), append it in-place.
40-
memmove(unsafe.Add(srcBuf, srcLen*elemSize), elemsBuf, elemsLen*elemSize)
41-
return srcBuf, srcLen + elemsLen, srcCap
23+
return srcBuf, newLen, srcCap
4224
}
4325

4426
// Builtin copy(dst, src) function: copy bytes from dst to src.
@@ -54,29 +36,28 @@ func sliceCopy(dst, src unsafe.Pointer, dstLen, srcLen uintptr, elemSize uintptr
5436

5537
// sliceGrow returns a new slice with space for at least newCap elements
5638
func sliceGrow(oldBuf unsafe.Pointer, oldLen, oldCap, newCap, elemSize uintptr) (unsafe.Pointer, uintptr, uintptr) {
57-
58-
// TODO(dgryski): sliceGrow() and sliceAppend() should be refactored to share the base growth code.
59-
6039
if oldCap >= newCap {
6140
// No need to grow, return the input slice.
6241
return oldBuf, oldLen, oldCap
6342
}
6443

65-
// allow nil slice
66-
if oldCap == 0 {
67-
oldCap++
68-
}
44+
// Mildly over-allocate using a similar integer sequence to what CPython uses when
45+
// allocating lists. This still helps amortizing repeated appends to this slice,
46+
// but is much more conservative than doubling the capacity every time a reallocation
47+
// is necessary.
48+
// Sequence goes as follows: 0, 8, 16, 24, 32, 40, 48, 56, 64, 74, 80, ...
49+
newCap = (newCap + (newCap >> 3) + 6) & ^uintptr(7)
6950

70-
// grow capacity
71-
for oldCap < newCap {
72-
oldCap *= 2
51+
// Account for nil slices.
52+
if newCap == 0 {
53+
newCap++
7354
}
7455

75-
buf := alloc(oldCap*elemSize, nil)
56+
buf := alloc(newCap*elemSize, nil)
7657
if oldLen > 0 {
7758
// copy any data to new slice
7859
memmove(buf, oldBuf, oldLen*elemSize)
7960
}
8061

81-
return buf, oldLen, oldCap
62+
return buf, oldLen, newCap
8263
}

testdata/slice.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ bar: len=3 cap=5 data: 1 2 4
88
slice is nil? true true
99
grow: len=0 cap=0 data:
1010
grow: len=1 cap=1 data: 42
11-
grow: len=3 cap=4 data: 42 -1 -2
11+
grow: len=3 cap=8 data: 42 -1 -2
1212
grow: len=7 cap=8 data: 42 -1 -2 1 2 4 5
1313
grow: len=7 cap=8 data: 42 -1 -2 1 2 4 5
1414
grow: len=14 cap=16 data: 42 -1 -2 1 2 4 5 42 -1 -2 1 2 4 5
15-
bytes: len=6 cap=6 data: 1 2 3 102 111 111
15+
bytes: len=6 cap=8 data: 1 2 3 102 111 111
1616
slice to array pointer: 1 -2 20 4
1717
unsafe.Add array: 1 5 8 4
1818
unsafe.Slice array: 3 3 9 15 4

0 commit comments

Comments
 (0)