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

View as plain text