Skip to content

Commit 925d2fb

Browse files
mdempskygopherbot
authored andcommitted
cmd/compile: restore zero-copy string->[]byte optimization
This CL implements the remainder of the zero-copy string->[]byte conversion optimization initially attempted in go.dev/cl/520395, but fixes the tracking of mutations due to ODEREF/ODOTPTR assignments, and adds more comprehensive tests that I should have included originally. However, this CL also keeps it behind the -d=zerocopy flag. The next CL will enable it by default (for easier rollback). Updates #2205. Change-Id: Ic330260099ead27fc00e2680a59c6ff23cb63c2b Reviewed-on: https://go-review.googlesource.com/c/go/+/520599 Auto-Submit: Matthew Dempsky <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Than McIntosh <[email protected]> Run-TryBot: Matthew Dempsky <[email protected]> Reviewed-by: Cuong Manh Le <[email protected]>
1 parent 243c8c0 commit 925d2fb

File tree

8 files changed

+145
-41
lines changed

8 files changed

+145
-41
lines changed

src/cmd/compile/internal/base/debug.go

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type DebugFlags struct {
2424
DumpInlFuncProps string `help:"dump function properties from inl heuristics to specified file"`
2525
DumpPtrs int `help:"show Node pointers values in dump output"`
2626
DwarfInl int `help:"print information about DWARF inlined function creation"`
27+
EscapeMutationsCalls int `help:"print extra escape analysis diagnostics about mutations and calls" concurrent:"ok"`
2728
Export int `help:"print export data"`
2829
Fmahash string `help:"hash value for use in debugging platform-dependent multiply-add use" concurrent:"ok"`
2930
GCAdjust int `help:"log adjustments to GOGC" concurrent:"ok"`
@@ -58,6 +59,7 @@ type DebugFlags struct {
5859
PGODevirtualize int `help:"enable profile-guided devirtualization" concurrent:"ok"`
5960
WrapGlobalMapDbg int `help:"debug trace output for global map init wrapping"`
6061
WrapGlobalMapCtl int `help:"global map init wrap control (0 => default, 1 => off, 2 => stress mode, no size cutoff)"`
62+
ZeroCopy int `help:"enable zero-copy string->[]byte conversions" concurrent:"ok"`
6163

6264
ConcurrentOk bool // true if only concurrentOk flags seen
6365
}

src/cmd/compile/internal/escape/assign.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,12 @@ func (e *escape) addr(n ir.Node) hole {
4141
} else {
4242
e.mutate(n.X)
4343
}
44-
case ir.ODEREF, ir.ODOTPTR:
45-
e.mutate(n)
44+
case ir.ODEREF:
45+
n := n.(*ir.StarExpr)
46+
e.mutate(n.X)
47+
case ir.ODOTPTR:
48+
n := n.(*ir.SelectorExpr)
49+
e.mutate(n.X)
4650
case ir.OINDEXMAP:
4751
n := n.(*ir.IndexExpr)
4852
e.discard(n.X)

src/cmd/compile/internal/escape/escape.go

+40-34
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"cmd/compile/internal/logopt"
1313
"cmd/compile/internal/typecheck"
1414
"cmd/compile/internal/types"
15+
"cmd/internal/src"
1516
)
1617

1718
// Escape analysis.
@@ -345,10 +346,7 @@ func (b *batch) finish(fns []*ir.Func) {
345346

346347
// If the result of a string->[]byte conversion is never mutated,
347348
// then it can simply reuse the string's memory directly.
348-
//
349-
// TODO(mdempsky): Enable in a subsequent CL. We need to ensure
350-
// []byte("") evaluates to []byte{}, not []byte(nil).
351-
if false {
349+
if base.Debug.ZeroCopy != 0 {
352350
if n, ok := n.(*ir.ConvExpr); ok && n.Op() == ir.OSTR2BYTES && !loc.hasAttr(attrMutates) {
353351
if base.Flag.LowerM >= 1 {
354352
base.WarnfAt(n.Pos(), "zero-copy string->[]byte conversion")
@@ -474,40 +472,48 @@ func (b *batch) paramTag(fn *ir.Func, narg int, f *types.Field) string {
474472
esc.Optimize()
475473

476474
if diagnose && !loc.hasAttr(attrEscapes) {
477-
anyLeaks := false
478-
if x := esc.Heap(); x >= 0 {
479-
if x == 0 {
480-
base.WarnfAt(f.Pos, "leaking param: %v", name())
481-
} else {
482-
// TODO(mdempsky): Mention level=x like below?
483-
base.WarnfAt(f.Pos, "leaking param content: %v", name())
484-
}
485-
anyLeaks = true
486-
}
487-
for i := 0; i < numEscResults; i++ {
488-
if x := esc.Result(i); x >= 0 {
489-
res := fn.Type().Results().Field(i).Sym
490-
base.WarnfAt(f.Pos, "leaking param: %v to result %v level=%d", name(), res, x)
491-
anyLeaks = true
492-
}
475+
b.reportLeaks(f.Pos, name(), esc, fn.Type())
476+
}
477+
478+
return esc.Encode()
479+
}
480+
481+
func (b *batch) reportLeaks(pos src.XPos, name string, esc leaks, sig *types.Type) {
482+
warned := false
483+
if x := esc.Heap(); x >= 0 {
484+
if x == 0 {
485+
base.WarnfAt(pos, "leaking param: %v", name)
486+
} else {
487+
// TODO(mdempsky): Mention level=x like below?
488+
base.WarnfAt(pos, "leaking param content: %v", name)
493489
}
494-
if !anyLeaks {
495-
base.WarnfAt(f.Pos, "%v does not escape", name())
490+
warned = true
491+
}
492+
for i := 0; i < numEscResults; i++ {
493+
if x := esc.Result(i); x >= 0 {
494+
res := sig.Results().Field(i).Sym
495+
base.WarnfAt(pos, "leaking param: %v to result %v level=%d", name, res, x)
496+
warned = true
496497
}
498+
}
497499

498-
if base.Flag.LowerM >= 2 {
499-
if x := esc.Mutator(); x >= 0 {
500-
base.WarnfAt(f.Pos, "mutates param: %v derefs=%v", name(), x)
501-
} else {
502-
base.WarnfAt(f.Pos, "does not mutate param: %v", name())
503-
}
504-
if x := esc.Callee(); x >= 0 {
505-
base.WarnfAt(f.Pos, "calls param: %v derefs=%v", name(), x)
506-
} else {
507-
base.WarnfAt(f.Pos, "does not call param: %v", name())
508-
}
500+
if base.Debug.EscapeMutationsCalls <= 0 {
501+
if !warned {
502+
base.WarnfAt(pos, "%v does not escape", name)
509503
}
504+
return
510505
}
511506

512-
return esc.Encode()
507+
if x := esc.Mutator(); x >= 0 {
508+
base.WarnfAt(pos, "mutates param: %v derefs=%v", name, x)
509+
warned = true
510+
}
511+
if x := esc.Callee(); x >= 0 {
512+
base.WarnfAt(pos, "calls param: %v derefs=%v", name, x)
513+
warned = true
514+
}
515+
516+
if !warned {
517+
base.WarnfAt(pos, "%v does not escape, mutate, or call", name)
518+
}
513519
}

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

+8
Original file line numberDiff line numberDiff line change
@@ -2659,6 +2659,14 @@ func (s *state) exprCheckPtr(n ir.Node, checkPtrOK bool) *ssa.Value {
26592659
n := n.(*ir.ConvExpr)
26602660
str := s.expr(n.X)
26612661
ptr := s.newValue1(ssa.OpStringPtr, s.f.Config.Types.BytePtr, str)
2662+
if !n.NonNil() {
2663+
// We need to ensure []byte("") evaluates to []byte{}, and not []byte(nil).
2664+
//
2665+
// TODO(mdempsky): Investigate using "len != 0" instead of "ptr != nil".
2666+
cond := s.newValue2(ssa.OpNeqPtr, types.Types[types.TBOOL], ptr, s.constNil(ptr.Type))
2667+
zerobase := s.newValue1A(ssa.OpAddr, ptr.Type, ir.Syms.Zerobase, s.sb)
2668+
ptr = s.ternary(cond, ptr, zerobase)
2669+
}
26622670
len := s.newValue1(ssa.OpStringLen, types.Types[types.TINT], str)
26632671
return s.newValue3(ssa.OpSliceMake, n.Type(), ptr, len, len)
26642672
case ir.OCFUNC:

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

+8-2
Original file line numberDiff line numberDiff line change
@@ -815,8 +815,14 @@ func (o *orderState) stmt(n ir.Node) {
815815
// Mark []byte(str) range expression to reuse string backing storage.
816816
// It is safe because the storage cannot be mutated.
817817
n := n.(*ir.RangeStmt)
818-
if n.X.Op() == ir.OSTR2BYTES {
819-
n.X.(*ir.ConvExpr).SetOp(ir.OSTR2BYTESTMP)
818+
if x, ok := n.X.(*ir.ConvExpr); ok {
819+
switch x.Op() {
820+
case ir.OSTR2BYTES:
821+
x.SetOp(ir.OSTR2BYTESTMP)
822+
fallthrough
823+
case ir.OSTR2BYTESTMP:
824+
x.MarkNonNil() // "range []byte(nil)" is fine
825+
}
820826
}
821827

822828
t := o.markTemp()

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

+1
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,7 @@ func stringSearch(expr ir.Node, cc []exprClause, out *ir.Nodes) {
736736
// Convert expr to a []int8
737737
slice := ir.NewConvExpr(base.Pos, ir.OSTR2BYTESTMP, types.NewSlice(types.Types[types.TINT8]), expr)
738738
slice.SetTypecheck(1) // legacy typechecker doesn't handle this op
739+
slice.MarkNonNil()
739740
// Load the byte we're splitting on.
740741
load := ir.NewIndexExpr(base.Pos, slice, ir.NewInt(base.Pos, int64(bestIdx)))
741742
// Compare with the value we're splitting on.

test/escape_mutations.go

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// errorcheck -0 -m -d=escapemutationscalls,zerocopy -l
2+
3+
// Copyright 2023 The Go Authors. All rights reserved.
4+
// Use of this source code is governed by a BSD-style
5+
// license that can be found in the LICENSE file.
6+
7+
package p
8+
9+
import "fmt"
10+
11+
type B struct {
12+
x int
13+
px *int
14+
pb *B
15+
}
16+
17+
func F1(b *B) { // ERROR "mutates param: b derefs=0"
18+
b.x = 1
19+
}
20+
21+
func F2(b *B) { // ERROR "mutates param: b derefs=1"
22+
*b.px = 1
23+
}
24+
25+
func F2a(b *B) { // ERROR "mutates param: b derefs=0"
26+
b.px = nil
27+
}
28+
29+
func F3(b *B) { // ERROR "leaking param: b"
30+
fmt.Println(b) // ERROR "\.\.\. argument does not escape"
31+
}
32+
33+
func F4(b *B) { // ERROR "leaking param content: b"
34+
fmt.Println(*b) // ERROR "\.\.\. argument does not escape" "\*b escapes to heap"
35+
}
36+
37+
func F4a(b *B) { // ERROR "leaking param content: b" "mutates param: b derefs=0"
38+
b.x = 2
39+
fmt.Println(*b) // ERROR "\.\.\. argument does not escape" "\*b escapes to heap"
40+
}
41+
42+
func F5(b *B) { // ERROR "leaking param: b"
43+
sink = b
44+
}
45+
46+
func F6(b *B) int { // ERROR "b does not escape, mutate, or call"
47+
return b.x
48+
}
49+
50+
var sink any
51+
52+
func M() {
53+
var b B // ERROR "moved to heap: b"
54+
F1(&b)
55+
F2(&b)
56+
F2a(&b)
57+
F3(&b)
58+
F4(&b)
59+
}
60+
61+
func g(s string) { // ERROR "s does not escape, mutate, or call"
62+
sink = &([]byte(s))[10] // ERROR "\(\[\]byte\)\(s\) escapes to heap"
63+
}
64+
65+
func h(out []byte, s string) { // ERROR "mutates param: out derefs=0" "s does not escape, mutate, or call"
66+
copy(out, []byte(s)) // ERROR "zero-copy string->\[\]byte conversion" "\(\[\]byte\)\(s\) does not escape"
67+
}
68+
69+
func i(s string) byte { // ERROR "s does not escape, mutate, or call"
70+
p := []byte(s) // ERROR "zero-copy string->\[\]byte conversion" "\(\[\]byte\)\(s\) does not escape"
71+
return p[20]
72+
}
73+
74+
func j(s string, x byte) { // ERROR "s does not escape, mutate, or call"
75+
p := []byte(s) // ERROR "\(\[\]byte\)\(s\) does not escape"
76+
p[20] = x
77+
}

test/inline_big.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@
99

1010
package foo
1111

12-
func small(a []int) int { // ERROR "can inline small with cost .* as:.*" "a does not escape" "does not mutate param: a" "does not call param: a"
12+
func small(a []int) int { // ERROR "can inline small with cost .* as:.*" "a does not escape"
1313
// Cost 16 body (need cost < 20).
1414
// See cmd/compile/internal/gc/inl.go:inlineBigFunction*
1515
return a[0] + a[1] + a[2] + a[3]
1616
}
17-
func medium(a []int) int { // ERROR "can inline medium with cost .* as:.*" "a does not escape" "does not mutate param: a" "does not call param: a"
17+
func medium(a []int) int { // ERROR "can inline medium with cost .* as:.*" "a does not escape"
1818
// Cost 32 body (need cost > 20 and cost < 80).
1919
// See cmd/compile/internal/gc/inl.go:inlineBigFunction*
2020
return a[0] + a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7]
2121
}
2222

23-
func f(a []int) int { // ERROR "cannot inline f:.*" "a does not escape" "function f considered 'big'" "mutates param: a derefs=0" "does not call param: a"
23+
func f(a []int) int { // ERROR "cannot inline f:.*" "a does not escape" "function f considered 'big'"
2424
// Add lots of nodes to f's body. We need >5000.
2525
// See cmd/compile/internal/gc/inl.go:inlineBigFunction*
2626
a[0] = 0

0 commit comments

Comments
 (0)