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  		"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["runtime"] = append(want["runtime"], "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  		"compress/flate.byLiteral.Len":  true,
   311  		"compress/flate.byLiteral.Less": true,
   312  		"compress/flate.byLiteral.Swap": true,
   313  	}
   314  
   315  	notInlinedReason := make(map[string]string)
   316  	pkgs := make([]string, 0, len(want))
   317  	for pname, fnames := range want {
   318  		pkgs = append(pkgs, pname)
   319  		for _, fname := range fnames {
   320  			fullName := pname + "." + fname
   321  			if _, ok := notInlinedReason[fullName]; ok {
   322  				t.Errorf("duplicate func: %s", fullName)
   323  			}
   324  			notInlinedReason[fullName] = "unknown reason"
   325  		}
   326  	}
   327  
   328  	args := append([]string{"build", "-gcflags=-m -m", "-tags=math_big_pure_go"}, pkgs...)
   329  	cmd := testenv.CleanCmdEnv(testenv.Command(t, testenv.GoToolPath(t), args...))
   330  	pr, pw := io.Pipe()
   331  	cmd.Stdout = pw
   332  	cmd.Stderr = pw
   333  	cmdErr := make(chan error, 1)
   334  	go func() {
   335  		cmdErr <- cmd.Run()
   336  		pw.Close()
   337  	}()
   338  	scanner := bufio.NewScanner(pr)
   339  	curPkg := ""
   340  	canInline := regexp.MustCompile(`: can inline ([^ ]*)`)
   341  	haveInlined := regexp.MustCompile(`: inlining call to ([^ ]*)`)
   342  	cannotInline := regexp.MustCompile(`: cannot inline ([^ ]*): (.*)`)
   343  	for scanner.Scan() {
   344  		line := scanner.Text()
   345  		if strings.HasPrefix(line, "# ") {
   346  			curPkg = line[2:]
   347  			continue
   348  		}
   349  		if m := haveInlined.FindStringSubmatch(line); m != nil {
   350  			fname := m[1]
   351  			delete(notInlinedReason, curPkg+"."+fname)
   352  			continue
   353  		}
   354  		if m := canInline.FindStringSubmatch(line); m != nil {
   355  			fname := m[1]
   356  			fullname := curPkg + "." + fname
   357  			// If function must be inlined somewhere, being inlinable is not enough
   358  			if _, ok := must[fullname]; !ok {
   359  				delete(notInlinedReason, fullname)
   360  				continue
   361  			}
   362  		}
   363  		if m := cannotInline.FindStringSubmatch(line); m != nil {
   364  			fname, reason := m[1], m[2]
   365  			fullName := curPkg + "." + fname
   366  			if _, ok := notInlinedReason[fullName]; ok {
   367  				// cmd/compile gave us a reason why
   368  				notInlinedReason[fullName] = reason
   369  			}
   370  			continue
   371  		}
   372  	}
   373  	if err := <-cmdErr; err != nil {
   374  		t.Fatal(err)
   375  	}
   376  	if err := scanner.Err(); err != nil {
   377  		t.Fatal(err)
   378  	}
   379  	for fullName, reason := range notInlinedReason {
   380  		t.Errorf("%s was not inlined: %s", fullName, reason)
   381  	}
   382  }
   383  
   384  func collectInlCands(msgs string) map[string]struct{} {
   385  	rv := make(map[string]struct{})
   386  	lines := strings.Split(msgs, "\n")
   387  	re := regexp.MustCompile(`^\S+\s+can\s+inline\s+(\S+)`)
   388  	for _, line := range lines {
   389  		m := re.FindStringSubmatch(line)
   390  		if m != nil {
   391  			rv[m[1]] = struct{}{}
   392  		}
   393  	}
   394  	return rv
   395  }
   396  
   397  func TestIssue56044(t *testing.T) {
   398  	if testing.Short() {
   399  		t.Skipf("skipping test: too long for short mode")
   400  	}
   401  	testenv.MustHaveGoBuild(t)
   402  
   403  	modes := []string{"-covermode=set", "-covermode=atomic"}
   404  
   405  	for _, mode := range modes {
   406  		// Build the Go runtime with "-m", capturing output.
   407  		args := []string{"build", "-gcflags=runtime=-m", "runtime"}
   408  		cmd := testenv.Command(t, testenv.GoToolPath(t), args...)
   409  		b, err := cmd.CombinedOutput()
   410  		if err != nil {
   411  			t.Fatalf("build failed (%v): %s", err, b)
   412  		}
   413  		mbase := collectInlCands(string(b))
   414  
   415  		// Redo the build with -cover, also with "-m".
   416  		args = []string{"build", "-gcflags=runtime=-m", mode, "runtime"}
   417  		cmd = testenv.Command(t, testenv.GoToolPath(t), args...)
   418  		b, err = cmd.CombinedOutput()
   419  		if err != nil {
   420  			t.Fatalf("build failed (%v): %s", err, b)
   421  		}
   422  		mcov := collectInlCands(string(b))
   423  
   424  		// Make sure that there aren't any functions that are marked
   425  		// as inline candidates at base but not with coverage.
   426  		for k := range mbase {
   427  			if _, ok := mcov[k]; !ok {
   428  				t.Errorf("error: did not find %s in coverage -m output", k)
   429  			}
   430  		}
   431  	}
   432  }
   433  

View as plain text