Skip to content

Commit a8a60ac

Browse files
committed
cmd/compile: optimize append(x, make([]T, y)...) slice extension
Changes the compiler to recognize the slice extension pattern append(x, make([]T, y)...) and replace it with growslice and an optional memclr to avoid an allocation for make([]T, y). Memclr is not called in case growslice already allocated a new cleared backing array when T contains pointers. amd64: name old time/op new time/op delta ExtendSlice/IntSlice 103ns ± 4% 57ns ± 4% -44.55% (p=0.000 n=18+18) ExtendSlice/PointerSlice 155ns ± 3% 77ns ± 3% -49.93% (p=0.000 n=20+20) ExtendSlice/NoGrow 50.2ns ± 3% 5.2ns ± 2% -89.67% (p=0.000 n=18+18) name old alloc/op new alloc/op delta ExtendSlice/IntSlice 64.0B ± 0% 32.0B ± 0% -50.00% (p=0.000 n=20+20) ExtendSlice/PointerSlice 64.0B ± 0% 32.0B ± 0% -50.00% (p=0.000 n=20+20) ExtendSlice/NoGrow 32.0B ± 0% 0.0B -100.00% (p=0.000 n=20+20) name old allocs/op new allocs/op delta ExtendSlice/IntSlice 2.00 ± 0% 1.00 ± 0% -50.00% (p=0.000 n=20+20) ExtendSlice/PointerSlice 2.00 ± 0% 1.00 ± 0% -50.00% (p=0.000 n=20+20) ExtendSlice/NoGrow 1.00 ± 0% 0.00 -100.00% (p=0.000 n=20+20) Fixes #21266 Change-Id: Idc3077665f63cbe89762b590c5967a864fd1c07f Reviewed-on: https://go-review.googlesource.com/109517 Run-TryBot: Martin Möhrmann <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Josh Bleecher Snyder <[email protected]>
1 parent 87412a1 commit a8a60ac

File tree

11 files changed

+295
-26
lines changed

11 files changed

+295
-26
lines changed

src/cmd/compile/internal/gc/builtin.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cmd/compile/internal/gc/builtin/runtime.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ func newobject(typ *byte) *any
1818
func panicindex()
1919
func panicslice()
2020
func panicdivide()
21+
func panicmakeslicelen()
2122
func throwinit()
2223
func panicwrap()
2324

src/cmd/compile/internal/gc/order.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1104,7 +1104,14 @@ func (o *Order) expr(n, lhs *Node) *Node {
11041104
}
11051105

11061106
case OAPPEND:
1107-
o.callArgs(&n.List)
1107+
// Check for append(x, make([]T, y)...) .
1108+
if isAppendOfMake(n) {
1109+
n.List.SetFirst(o.expr(n.List.First(), nil)) // order x
1110+
n.List.Second().Left = o.expr(n.List.Second().Left, nil) // order y
1111+
} else {
1112+
o.callArgs(&n.List)
1113+
}
1114+
11081115
if lhs == nil || lhs.Op != ONAME && !samesafeexpr(lhs, n.List.First()) {
11091116
n = o.copyExpr(n, n.Type, false)
11101117
}

src/cmd/compile/internal/gc/ssa.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,7 @@ func (s *state) stmt(n *Node) {
703703
s.call(n, callNormal)
704704
if n.Op == OCALLFUNC && n.Left.Op == ONAME && n.Left.Class() == PFUNC {
705705
if fn := n.Left.Sym.Name; compiling_runtime && fn == "throw" ||
706-
n.Left.Sym.Pkg == Runtimepkg && (fn == "throwinit" || fn == "gopanic" || fn == "panicwrap" || fn == "block") {
706+
n.Left.Sym.Pkg == Runtimepkg && (fn == "throwinit" || fn == "gopanic" || fn == "panicwrap" || fn == "block" || fn == "panicmakeslicelen" || fn == "panicmakeslicecap") {
707707
m := s.mem()
708708
b := s.endBlock()
709709
b.Kind = ssa.BlockExit

src/cmd/compile/internal/gc/walk.go

Lines changed: 187 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -728,9 +728,13 @@ opswitch:
728728
if r.Type.Elem().NotInHeap() {
729729
yyerror("%v is go:notinheap; heap allocation disallowed", r.Type.Elem())
730730
}
731-
if r.Isddd() {
731+
switch {
732+
case isAppendOfMake(r):
733+
// x = append(y, make([]T, y)...)
734+
r = extendslice(r, init)
735+
case r.Isddd():
732736
r = appendslice(r, init) // also works for append(slice, string).
733-
} else {
737+
default:
734738
r = walkappend(r, init, n)
735739
}
736740
n.Right = r
@@ -2910,6 +2914,18 @@ func addstr(n *Node, init *Nodes) *Node {
29102914
return r
29112915
}
29122916

2917+
func walkAppendArgs(n *Node, init *Nodes) {
2918+
walkexprlistsafe(n.List.Slice(), init)
2919+
2920+
// walkexprlistsafe will leave OINDEX (s[n]) alone if both s
2921+
// and n are name or literal, but those may index the slice we're
2922+
// modifying here. Fix explicitly.
2923+
ls := n.List.Slice()
2924+
for i1, n1 := range ls {
2925+
ls[i1] = cheapexpr(n1, init)
2926+
}
2927+
}
2928+
29132929
// expand append(l1, l2...) to
29142930
// init {
29152931
// s := l1
@@ -2925,15 +2941,7 @@ func addstr(n *Node, init *Nodes) *Node {
29252941
//
29262942
// l2 is allowed to be a string.
29272943
func appendslice(n *Node, init *Nodes) *Node {
2928-
walkexprlistsafe(n.List.Slice(), init)
2929-
2930-
// walkexprlistsafe will leave OINDEX (s[n]) alone if both s
2931-
// and n are name or literal, but those may index the slice we're
2932-
// modifying here. Fix explicitly.
2933-
ls := n.List.Slice()
2934-
for i1, n1 := range ls {
2935-
ls[i1] = cheapexpr(n1, init)
2936-
}
2944+
walkAppendArgs(n, init)
29372945

29382946
l1 := n.List.First()
29392947
l2 := n.List.Second()
@@ -3027,6 +3035,174 @@ func appendslice(n *Node, init *Nodes) *Node {
30273035
return s
30283036
}
30293037

3038+
// isAppendOfMake reports whether n is of the form append(x , make([]T, y)...).
3039+
// isAppendOfMake assumes n has already been typechecked.
3040+
func isAppendOfMake(n *Node) bool {
3041+
if Debug['N'] != 0 || instrumenting {
3042+
return false
3043+
}
3044+
3045+
if n.Typecheck() == 0 {
3046+
Fatalf("missing typecheck: %+v", n)
3047+
}
3048+
3049+
if n.Op != OAPPEND || !n.Isddd() || n.List.Len() != 2 {
3050+
return false
3051+
}
3052+
3053+
second := n.List.Second()
3054+
if second.Op != OMAKESLICE {
3055+
return false
3056+
}
3057+
3058+
if n.List.Second().Right != nil {
3059+
return false
3060+
}
3061+
3062+
// y must be either an integer constant or a variable of type int.
3063+
// typecheck checks that constant arguments to make are not negative and
3064+
// fit into an int.
3065+
// runtime.growslice uses int as type for the newcap argument.
3066+
// Constraining variables to be type int avoids the need for runtime checks
3067+
// that e.g. check if an int64 value fits into an int.
3068+
// TODO(moehrmann): support other integer types that always fit in an int
3069+
y := second.Left
3070+
if !Isconst(y, CTINT) && y.Type.Etype != TINT {
3071+
return false
3072+
}
3073+
3074+
return true
3075+
}
3076+
3077+
// extendslice rewrites append(l1, make([]T, l2)...) to
3078+
// init {
3079+
// if l2 < 0 {
3080+
// panicmakeslicelen()
3081+
// }
3082+
// s := l1
3083+
// n := len(s) + l2
3084+
// // Compare n and s as uint so growslice can panic on overflow of len(s) + l2.
3085+
// // cap is a positive int and n can become negative when len(s) + l2
3086+
// // overflows int. Interpreting n when negative as uint makes it larger
3087+
// // than cap(s). growslice will check the int n arg and panic if n is
3088+
// // negative. This prevents the overflow from being undetected.
3089+
// if uint(n) > uint(cap(s)) {
3090+
// s = growslice(T, s, n)
3091+
// }
3092+
// s = s[:n]
3093+
// lptr := &l1[0]
3094+
// sptr := &s[0]
3095+
// if lptr == sptr || !hasPointers(T) {
3096+
// // growslice did not clear the whole underlying array (or did not get called)
3097+
// hp := &s[len(l1)]
3098+
// hn := l2 * sizeof(T)
3099+
// memclr(hp, hn)
3100+
// }
3101+
// }
3102+
// s
3103+
func extendslice(n *Node, init *Nodes) *Node {
3104+
// isAppendOfMake made sure l2 fits in an int.
3105+
l2 := conv(n.List.Second().Left, types.Types[TINT])
3106+
l2 = typecheck(l2, Erv)
3107+
n.List.SetSecond(l2) // walkAppendArgs expects l2 in n.List.Second().
3108+
3109+
walkAppendArgs(n, init)
3110+
3111+
l1 := n.List.First()
3112+
l2 = n.List.Second() // re-read l2, as it may have been updated by walkAppendArgs
3113+
3114+
var nodes []*Node
3115+
3116+
// if l2 < 0
3117+
nifneg := nod(OIF, nod(OLT, l2, nodintconst(0)), nil)
3118+
nifneg.SetLikely(false)
3119+
3120+
// panicmakeslicelen()
3121+
nifneg.Nbody.Set1(mkcall("panicmakeslicelen", nil, init))
3122+
nodes = append(nodes, nifneg)
3123+
3124+
// s := l1
3125+
s := temp(l1.Type)
3126+
nodes = append(nodes, nod(OAS, s, l1))
3127+
3128+
elemtype := s.Type.Elem()
3129+
3130+
// n := len(s) + l2
3131+
nn := temp(types.Types[TINT])
3132+
nodes = append(nodes, nod(OAS, nn, nod(OADD, nod(OLEN, s, nil), l2)))
3133+
3134+
// if uint(n) > uint(cap(s))
3135+
nuint := nod(OCONV, nn, nil)
3136+
nuint.Type = types.Types[TUINT]
3137+
capuint := nod(OCONV, nod(OCAP, s, nil), nil)
3138+
capuint.Type = types.Types[TUINT]
3139+
nif := nod(OIF, nod(OGT, nuint, capuint), nil)
3140+
3141+
// instantiate growslice(typ *type, old []any, newcap int) []any
3142+
fn := syslook("growslice")
3143+
fn = substArgTypes(fn, elemtype, elemtype)
3144+
3145+
// s = growslice(T, s, n)
3146+
nif.Nbody.Set1(nod(OAS, s, mkcall1(fn, s.Type, &nif.Ninit, typename(elemtype), s, nn)))
3147+
nodes = append(nodes, nif)
3148+
3149+
// s = s[:n]
3150+
nt := nod(OSLICE, s, nil)
3151+
nt.SetSliceBounds(nil, nn, nil)
3152+
nodes = append(nodes, nod(OAS, s, nt))
3153+
3154+
// lptr := &l1[0]
3155+
l1ptr := temp(l1.Type.Elem().PtrTo())
3156+
tmp := nod(OSPTR, l1, nil)
3157+
nodes = append(nodes, nod(OAS, l1ptr, tmp))
3158+
3159+
// sptr := &s[0]
3160+
sptr := temp(elemtype.PtrTo())
3161+
tmp = nod(OSPTR, s, nil)
3162+
nodes = append(nodes, nod(OAS, sptr, tmp))
3163+
3164+
var clr []*Node
3165+
3166+
// hp := &s[len(l1)]
3167+
hp := temp(types.Types[TUNSAFEPTR])
3168+
3169+
tmp = nod(OINDEX, s, nod(OLEN, l1, nil))
3170+
tmp.SetBounded(true)
3171+
tmp = nod(OADDR, tmp, nil)
3172+
tmp = nod(OCONVNOP, tmp, nil)
3173+
tmp.Type = types.Types[TUNSAFEPTR]
3174+
clr = append(clr, nod(OAS, hp, tmp))
3175+
3176+
// hn := l2 * sizeof(elem(s))
3177+
hn := temp(types.Types[TUINTPTR])
3178+
3179+
tmp = nod(OMUL, l2, nodintconst(elemtype.Width))
3180+
tmp = conv(tmp, types.Types[TUINTPTR])
3181+
clr = append(clr, nod(OAS, hn, tmp))
3182+
3183+
clrname := "memclrNoHeapPointers"
3184+
hasPointers := types.Haspointers(elemtype)
3185+
if hasPointers {
3186+
clrname = "memclrHasPointers"
3187+
}
3188+
clrfn := mkcall(clrname, nil, init, hp, hn)
3189+
clr = append(clr, clrfn)
3190+
3191+
if hasPointers {
3192+
// if l1ptr == sptr
3193+
nifclr := nod(OIF, nod(OEQ, l1ptr, sptr), nil)
3194+
nifclr.Nbody.Set(clr)
3195+
nodes = append(nodes, nifclr)
3196+
} else {
3197+
nodes = append(nodes, clr...)
3198+
}
3199+
3200+
typecheckslice(nodes, Etop)
3201+
walkstmtlist(nodes)
3202+
init.Append(nodes...)
3203+
return s
3204+
}
3205+
30303206
// Rewrite append(src, x, y, z) so that any side effects in
30313207
// x, y, z (including runtime panics) are evaluated in
30323208
// initialization statements before the append.

src/runtime/slice.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ func maxSliceCap(elemsize uintptr) uintptr {
4444
return maxAlloc / elemsize
4545
}
4646

47+
func panicmakeslicelen() {
48+
panic(errorString("makeslice: len out of range"))
49+
}
50+
51+
func panicmakeslicecap() {
52+
panic(errorString("makeslice: cap out of range"))
53+
}
54+
4755
func makeslice(et *_type, len, cap int) slice {
4856
// NOTE: The len > maxElements check here is not strictly necessary,
4957
// but it produces a 'len out of range' error instead of a 'cap out of range' error
@@ -52,11 +60,11 @@ func makeslice(et *_type, len, cap int) slice {
5260
// See issue 4085.
5361
maxElements := maxSliceCap(et.size)
5462
if len < 0 || uintptr(len) > maxElements {
55-
panic(errorString("makeslice: len out of range"))
63+
panicmakeslicelen()
5664
}
5765

5866
if cap < len || uintptr(cap) > maxElements {
59-
panic(errorString("makeslice: cap out of range"))
67+
panicmakeslicecap()
6068
}
6169

6270
p := mallocgc(et.size*uintptr(cap), et, true)
@@ -66,12 +74,12 @@ func makeslice(et *_type, len, cap int) slice {
6674
func makeslice64(et *_type, len64, cap64 int64) slice {
6775
len := int(len64)
6876
if int64(len) != len64 {
69-
panic(errorString("makeslice: len out of range"))
77+
panicmakeslicelen()
7078
}
7179

7280
cap := int(cap64)
7381
if int64(cap) != cap64 {
74-
panic(errorString("makeslice: cap out of range"))
82+
panicmakeslicecap()
7583
}
7684

7785
return makeslice(et, len, cap)

src/runtime/slice_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,36 @@ func BenchmarkGrowSlice(b *testing.B) {
7272
})
7373
}
7474

75+
var (
76+
SinkIntSlice []int
77+
SinkIntPointerSlice []*int
78+
)
79+
80+
func BenchmarkExtendSlice(b *testing.B) {
81+
var length = 4 // Use a variable to prevent stack allocation of slices.
82+
b.Run("IntSlice", func(b *testing.B) {
83+
s := make([]int, 0, length)
84+
for i := 0; i < b.N; i++ {
85+
s = append(s[:0:length/2], make([]int, length)...)
86+
}
87+
SinkIntSlice = s
88+
})
89+
b.Run("PointerSlice", func(b *testing.B) {
90+
s := make([]*int, 0, length)
91+
for i := 0; i < b.N; i++ {
92+
s = append(s[:0:length/2], make([]*int, length)...)
93+
}
94+
SinkIntPointerSlice = s
95+
})
96+
b.Run("NoGrow", func(b *testing.B) {
97+
s := make([]int, 0, length)
98+
for i := 0; i < b.N; i++ {
99+
s = append(s[:0:length], make([]int, length)...)
100+
}
101+
SinkIntSlice = s
102+
})
103+
}
104+
75105
func BenchmarkAppend(b *testing.B) {
76106
b.StopTimer()
77107
x := make([]int, 0, N)

0 commit comments

Comments
 (0)