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

View as plain text