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

View as plain text