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  			"readUnaligned64",
    55  			"releasem",
    56  			"roundupsize",
    57  			"stackmapdata",
    58  			"stringStructOf",
    59  			"subtract1",
    60  			"subtractb",
    61  			"(*waitq).enqueue",
    62  			"funcInfo.entry",
    63  
    64  			// GC-related ones
    65  			"cgoInRange",
    66  			"gclinkptr.ptr",
    67  			"gcUsesSpanInlineMarkBits",
    68  			"guintptr.ptr",
    69  			"heapBitsSlice",
    70  			"markBits.isMarked",
    71  			"muintptr.ptr",
    72  			"puintptr.ptr",
    73  			"spanHeapBitsRange",
    74  			"spanOf",
    75  			"spanOfUnchecked",
    76  			"typePointers.nextFast",
    77  			"(*gcWork).putObjFast",
    78  			"(*gcWork).tryGetObjFast",
    79  			"(*guintptr).set",
    80  			"(*markBits).advance",
    81  			"(*mspan).allocBitsForIndex",
    82  			"(*mspan).base",
    83  			"(*mspan).markBitsForBase",
    84  			"(*mspan).markBitsForIndex",
    85  			"(*mspan).writeUserArenaHeapBits",
    86  			"(*muintptr).set",
    87  			"(*puintptr).set",
    88  			"(*wbBuf).get1",
    89  			"(*wbBuf).get2",
    90  
    91  			// Trace-related ones.
    92  			"traceLocker.ok",
    93  			"traceEnabled",
    94  		},
    95  		"bytes": {
    96  			"(*Buffer).Bytes",
    97  			"(*Buffer).Cap",
    98  			"(*Buffer).Len",
    99  			"(*Buffer).Grow",
   100  			"(*Buffer).Next",
   101  			"(*Buffer).Read",
   102  			"(*Buffer).ReadByte",
   103  			"(*Buffer).Reset",
   104  			"(*Buffer).String",
   105  			"(*Buffer).UnreadByte",
   106  			"(*Buffer).tryGrowByReslice",
   107  		},
   108  		"internal/abi": {
   109  			"(*Type).IsDirectIface",
   110  			"UseInterfaceSwitchCache",
   111  		},
   112  		"internal/runtime/math": {
   113  			"MulUintptr",
   114  		},
   115  		"internal/runtime/maps": {
   116  			"readUnaligned32",
   117  			"readUnaligned64",
   118  		},
   119  		"internal/runtime/sys": {},
   120  		"compress/flate": {
   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  		"time": {
   237  			"Duration.String",
   238  		},
   239  		"path": {
   240  			"Base",
   241  			"scanChunk",
   242  		},
   243  		"path/filepath": {
   244  			"scanChunk",
   245  		},
   246  	}
   247  
   248  	if runtime.GOARCH != "386" && runtime.GOARCH != "loong64" && runtime.GOARCH != "mips64" && runtime.GOARCH != "mips64le" && runtime.GOARCH != "riscv64" {
   249  		// nextFreeFast calls sys.TrailingZeros64, which on 386 is implemented in asm and is not inlinable.
   250  		// We currently don't have midstack inlining so nextFreeFast is also not inlinable on 386.
   251  		// On loong64, mips64x and riscv64, TrailingZeros64 is not intrinsified and causes nextFreeFast
   252  		// too expensive to inline (Issue 22239).
   253  		want["runtime"] = append(want["runtime"], "nextFreeFast")
   254  	}
   255  	if runtime.GOARCH != "386" {
   256  		// As explained above, TrailingZeros64 and TrailingZeros32 are not Go code on 386.
   257  		// The same applies to Bswap32.
   258  		want["internal/runtime/sys"] = append(want["internal/runtime/sys"], "TrailingZeros64")
   259  		want["internal/runtime/sys"] = append(want["internal/runtime/sys"], "TrailingZeros32")
   260  		want["internal/runtime/sys"] = append(want["internal/runtime/sys"], "Bswap32")
   261  	}
   262  	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" {
   263  		// internal/runtime/atomic.Loaduintptr is only intrinsified on these platforms.
   264  		want["runtime"] = append(want["runtime"], "traceAcquire")
   265  	}
   266  	if bits.UintSize == 64 {
   267  		// mix is only defined on 64-bit architectures
   268  		want["internal/runtime/maps"] = append(want["internal/runtime/maps"], "mix")
   269  		// (*Bool).CompareAndSwap is just over budget on 32-bit systems (386, arm).
   270  		want["sync/atomic"] = append(want["sync/atomic"], "(*Bool).CompareAndSwap")
   271  	}
   272  
   273  	switch runtime.GOARCH {
   274  	case "386", "wasm", "arm":
   275  	default:
   276  		// TODO(mvdan): As explained in /test/inline_sync.go, some
   277  		// architectures don't have atomic intrinsics, so these go over
   278  		// the inlining budget. Move back to the main table once that
   279  		// problem is solved.
   280  		want["sync"] = []string{
   281  			"(*Mutex).Lock",
   282  			"(*Mutex).Unlock",
   283  			"(*RWMutex).RLock",
   284  			"(*RWMutex).RUnlock",
   285  			"(*Once).Do",
   286  		}
   287  	}
   288  
   289  	if runtime.GOARCH != "wasm" {
   290  		// mutex implementation for multi-threaded GOARCHes
   291  		want["runtime"] = append(want["runtime"],
   292  			// in the fast paths of lock2 and unlock2
   293  			"key8",
   294  			"(*mLockProfile).store",
   295  		)
   296  		if bits.UintSize == 64 {
   297  			// these use 64-bit arithmetic, which is hard to inline on 32-bit platforms
   298  			want["runtime"] = append(want["runtime"],
   299  				// in the fast paths of lock2 and unlock2
   300  				"mutexSampleContention",
   301  
   302  				// in a slow path of lock2, but within the critical section
   303  				"(*mLockProfile).end",
   304  			)
   305  		}
   306  	}
   307  
   308  	// Functions that must actually be inlined; they must have actual callers.
   309  	must := map[string]bool{}
   310  
   311  	notInlinedReason := make(map[string]string)
   312  	pkgs := make([]string, 0, len(want))
   313  	for pname, fnames := range want {
   314  		pkgs = append(pkgs, pname)
   315  		for _, fname := range fnames {
   316  			fullName := pname + "." + fname
   317  			if _, ok := notInlinedReason[fullName]; ok {
   318  				t.Errorf("duplicate func: %s", fullName)
   319  			}
   320  			notInlinedReason[fullName] = "unknown reason"
   321  		}
   322  	}
   323  
   324  	args := append([]string{"build", "-gcflags=-m -m", "-tags=math_big_pure_go"}, pkgs...)
   325  	cmd := testenv.CleanCmdEnv(testenv.Command(t, testenv.GoToolPath(t), args...))
   326  	pr, pw := io.Pipe()
   327  	cmd.Stdout = pw
   328  	cmd.Stderr = pw
   329  	cmdErr := make(chan error, 1)
   330  	go func() {
   331  		cmdErr <- cmd.Run()
   332  		pw.Close()
   333  	}()
   334  	scanner := bufio.NewScanner(pr)
   335  	curPkg := ""
   336  	canInline := regexp.MustCompile(`: can inline ([^ ]*)`)
   337  	haveInlined := regexp.MustCompile(`: inlining call to ([^ ]*)`)
   338  	cannotInline := regexp.MustCompile(`: cannot inline ([^ ]*): (.*)`)
   339  	for scanner.Scan() {
   340  		line := scanner.Text()
   341  		if strings.HasPrefix(line, "# ") {
   342  			curPkg = line[2:]
   343  			continue
   344  		}
   345  		if m := haveInlined.FindStringSubmatch(line); m != nil {
   346  			fname := m[1]
   347  			delete(notInlinedReason, curPkg+"."+fname)
   348  			continue
   349  		}
   350  		if m := canInline.FindStringSubmatch(line); m != nil {
   351  			fname := m[1]
   352  			fullname := curPkg + "." + fname
   353  			// If function must be inlined somewhere, being inlinable is not enough
   354  			if _, ok := must[fullname]; !ok {
   355  				delete(notInlinedReason, fullname)
   356  				continue
   357  			}
   358  		}
   359  		if m := cannotInline.FindStringSubmatch(line); m != nil {
   360  			fname, reason := m[1], m[2]
   361  			fullName := curPkg + "." + fname
   362  			if _, ok := notInlinedReason[fullName]; ok {
   363  				// cmd/compile gave us a reason why
   364  				notInlinedReason[fullName] = reason
   365  			}
   366  			continue
   367  		}
   368  	}
   369  	if err := <-cmdErr; err != nil {
   370  		t.Fatal(err)
   371  	}
   372  	if err := scanner.Err(); err != nil {
   373  		t.Fatal(err)
   374  	}
   375  	for fullName, reason := range notInlinedReason {
   376  		t.Errorf("%s was not inlined: %s", fullName, reason)
   377  	}
   378  }
   379  
   380  func collectInlCands(msgs string) map[string]struct{} {
   381  	rv := make(map[string]struct{})
   382  	lines := strings.Split(msgs, "\n")
   383  	re := regexp.MustCompile(`^\S+\s+can\s+inline\s+(\S+)`)
   384  	for _, line := range lines {
   385  		m := re.FindStringSubmatch(line)
   386  		if m != nil {
   387  			rv[m[1]] = struct{}{}
   388  		}
   389  	}
   390  	return rv
   391  }
   392  
   393  func TestIssue56044(t *testing.T) {
   394  	if testing.Short() {
   395  		t.Skipf("skipping test: too long for short mode")
   396  	}
   397  	testenv.MustHaveGoBuild(t)
   398  
   399  	modes := []string{"-covermode=set", "-covermode=atomic"}
   400  
   401  	for _, mode := range modes {
   402  		// Build the Go runtime with "-m", capturing output.
   403  		args := []string{"build", "-gcflags=runtime=-m", "runtime"}
   404  		cmd := testenv.Command(t, testenv.GoToolPath(t), args...)
   405  		b, err := cmd.CombinedOutput()
   406  		if err != nil {
   407  			t.Fatalf("build failed (%v): %s", err, b)
   408  		}
   409  		mbase := collectInlCands(string(b))
   410  
   411  		// Redo the build with -cover, also with "-m".
   412  		args = []string{"build", "-gcflags=runtime=-m", mode, "runtime"}
   413  		cmd = testenv.Command(t, testenv.GoToolPath(t), args...)
   414  		b, err = cmd.CombinedOutput()
   415  		if err != nil {
   416  			t.Fatalf("build failed (%v): %s", err, b)
   417  		}
   418  		mcov := collectInlCands(string(b))
   419  
   420  		// Make sure that there aren't any functions that are marked
   421  		// as inline candidates at base but not with coverage.
   422  		for k := range mbase {
   423  			if _, ok := mcov[k]; !ok {
   424  				t.Errorf("error: did not find %s in coverage -m output", k)
   425  			}
   426  		}
   427  	}
   428  }
   429  

View as plain text