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  			"FullRune",
   129  			"FullRuneInString",
   130  			"RuneLen",
   131  			"AppendRune",
   132  			"ValidRune",
   133  		},
   134  		"unicode/utf16": {
   135  			"Decode",
   136  		},
   137  		"reflect": {
   138  			"Value.Bool",
   139  			"Value.Bytes",
   140  			"Value.CanAddr",
   141  			"Value.CanComplex",
   142  			"Value.CanFloat",
   143  			"Value.CanInt",
   144  			"Value.CanInterface",
   145  			"Value.CanSet",
   146  			"Value.CanUint",
   147  			"Value.Cap",
   148  			"Value.Complex",
   149  			"Value.Float",
   150  			"Value.Int",
   151  			"Value.Interface",
   152  			"Value.IsNil",
   153  			"Value.IsValid",
   154  			"Value.Kind",
   155  			"Value.Len",
   156  			"Value.MapRange",
   157  			"Value.OverflowComplex",
   158  			"Value.OverflowFloat",
   159  			"Value.OverflowInt",
   160  			"Value.OverflowUint",
   161  			"Value.String",
   162  			"Value.Type",
   163  			"Value.Uint",
   164  			"Value.UnsafeAddr",
   165  			"Value.pointer",
   166  			"add",
   167  			"align",
   168  			"flag.mustBe",
   169  			"flag.mustBeAssignable",
   170  			"flag.mustBeExported",
   171  			"flag.kind",
   172  			"flag.ro",
   173  		},
   174  		"regexp": {
   175  			"(*bitState).push",
   176  		},
   177  		"math/big": {
   178  			"bigEndianWord",
   179  		},
   180  		"math/rand": {
   181  			"(*rngSource).Int63",
   182  			"(*rngSource).Uint64",
   183  		},
   184  		"net": {
   185  			"(*UDPConn).ReadFromUDP",
   186  		},
   187  		"sync": {
   188  			// Both OnceFunc and its returned closure need to be inlinable so
   189  			// that the returned closure can be inlined into the caller of OnceFunc.
   190  			"OnceFunc",
   191  			"OnceFunc.func1", // The returned closure.
   192  			// TODO(austin): It would be good to check OnceValue and OnceValues,
   193  			// too, but currently they aren't reported because they have type
   194  			// parameters and aren't instantiated in sync.
   195  		},
   196  		"sync/atomic": {
   197  			// (*Bool).CompareAndSwap handled below.
   198  			"(*Bool).Load",
   199  			"(*Bool).Store",
   200  			"(*Bool).Swap",
   201  			"(*Int32).Add",
   202  			"(*Int32).CompareAndSwap",
   203  			"(*Int32).Load",
   204  			"(*Int32).Store",
   205  			"(*Int32).Swap",
   206  			"(*Int64).Add",
   207  			"(*Int64).CompareAndSwap",
   208  			"(*Int64).Load",
   209  			"(*Int64).Store",
   210  			"(*Int64).Swap",
   211  			"(*Uint32).Add",
   212  			"(*Uint32).CompareAndSwap",
   213  			"(*Uint32).Load",
   214  			"(*Uint32).Store",
   215  			"(*Uint32).Swap",
   216  			"(*Uint64).Add",
   217  			"(*Uint64).CompareAndSwap",
   218  			"(*Uint64).Load",
   219  			"(*Uint64).Store",
   220  			"(*Uint64).Swap",
   221  			"(*Uintptr).Add",
   222  			"(*Uintptr).CompareAndSwap",
   223  			"(*Uintptr).Load",
   224  			"(*Uintptr).Store",
   225  			"(*Uintptr).Swap",
   226  			"(*Pointer[go.shape.int]).CompareAndSwap",
   227  			"(*Pointer[go.shape.int]).Load",
   228  			"(*Pointer[go.shape.int]).Store",
   229  			"(*Pointer[go.shape.int]).Swap",
   230  		},
   231  		"testing": {
   232  			"(*B).Loop",
   233  		},
   234  	}
   235  
   236  	if runtime.GOARCH != "386" && runtime.GOARCH != "loong64" && runtime.GOARCH != "mips64" && runtime.GOARCH != "mips64le" && runtime.GOARCH != "riscv64" {
   237  		// nextFreeFast calls sys.TrailingZeros64, which on 386 is implemented in asm and is not inlinable.
   238  		// We currently don't have midstack inlining so nextFreeFast is also not inlinable on 386.
   239  		// On loong64, mips64x and riscv64, TrailingZeros64 is not intrinsified and causes nextFreeFast
   240  		// too expensive to inline (Issue 22239).
   241  		want["runtime"] = append(want["runtime"], "nextFreeFast")
   242  	}
   243  	if runtime.GOARCH != "386" {
   244  		// As explained above, TrailingZeros64 and TrailingZeros32 are not Go code on 386.
   245  		// The same applies to Bswap32.
   246  		want["internal/runtime/sys"] = append(want["internal/runtime/sys"], "TrailingZeros64")
   247  		want["internal/runtime/sys"] = append(want["internal/runtime/sys"], "TrailingZeros32")
   248  		want["internal/runtime/sys"] = append(want["internal/runtime/sys"], "Bswap32")
   249  	}
   250  	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" {
   251  		// internal/runtime/atomic.Loaduintptr is only intrinsified on these platforms.
   252  		want["runtime"] = append(want["runtime"], "traceAcquire")
   253  	}
   254  	if bits.UintSize == 64 {
   255  		// mix is only defined on 64-bit architectures
   256  		want["runtime"] = append(want["runtime"], "mix")
   257  		// (*Bool).CompareAndSwap is just over budget on 32-bit systems (386, arm).
   258  		want["sync/atomic"] = append(want["sync/atomic"], "(*Bool).CompareAndSwap")
   259  	}
   260  
   261  	switch runtime.GOARCH {
   262  	case "386", "wasm", "arm":
   263  	default:
   264  		// TODO(mvdan): As explained in /test/inline_sync.go, some
   265  		// architectures don't have atomic intrinsics, so these go over
   266  		// the inlining budget. Move back to the main table once that
   267  		// problem is solved.
   268  		want["sync"] = []string{
   269  			"(*Mutex).Lock",
   270  			"(*Mutex).Unlock",
   271  			"(*RWMutex).RLock",
   272  			"(*RWMutex).RUnlock",
   273  			"(*Once).Do",
   274  		}
   275  	}
   276  
   277  	if runtime.GOARCH != "wasm" {
   278  		// mutex implementation for multi-threaded GOARCHes
   279  		want["runtime"] = append(want["runtime"],
   280  			// in the fast paths of lock2 and unlock2
   281  			"key8",
   282  			"(*mLockProfile).store",
   283  		)
   284  		if bits.UintSize == 64 {
   285  			// these use 64-bit arithmetic, which is hard to inline on 32-bit platforms
   286  			want["runtime"] = append(want["runtime"],
   287  				// in the fast paths of lock2 and unlock2
   288  				"mutexSampleContention",
   289  
   290  				// in a slow path of lock2, but within the critical section
   291  				"(*mLockProfile).end",
   292  			)
   293  		}
   294  	}
   295  
   296  	// Functions that must actually be inlined; they must have actual callers.
   297  	must := map[string]bool{
   298  		"compress/flate.byLiteral.Len":  true,
   299  		"compress/flate.byLiteral.Less": true,
   300  		"compress/flate.byLiteral.Swap": true,
   301  	}
   302  
   303  	notInlinedReason := make(map[string]string)
   304  	pkgs := make([]string, 0, len(want))
   305  	for pname, fnames := range want {
   306  		pkgs = append(pkgs, pname)
   307  		for _, fname := range fnames {
   308  			fullName := pname + "." + fname
   309  			if _, ok := notInlinedReason[fullName]; ok {
   310  				t.Errorf("duplicate func: %s", fullName)
   311  			}
   312  			notInlinedReason[fullName] = "unknown reason"
   313  		}
   314  	}
   315  
   316  	args := append([]string{"build", "-gcflags=-m -m", "-tags=math_big_pure_go"}, pkgs...)
   317  	cmd := testenv.CleanCmdEnv(testenv.Command(t, testenv.GoToolPath(t), args...))
   318  	pr, pw := io.Pipe()
   319  	cmd.Stdout = pw
   320  	cmd.Stderr = pw
   321  	cmdErr := make(chan error, 1)
   322  	go func() {
   323  		cmdErr <- cmd.Run()
   324  		pw.Close()
   325  	}()
   326  	scanner := bufio.NewScanner(pr)
   327  	curPkg := ""
   328  	canInline := regexp.MustCompile(`: can inline ([^ ]*)`)
   329  	haveInlined := regexp.MustCompile(`: inlining call to ([^ ]*)`)
   330  	cannotInline := regexp.MustCompile(`: cannot inline ([^ ]*): (.*)`)
   331  	for scanner.Scan() {
   332  		line := scanner.Text()
   333  		if strings.HasPrefix(line, "# ") {
   334  			curPkg = line[2:]
   335  			continue
   336  		}
   337  		if m := haveInlined.FindStringSubmatch(line); m != nil {
   338  			fname := m[1]
   339  			delete(notInlinedReason, curPkg+"."+fname)
   340  			continue
   341  		}
   342  		if m := canInline.FindStringSubmatch(line); m != nil {
   343  			fname := m[1]
   344  			fullname := curPkg + "." + fname
   345  			// If function must be inlined somewhere, being inlinable is not enough
   346  			if _, ok := must[fullname]; !ok {
   347  				delete(notInlinedReason, fullname)
   348  				continue
   349  			}
   350  		}
   351  		if m := cannotInline.FindStringSubmatch(line); m != nil {
   352  			fname, reason := m[1], m[2]
   353  			fullName := curPkg + "." + fname
   354  			if _, ok := notInlinedReason[fullName]; ok {
   355  				// cmd/compile gave us a reason why
   356  				notInlinedReason[fullName] = reason
   357  			}
   358  			continue
   359  		}
   360  	}
   361  	if err := <-cmdErr; err != nil {
   362  		t.Fatal(err)
   363  	}
   364  	if err := scanner.Err(); err != nil {
   365  		t.Fatal(err)
   366  	}
   367  	for fullName, reason := range notInlinedReason {
   368  		t.Errorf("%s was not inlined: %s", fullName, reason)
   369  	}
   370  }
   371  
   372  func collectInlCands(msgs string) map[string]struct{} {
   373  	rv := make(map[string]struct{})
   374  	lines := strings.Split(msgs, "\n")
   375  	re := regexp.MustCompile(`^\S+\s+can\s+inline\s+(\S+)`)
   376  	for _, line := range lines {
   377  		m := re.FindStringSubmatch(line)
   378  		if m != nil {
   379  			rv[m[1]] = struct{}{}
   380  		}
   381  	}
   382  	return rv
   383  }
   384  
   385  func TestIssue56044(t *testing.T) {
   386  	if testing.Short() {
   387  		t.Skipf("skipping test: too long for short mode")
   388  	}
   389  	testenv.MustHaveGoBuild(t)
   390  
   391  	modes := []string{"-covermode=set", "-covermode=atomic"}
   392  
   393  	for _, mode := range modes {
   394  		// Build the Go runtime with "-m", capturing output.
   395  		args := []string{"build", "-gcflags=runtime=-m", "runtime"}
   396  		cmd := testenv.Command(t, testenv.GoToolPath(t), args...)
   397  		b, err := cmd.CombinedOutput()
   398  		if err != nil {
   399  			t.Fatalf("build failed (%v): %s", err, b)
   400  		}
   401  		mbase := collectInlCands(string(b))
   402  
   403  		// Redo the build with -cover, also with "-m".
   404  		args = []string{"build", "-gcflags=runtime=-m", mode, "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  		mcov := collectInlCands(string(b))
   411  
   412  		// Make sure that there aren't any functions that are marked
   413  		// as inline candidates at base but not with coverage.
   414  		for k := range mbase {
   415  			if _, ok := mcov[k]; !ok {
   416  				t.Errorf("error: did not find %s in coverage -m output", k)
   417  			}
   418  		}
   419  	}
   420  }
   421  

View as plain text