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

View as plain text