Source file src/cmd/compile/internal/test/inl_test.go

     1  // Copyright 2017 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package test
     6  
     7  import (
     8  	"bufio"
     9  	"internal/goexperiment"
    10  	"internal/testenv"
    11  	"io"
    12  	"math/bits"
    13  	"regexp"
    14  	"runtime"
    15  	"strings"
    16  	"testing"
    17  )
    18  
    19  // TestIntendedInlining tests that specific functions are inlined.
    20  // This allows refactoring for code clarity and re-use without fear that
    21  // changes to the compiler will cause silent performance regressions.
    22  func TestIntendedInlining(t *testing.T) {
    23  	if testing.Short() && testenv.Builder() == "" {
    24  		t.Skip("skipping in short mode")
    25  	}
    26  	testenv.MustHaveGoRun(t)
    27  	t.Parallel()
    28  
    29  	// want is the list of function names (by package) that should
    30  	// be inlinable. If they have no callers in their packages, they
    31  	// might not actually be inlined anywhere.
    32  	want := map[string][]string{
    33  		"runtime": {
    34  			"add",
    35  			"acquirem",
    36  			"add1",
    37  			"addb",
    38  			"adjustpanics",
    39  			"adjustpointer",
    40  			"alignDown",
    41  			"alignUp",
    42  			"bucketMask",
    43  			"bucketShift",
    44  			"chanbuf",
    45  			"evacuated",
    46  			"fastlog2",
    47  			"float64bits",
    48  			"funcspdelta",
    49  			"getm",
    50  			"getMCache",
    51  			"isDirectIface",
    52  			"itabHashFunc",
    53  			"nextslicecap",
    54  			"noescape",
    55  			"pcvalueCacheKey",
    56  			"rand32",
    57  			"readUnaligned32",
    58  			"readUnaligned64",
    59  			"releasem",
    60  			"roundupsize",
    61  			"stackmapdata",
    62  			"stringStructOf",
    63  			"subtract1",
    64  			"subtractb",
    65  			"tophash",
    66  			"(*bmap).keys",
    67  			"(*bmap).overflow",
    68  			"(*waitq).enqueue",
    69  			"funcInfo.entry",
    70  
    71  			// GC-related ones
    72  			"cgoInRange",
    73  			"gclinkptr.ptr",
    74  			"guintptr.ptr",
    75  			"writeHeapBitsForAddr",
    76  			"heapBitsSlice",
    77  			"markBits.isMarked",
    78  			"muintptr.ptr",
    79  			"puintptr.ptr",
    80  			"spanOf",
    81  			"spanOfUnchecked",
    82  			"typePointers.nextFast",
    83  			"(*gcWork).putFast",
    84  			"(*gcWork).tryGetFast",
    85  			"(*guintptr).set",
    86  			"(*markBits).advance",
    87  			"(*mspan).allocBitsForIndex",
    88  			"(*mspan).base",
    89  			"(*mspan).markBitsForBase",
    90  			"(*mspan).markBitsForIndex",
    91  			"(*mspan).writeUserArenaHeapBits",
    92  			"(*muintptr).set",
    93  			"(*puintptr).set",
    94  			"(*wbBuf).get1",
    95  			"(*wbBuf).get2",
    96  
    97  			// Trace-related ones.
    98  			"traceLocker.ok",
    99  			"traceEnabled",
   100  		},
   101  		"runtime/internal/sys": {},
   102  		"runtime/internal/math": {
   103  			"MulUintptr",
   104  		},
   105  		"bytes": {
   106  			"(*Buffer).Bytes",
   107  			"(*Buffer).Cap",
   108  			"(*Buffer).Len",
   109  			"(*Buffer).Grow",
   110  			"(*Buffer).Next",
   111  			"(*Buffer).Read",
   112  			"(*Buffer).ReadByte",
   113  			"(*Buffer).Reset",
   114  			"(*Buffer).String",
   115  			"(*Buffer).UnreadByte",
   116  			"(*Buffer).tryGrowByReslice",
   117  		},
   118  		"internal/abi": {
   119  			"UseInterfaceSwitchCache",
   120  		},
   121  		"compress/flate": {
   122  			"byLiteral.Len",
   123  			"byLiteral.Less",
   124  			"byLiteral.Swap",
   125  			"(*dictDecoder).tryWriteCopy",
   126  		},
   127  		"encoding/base64": {
   128  			"assemble32",
   129  			"assemble64",
   130  		},
   131  		"unicode/utf8": {
   132  			"FullRune",
   133  			"FullRuneInString",
   134  			"RuneLen",
   135  			"AppendRune",
   136  			"ValidRune",
   137  		},
   138  		"unicode/utf16": {
   139  			"Decode",
   140  		},
   141  		"reflect": {
   142  			"Value.Bool",
   143  			"Value.Bytes",
   144  			"Value.CanAddr",
   145  			"Value.CanComplex",
   146  			"Value.CanFloat",
   147  			"Value.CanInt",
   148  			"Value.CanInterface",
   149  			"Value.CanSet",
   150  			"Value.CanUint",
   151  			"Value.Cap",
   152  			"Value.Complex",
   153  			"Value.Float",
   154  			"Value.Int",
   155  			"Value.Interface",
   156  			"Value.IsNil",
   157  			"Value.IsValid",
   158  			"Value.Kind",
   159  			"Value.Len",
   160  			"Value.MapRange",
   161  			"Value.OverflowComplex",
   162  			"Value.OverflowFloat",
   163  			"Value.OverflowInt",
   164  			"Value.OverflowUint",
   165  			"Value.String",
   166  			"Value.Type",
   167  			"Value.Uint",
   168  			"Value.UnsafeAddr",
   169  			"Value.pointer",
   170  			"add",
   171  			"align",
   172  			"flag.mustBe",
   173  			"flag.mustBeAssignable",
   174  			"flag.mustBeExported",
   175  			"flag.kind",
   176  			"flag.ro",
   177  		},
   178  		"regexp": {
   179  			"(*bitState).push",
   180  		},
   181  		"math/big": {
   182  			"bigEndianWord",
   183  			// The following functions require the math_big_pure_go build tag.
   184  			"addVW",
   185  			"subVW",
   186  		},
   187  		"math/rand": {
   188  			"(*rngSource).Int63",
   189  			"(*rngSource).Uint64",
   190  		},
   191  		"net": {
   192  			"(*UDPConn).ReadFromUDP",
   193  		},
   194  		"sync": {
   195  			// Both OnceFunc and its returned closure need to be inlinable so
   196  			// that the returned closure can be inlined into the caller of OnceFunc.
   197  			"OnceFunc",
   198  			"OnceFunc.func2", // The returned closure.
   199  			// TODO(austin): It would be good to check OnceValue and OnceValues,
   200  			// too, but currently they aren't reported because they have type
   201  			// parameters and aren't instantiated in sync.
   202  		},
   203  		"sync/atomic": {
   204  			// (*Bool).CompareAndSwap handled below.
   205  			"(*Bool).Load",
   206  			"(*Bool).Store",
   207  			"(*Bool).Swap",
   208  			"(*Int32).Add",
   209  			"(*Int32).CompareAndSwap",
   210  			"(*Int32).Load",
   211  			"(*Int32).Store",
   212  			"(*Int32).Swap",
   213  			"(*Int64).Add",
   214  			"(*Int64).CompareAndSwap",
   215  			"(*Int64).Load",
   216  			"(*Int64).Store",
   217  			"(*Int64).Swap",
   218  			"(*Uint32).Add",
   219  			"(*Uint32).CompareAndSwap",
   220  			"(*Uint32).Load",
   221  			"(*Uint32).Store",
   222  			"(*Uint32).Swap",
   223  			"(*Uint64).Add",
   224  			"(*Uint64).CompareAndSwap",
   225  			"(*Uint64).Load",
   226  			"(*Uint64).Store",
   227  			"(*Uint64).Swap",
   228  			"(*Uintptr).Add",
   229  			"(*Uintptr).CompareAndSwap",
   230  			"(*Uintptr).Load",
   231  			"(*Uintptr).Store",
   232  			"(*Uintptr).Swap",
   233  			"(*Pointer[go.shape.int]).CompareAndSwap",
   234  			"(*Pointer[go.shape.int]).Load",
   235  			"(*Pointer[go.shape.int]).Store",
   236  			"(*Pointer[go.shape.int]).Swap",
   237  		},
   238  	}
   239  
   240  	if runtime.GOARCH != "386" && runtime.GOARCH != "loong64" && runtime.GOARCH != "mips64" && runtime.GOARCH != "mips64le" && runtime.GOARCH != "riscv64" {
   241  		// nextFreeFast calls sys.TrailingZeros64, which on 386 is implemented in asm and is not inlinable.
   242  		// We currently don't have midstack inlining so nextFreeFast is also not inlinable on 386.
   243  		// On loong64, mips64x and riscv64, TrailingZeros64 is not intrinsified and causes nextFreeFast
   244  		// too expensive to inline (Issue 22239).
   245  		want["runtime"] = append(want["runtime"], "nextFreeFast")
   246  		// Same behavior for heapBits.nextFast.
   247  		want["runtime"] = append(want["runtime"], "heapBits.nextFast")
   248  	}
   249  	if runtime.GOARCH != "386" {
   250  		// As explained above, TrailingZeros64 and TrailingZeros32 are not Go code on 386.
   251  		// The same applies to Bswap32.
   252  		want["runtime/internal/sys"] = append(want["runtime/internal/sys"], "TrailingZeros64")
   253  		want["runtime/internal/sys"] = append(want["runtime/internal/sys"], "TrailingZeros32")
   254  		want["runtime/internal/sys"] = append(want["runtime/internal/sys"], "Bswap32")
   255  	}
   256  	if runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "mips" || runtime.GOARCH == "mips64" || runtime.GOARCH == "ppc64" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" {
   257  		// runtime/internal/atomic.Loaduintptr is only intrinsified on these platforms.
   258  		want["runtime"] = append(want["runtime"], "traceAcquire")
   259  	}
   260  	if bits.UintSize == 64 {
   261  		// mix is only defined on 64-bit architectures
   262  		want["runtime"] = append(want["runtime"], "mix")
   263  		// (*Bool).CompareAndSwap is just over budget on 32-bit systems (386, arm).
   264  		want["sync/atomic"] = append(want["sync/atomic"], "(*Bool).CompareAndSwap")
   265  	}
   266  
   267  	switch runtime.GOARCH {
   268  	case "386", "wasm", "arm":
   269  	default:
   270  		// TODO(mvdan): As explained in /test/inline_sync.go, some
   271  		// architectures don't have atomic intrinsics, so these go over
   272  		// the inlining budget. Move back to the main table once that
   273  		// problem is solved.
   274  		want["sync"] = []string{
   275  			"(*Mutex).Lock",
   276  			"(*Mutex).Unlock",
   277  			"(*RWMutex).RLock",
   278  			"(*RWMutex).RUnlock",
   279  			"(*Once).Do",
   280  		}
   281  	}
   282  
   283  	// Functions that must actually be inlined; they must have actual callers.
   284  	must := map[string]bool{
   285  		"compress/flate.byLiteral.Len":  true,
   286  		"compress/flate.byLiteral.Less": true,
   287  		"compress/flate.byLiteral.Swap": true,
   288  	}
   289  
   290  	notInlinedReason := make(map[string]string)
   291  	pkgs := make([]string, 0, len(want))
   292  	for pname, fnames := range want {
   293  		pkgs = append(pkgs, pname)
   294  		for _, fname := range fnames {
   295  			fullName := pname + "." + fname
   296  			if _, ok := notInlinedReason[fullName]; ok {
   297  				t.Errorf("duplicate func: %s", fullName)
   298  			}
   299  			notInlinedReason[fullName] = "unknown reason"
   300  		}
   301  	}
   302  
   303  	args := append([]string{"build", "-gcflags=-m -m", "-tags=math_big_pure_go"}, pkgs...)
   304  	cmd := testenv.CleanCmdEnv(testenv.Command(t, testenv.GoToolPath(t), args...))
   305  	pr, pw := io.Pipe()
   306  	cmd.Stdout = pw
   307  	cmd.Stderr = pw
   308  	cmdErr := make(chan error, 1)
   309  	go func() {
   310  		cmdErr <- cmd.Run()
   311  		pw.Close()
   312  	}()
   313  	scanner := bufio.NewScanner(pr)
   314  	curPkg := ""
   315  	canInline := regexp.MustCompile(`: can inline ([^ ]*)`)
   316  	haveInlined := regexp.MustCompile(`: inlining call to ([^ ]*)`)
   317  	cannotInline := regexp.MustCompile(`: cannot inline ([^ ]*): (.*)`)
   318  	for scanner.Scan() {
   319  		line := scanner.Text()
   320  		if strings.HasPrefix(line, "# ") {
   321  			curPkg = line[2:]
   322  			continue
   323  		}
   324  		if m := haveInlined.FindStringSubmatch(line); m != nil {
   325  			fname := m[1]
   326  			delete(notInlinedReason, curPkg+"."+fname)
   327  			continue
   328  		}
   329  		if m := canInline.FindStringSubmatch(line); m != nil {
   330  			fname := m[1]
   331  			fullname := curPkg + "." + fname
   332  			// If function must be inlined somewhere, being inlinable is not enough
   333  			if _, ok := must[fullname]; !ok {
   334  				delete(notInlinedReason, fullname)
   335  				continue
   336  			}
   337  		}
   338  		if m := cannotInline.FindStringSubmatch(line); m != nil {
   339  			fname, reason := m[1], m[2]
   340  			fullName := curPkg + "." + fname
   341  			if _, ok := notInlinedReason[fullName]; ok {
   342  				// cmd/compile gave us a reason why
   343  				notInlinedReason[fullName] = reason
   344  			}
   345  			continue
   346  		}
   347  	}
   348  	if err := <-cmdErr; err != nil {
   349  		t.Fatal(err)
   350  	}
   351  	if err := scanner.Err(); err != nil {
   352  		t.Fatal(err)
   353  	}
   354  	for fullName, reason := range notInlinedReason {
   355  		t.Errorf("%s was not inlined: %s", fullName, reason)
   356  	}
   357  }
   358  
   359  func collectInlCands(msgs string) map[string]struct{} {
   360  	rv := make(map[string]struct{})
   361  	lines := strings.Split(msgs, "\n")
   362  	re := regexp.MustCompile(`^\S+\s+can\s+inline\s+(\S+)`)
   363  	for _, line := range lines {
   364  		m := re.FindStringSubmatch(line)
   365  		if m != nil {
   366  			rv[m[1]] = struct{}{}
   367  		}
   368  	}
   369  	return rv
   370  }
   371  
   372  func TestIssue56044(t *testing.T) {
   373  	if testing.Short() {
   374  		t.Skipf("skipping test: too long for short mode")
   375  	}
   376  	if !goexperiment.CoverageRedesign {
   377  		t.Skipf("skipping new coverage tests (experiment not enabled)")
   378  	}
   379  
   380  	testenv.MustHaveGoBuild(t)
   381  
   382  	modes := []string{"-covermode=set", "-covermode=atomic"}
   383  
   384  	for _, mode := range modes {
   385  		// Build the Go runtime with "-m", capturing output.
   386  		args := []string{"build", "-gcflags=runtime=-m", "runtime"}
   387  		cmd := testenv.Command(t, testenv.GoToolPath(t), args...)
   388  		b, err := cmd.CombinedOutput()
   389  		if err != nil {
   390  			t.Fatalf("build failed (%v): %s", err, b)
   391  		}
   392  		mbase := collectInlCands(string(b))
   393  
   394  		// Redo the build with -cover, also with "-m".
   395  		args = []string{"build", "-gcflags=runtime=-m", mode, "runtime"}
   396  		cmd = testenv.Command(t, testenv.GoToolPath(t), args...)
   397  		b, err = cmd.CombinedOutput()
   398  		if err != nil {
   399  			t.Fatalf("build failed (%v): %s", err, b)
   400  		}
   401  		mcov := collectInlCands(string(b))
   402  
   403  		// Make sure that there aren't any functions that are marked
   404  		// as inline candidates at base but not with coverage.
   405  		for k := range mbase {
   406  			if _, ok := mcov[k]; !ok {
   407  				t.Errorf("error: did not find %s in coverage -m output", k)
   408  			}
   409  		}
   410  	}
   411  }
   412  

View as plain text