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.
10 "internal/goexperiment"
21 // TestIntendedInlining tests that specific functions are inlined.
22 // This allows refactoring for code clarity and re-use without fear that
23 // changes to the compiler will cause silent performance regressions.
24 func TestIntendedInlining(t *testing.T) {
25 if testing.Short() && testenv.Builder() == "" {
26 t.Skip("skipping in short mode")
28 testenv.MustHaveGoRun(t)
31 // want is the list of function names (by package) that should
32 // be inlinable. If they have no callers in their packages, they
33 // might not actually be inlined anywhere.
34 want := map[string][]string{
76 "writeHeapBitsForAddr",
83 "(*gcWork).tryGetFast",
85 "(*markBits).advance",
86 "(*mspan).allocBitsForIndex",
88 "(*mspan).markBitsForBase",
89 "(*mspan).markBitsForIndex",
93 "runtime/internal/sys": {},
94 "runtime/internal/math": {
104 "(*Buffer).ReadByte",
107 "(*Buffer).UnreadByte",
108 "(*Buffer).tryGrowByReslice",
114 "(*dictDecoder).tryWriteCopy",
134 "Value.CanInterface",
147 "Value.OverflowComplex",
148 "Value.OverflowFloat",
150 "Value.OverflowUint",
159 "flag.mustBeAssignable",
160 "flag.mustBeExported",
169 // The following functions require the math_big_pure_go build tag.
174 "(*rngSource).Int63",
175 "(*rngSource).Uint64",
178 "(*UDPConn).ReadFromUDP",
181 // (*Bool).CompareAndSwap handled below.
186 "(*Int32).CompareAndSwap",
191 "(*Int64).CompareAndSwap",
196 "(*Uint32).CompareAndSwap",
201 "(*Uint64).CompareAndSwap",
206 "(*Uintptr).CompareAndSwap",
210 // (*Pointer[T])'s methods' handled below.
214 if runtime.GOARCH != "386" && runtime.GOARCH != "loong64" && runtime.GOARCH != "mips64" && runtime.GOARCH != "mips64le" && runtime.GOARCH != "riscv64" {
215 // nextFreeFast calls sys.Ctz64, which on 386 is implemented in asm and is not inlinable.
216 // We currently don't have midstack inlining so nextFreeFast is also not inlinable on 386.
217 // On loong64, mips64x and riscv64, Ctz64 is not intrinsified and causes nextFreeFast too expensive
218 // to inline (Issue 22239).
219 want["runtime"] = append(want["runtime"], "nextFreeFast")
220 // Same behavior for heapBits.nextFast.
221 want["runtime"] = append(want["runtime"], "heapBits.nextFast")
223 if runtime.GOARCH != "386" {
224 // As explained above, Ctz64 and Ctz32 are not Go code on 386.
225 // The same applies to Bswap32.
226 want["runtime/internal/sys"] = append(want["runtime/internal/sys"], "Ctz64")
227 want["runtime/internal/sys"] = append(want["runtime/internal/sys"], "Ctz32")
228 want["runtime/internal/sys"] = append(want["runtime/internal/sys"], "Bswap32")
230 if bits.UintSize == 64 {
231 // mix is only defined on 64-bit architectures
232 want["runtime"] = append(want["runtime"], "mix")
233 // (*Bool).CompareAndSwap is just over budget on 32-bit systems (386, arm).
234 want["sync/atomic"] = append(want["sync/atomic"], "(*Bool).CompareAndSwap")
236 if buildcfg.Experiment.Unified {
237 // Non-unified IR does not report "inlining call ..." for atomic.Pointer[T]'s methods.
238 // TODO(cuonglm): remove once non-unified IR frontend gone.
239 want["sync/atomic"] = append(want["sync/atomic"], "(*Pointer[go.shape.int]).CompareAndSwap")
240 want["sync/atomic"] = append(want["sync/atomic"], "(*Pointer[go.shape.int]).Load")
241 want["sync/atomic"] = append(want["sync/atomic"], "(*Pointer[go.shape.int]).Store")
242 want["sync/atomic"] = append(want["sync/atomic"], "(*Pointer[go.shape.int]).Swap")
245 switch runtime.GOARCH {
246 case "386", "wasm", "arm":
248 // TODO(mvdan): As explained in /test/inline_sync.go, some
249 // architectures don't have atomic intrinsics, so these go over
250 // the inlining budget. Move back to the main table once that
251 // problem is solved.
252 want["sync"] = []string{
256 "(*RWMutex).RUnlock",
261 // Functions that must actually be inlined; they must have actual callers.
262 must := map[string]bool{
263 "compress/flate.byLiteral.Len": true,
264 "compress/flate.byLiteral.Less": true,
265 "compress/flate.byLiteral.Swap": true,
268 notInlinedReason := make(map[string]string)
269 pkgs := make([]string, 0, len(want))
270 for pname, fnames := range want {
271 pkgs = append(pkgs, pname)
272 for _, fname := range fnames {
273 fullName := pname + "." + fname
274 if _, ok := notInlinedReason[fullName]; ok {
275 t.Errorf("duplicate func: %s", fullName)
277 notInlinedReason[fullName] = "unknown reason"
281 args := append([]string{"build", "-gcflags=-m -m", "-tags=math_big_pure_go"}, pkgs...)
282 cmd := testenv.CleanCmdEnv(exec.Command(testenv.GoToolPath(t), args...))
286 cmdErr := make(chan error, 1)
291 scanner := bufio.NewScanner(pr)
293 canInline := regexp.MustCompile(`: can inline ([^ ]*)`)
294 haveInlined := regexp.MustCompile(`: inlining call to ([^ ]*)`)
295 cannotInline := regexp.MustCompile(`: cannot inline ([^ ]*): (.*)`)
297 line := scanner.Text()
298 if strings.HasPrefix(line, "# ") {
302 if m := haveInlined.FindStringSubmatch(line); m != nil {
304 delete(notInlinedReason, curPkg+"."+fname)
307 if m := canInline.FindStringSubmatch(line); m != nil {
309 fullname := curPkg + "." + fname
310 // If function must be inlined somewhere, being inlinable is not enough
311 if _, ok := must[fullname]; !ok {
312 delete(notInlinedReason, fullname)
316 if m := cannotInline.FindStringSubmatch(line); m != nil {
317 fname, reason := m[1], m[2]
318 fullName := curPkg + "." + fname
319 if _, ok := notInlinedReason[fullName]; ok {
320 // cmd/compile gave us a reason why
321 notInlinedReason[fullName] = reason
326 if err := <-cmdErr; err != nil {
329 if err := scanner.Err(); err != nil {
332 for fullName, reason := range notInlinedReason {
333 t.Errorf("%s was not inlined: %s", fullName, reason)
337 func collectInlCands(msgs string) map[string]struct{} {
338 rv := make(map[string]struct{})
339 lines := strings.Split(msgs, "\n")
340 re := regexp.MustCompile(`^\S+\s+can\s+inline\s+(\S+)`)
341 for _, line := range lines {
342 m := re.FindStringSubmatch(line)
344 rv[m[1]] = struct{}{}
350 func TestIssue56044(t *testing.T) {
352 t.Skipf("skipping test: too long for short mode")
354 if !goexperiment.CoverageRedesign {
355 t.Skipf("skipping new coverage tests (experiment not enabled)")
358 testenv.MustHaveGoBuild(t)
360 modes := []string{"-covermode=set", "-covermode=atomic"}
362 for _, mode := range modes {
363 // Build the Go runtime with "-m", capturing output.
364 args := []string{"build", "-gcflags=runtime=-m", "runtime"}
365 cmd := exec.Command(testenv.GoToolPath(t), args...)
366 b, err := cmd.CombinedOutput()
368 t.Fatalf("build failed (%v): %s", err, b)
370 mbase := collectInlCands(string(b))
372 // Redo the build with -cover, also with "-m".
373 args = []string{"build", "-gcflags=runtime=-m", mode, "runtime"}
374 cmd = exec.Command(testenv.GoToolPath(t), args...)
375 b, err = cmd.CombinedOutput()
377 t.Fatalf("build failed (%v): %s", err, b)
379 mcov := collectInlCands(string(b))
381 // Make sure that there aren't any functions that are marked
382 // as inline candidates at base but not with coverage.
383 for k := range mbase {
384 if _, ok := mcov[k]; !ok {
385 t.Errorf("error: did not find %s in coverage -m output", k)