| // Copyright 2025 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package main | |
| import ( | |
| "bytes" | |
| "flag" | |
| "fmt" | |
| "go/ast" | |
| "go/format" | |
| "go/parser" | |
| "go/token" | |
| "log" | |
| "os" | |
| "strings" | |
| "golang.org/x/tools/go/ast/astutil" | |
| internalastutil "runtime/_mkmalloc/astutil" | |
| ) | |
| var stdout = flag.Bool("stdout", false, "write sizeclasses source to stdout instead of sizeclasses.go") | |
| func makeSizeToSizeClass(classes []class) []uint8 { | |
| sc := uint8(0) | |
| ret := make([]uint8, benchmarkMax+1) | |
| for i := range ret { | |
| if i > classes[sc].size { | |
| sc++ | |
| } | |
| ret[i] = sc | |
| } | |
| return ret | |
| } | |
| func main() { | |
| log.SetFlags(0) | |
| log.SetPrefix("mkmalloc: ") | |
| classes := makeClasses() | |
| sizeToSizeClass := makeSizeToSizeClass(classes) | |
| if *stdout { | |
| if _, err := os.Stdout.Write(mustFormat(generateSizeClasses(classes))); err != nil { | |
| log.Fatal(err) | |
| } | |
| return | |
| } | |
| sizeclasesesfile := "../../internal/runtime/gc/sizeclasses.go" | |
| if err := os.WriteFile(sizeclasesesfile, mustFormat(generateSizeClasses(classes)), 0666); err != nil { | |
| log.Fatal(err) | |
| } | |
| outfile := "../malloc_generated.go" | |
| if err := os.WriteFile(outfile, mustFormat(inline(specializedMallocConfig(classes, sizeToSizeClass))), 0666); err != nil { | |
| log.Fatal(err) | |
| } | |
| tablefile := "../malloc_tables_generated.go" | |
| if err := os.WriteFile(tablefile, mustFormat(generateTable(sizeToSizeClass)), 0666); err != nil { | |
| log.Fatal(err) | |
| } | |
| benchmarkFile := "../malloc_bench_generated_test.go" | |
| if err := os.WriteFile(benchmarkFile, mustFormat(append(inline(benchmarkConfig(classes, sizeToSizeClass)), []byte(generateTopBenchmark(classes, sizeToSizeClass))...)), 0666); err != nil { | |
| log.Fatal(err) | |
| } | |
| } | |
| // withLineNumbers returns b with line numbers added to help debugging. | |
| func withLineNumbers(b []byte) []byte { | |
| var buf bytes.Buffer | |
| i := 1 | |
| for line := range bytes.Lines(b) { | |
| fmt.Fprintf(&buf, "%d: %s", i, line) | |
| i++ | |
| } | |
| return buf.Bytes() | |
| } | |
| // mustFormat formats the input source, or exits if there's an error. | |
| func mustFormat(b []byte) []byte { | |
| formatted, err := format.Source(b) | |
| if err != nil { | |
| log.Fatalf("error formatting source: %v\nsource:\n%s\n", err, withLineNumbers(b)) | |
| } | |
| return formatted | |
| } | |
| // generatorConfig is the configuration for the generator. It uses the given file to find | |
| // its templates, and generates each of the functions specified by specs. | |
| type generatorConfig struct { | |
| file string | |
| specs []spec | |
| } | |
| // spec is the specification for a function for the inliner to produce. The function gets | |
| // the given name, and is produced by starting with the function with the name given by | |
| // templateFunc and applying each of the ops. | |
| type spec struct { | |
| name string | |
| templateFunc string | |
| ops []op | |
| } | |
| // replacementKind specifies the operation to ben done by a op. | |
| type replacementKind int | |
| const ( | |
| inlineFunc = replacementKind(iota) | |
| subBasicLit | |
| foldCondition | |
| subIdent | |
| deleteConst | |
| ) | |
| // op is a single inlining operation for the inliner. Any calls to the function | |
| // from are replaced with the inlined body of to. For non-functions, uses of from are | |
| // replaced with the basic literal expression given by to. | |
| type op struct { | |
| kind replacementKind | |
| from string | |
| to string | |
| } | |
| func smallScanNoHeaderSCFuncName(sc, scMax uint8) string { | |
| if sc == 0 || sc > scMax { | |
| return "mallocPanic" | |
| } | |
| return fmt.Sprintf("mallocgcSmallScanNoHeaderSC%d", sc) | |
| } | |
| const tinyFuncName = "mallocgcTinySC2" | |
| func smallNoScanSCFuncName(sc, scMax uint8) string { | |
| if sc < 2 || sc > scMax { | |
| return "mallocPanic" | |
| } | |
| return fmt.Sprintf("mallocgcSmallNoScanSC%d", sc) | |
| } | |
| // specializedMallocConfig produces an inlining config to stamp out the definitions of the size-specialized | |
| // malloc functions to be written by mkmalloc. | |
| func specializedMallocConfig(classes []class, sizeToSizeClass []uint8) generatorConfig { | |
| config := generatorConfig{file: "../malloc_stubs.go"} | |
| // Only generate specialized functions for sizes up to specializedMallocMax. | |
| // We've noticed limited benefit (or sometimes worse performance) for specialized | |
| // functions for larger sizes, and having too many functions causes icache issues. | |
| scMax := sizeToSizeClass[specializedMallocMax] | |
| str := fmt.Sprint | |
| // allocations with pointer bits | |
| { | |
| const noscan = 0 | |
| for sc := uint8(0); sc <= scMax; sc++ { | |
| if sc == 0 { | |
| continue | |
| } | |
| name := smallScanNoHeaderSCFuncName(sc, scMax) | |
| elemsize := classes[sc].size | |
| config.specs = append(config.specs, spec{ | |
| templateFunc: "mallocStub", | |
| name: name, | |
| ops: []op{ | |
| {inlineFunc, "inlinedMalloc", "smallStub"}, | |
| {inlineFunc, "postMallocgc", "postMallocgc"}, | |
| {foldCondition, "isNoScan_", str(false)}, | |
| {inlineFunc, "heapSetTypeNoHeaderStub", "heapSetTypeNoHeaderStub"}, | |
| {inlineFunc, "nextFreeFastStub", "nextFreeFastStub"}, | |
| {inlineFunc, "writeHeapBitsSmallStub", "writeHeapBitsSmallStub"}, | |
| {foldCondition, "isSlowPath_", str(false)}, | |
| {subBasicLit, "elemsize_", str(elemsize)}, | |
| {subBasicLit, "sizeclass_", str(sc)}, | |
| {subBasicLit, "noscanint_", str(noscan)}, | |
| {foldCondition, "isTiny_", str(false)}, | |
| {subIdent, "mallocgcSlowPathStub", "mallocgcSmallScanSlowPath"}, | |
| }, | |
| }) | |
| } | |
| } | |
| // allocations without pointer bits | |
| { | |
| const noscan = 1 | |
| // tiny | |
| tinySizeClass := sizeToSizeClass[tinySize] | |
| { | |
| name := tinyFuncName | |
| elemsize := classes[tinySizeClass].size | |
| config.specs = append(config.specs, spec{ | |
| templateFunc: "mallocStub", | |
| name: name, | |
| ops: []op{ | |
| {inlineFunc, "inlinedMalloc", "tinyStub"}, | |
| {inlineFunc, "nextFreeFastTiny", "nextFreeFastTiny"}, | |
| {inlineFunc, "postMallocgc", "postMallocgc"}, | |
| {inlineFunc, "nextFreeFastStub", "nextFreeFastStub"}, | |
| {foldCondition, "isSlowPath_", str(false)}, | |
| {subBasicLit, "elemsize_", str(elemsize)}, | |
| {subBasicLit, "sizeclass_", str(tinySizeClass)}, | |
| {subBasicLit, "noscanint_", str(noscan)}, | |
| {foldCondition, "isTiny_", str(true)}, | |
| }, | |
| }) | |
| } | |
| // non-tiny | |
| for sc := uint8(tinySizeClass); sc <= scMax; sc++ { | |
| name := smallNoScanSCFuncName(sc, scMax) | |
| elemsize := classes[sc].size | |
| config.specs = append(config.specs, spec{ | |
| templateFunc: "mallocStub", | |
| name: name, | |
| ops: []op{ | |
| {inlineFunc, "inlinedMalloc", "smallStub"}, | |
| {inlineFunc, "postMallocgc", "postMallocgc"}, | |
| {foldCondition, "isNoScan_", str(true)}, | |
| {inlineFunc, "nextFreeFastStub", "nextFreeFastStub"}, | |
| {foldCondition, "isSlowPath_", str(false)}, | |
| {subBasicLit, "elemsize_", str(elemsize)}, | |
| {subBasicLit, "sizeclass_", str(sc)}, | |
| {subBasicLit, "noscanint_", str(noscan)}, | |
| {foldCondition, "isTiny_", str(false)}, | |
| {subIdent, "mallocgcSlowPathStub", "mallocgcSmallNoScanSlowPath"}, | |
| }, | |
| }) | |
| } | |
| } | |
| // Non-size-specialized fallbacks in case we can't do the fast path. | |
| config.specs = append(config.specs, spec{ | |
| templateFunc: "mallocStub", | |
| name: "mallocgcTinySlowPath", | |
| ops: []op{ | |
| {inlineFunc, "inlinedMalloc", "tinyStub"}, | |
| {inlineFunc, "postMallocgc", "postMallocgc"}, | |
| {inlineFunc, "nextFreeFastTiny", "nextFreeFastTiny"}, | |
| {inlineFunc, "deductAssistCredit", "deductAssistCredit"}, | |
| {foldCondition, "isSlowPath_", str(true)}, | |
| {foldCondition, "isTiny_", str(true)}, | |
| {subBasicLit, "elemsize_", str(classes[sizeToSizeClass[tinySize]].size)}, | |
| }, | |
| }) | |
| config.specs = append(config.specs, spec{ | |
| templateFunc: "mallocgcSlowPathStub", | |
| name: "mallocgcSmallScanSlowPath", | |
| ops: []op{ | |
| {inlineFunc, "mallocStub", "mallocStub"}, | |
| {inlineFunc, "inlinedMalloc", "smallStub"}, | |
| {inlineFunc, "heapSetTypeNoHeaderStub", "heapSetTypeNoHeaderStub"}, | |
| {inlineFunc, "writeHeapBitsSmallStub", "writeHeapBitsSmallStub"}, | |
| {inlineFunc, "postMallocgc", "postMallocgc"}, | |
| {inlineFunc, "nextFreeFastStub", "nextFreeFastStub"}, | |
| {inlineFunc, "deductAssistCredit", "deductAssistCredit"}, | |
| {foldCondition, "isSlowPath_", str(true)}, | |
| {foldCondition, "isTiny_", str(false)}, | |
| {foldCondition, "isNoScan_", str(false)}, | |
| // Remove constants used by size-specialized variants. | |
| {deleteConst, "elemsize", ""}, | |
| {deleteConst, "sizeclass", ""}, | |
| {deleteConst, "spc", ""}, | |
| }, | |
| }) | |
| config.specs = append(config.specs, spec{ | |
| templateFunc: "mallocgcSlowPathStub", | |
| name: "mallocgcSmallNoScanSlowPath", | |
| ops: []op{ | |
| {inlineFunc, "mallocStub", "mallocStub"}, | |
| {inlineFunc, "inlinedMalloc", "smallStub"}, | |
| {inlineFunc, "postMallocgc", "postMallocgc"}, | |
| {inlineFunc, "nextFreeFastStub", "nextFreeFastStub"}, | |
| {inlineFunc, "deductAssistCredit", "deductAssistCredit"}, | |
| {foldCondition, "isSlowPath_", str(true)}, | |
| {foldCondition, "isTiny_", str(false)}, | |
| {foldCondition, "isNoScan_", str(true)}, | |
| // Remove constants used by size-specialized variants. | |
| {deleteConst, "elemsize", ""}, | |
| {deleteConst, "sizeclass", ""}, | |
| {deleteConst, "spc", ""}, | |
| }, | |
| }) | |
| return config | |
| } | |
| // inline applies the inlining operations given by the config. | |
| func inline(config generatorConfig) []byte { | |
| var out bytes.Buffer | |
| // Read the template file in. | |
| fset := token.NewFileSet() | |
| f, err := parser.ParseFile(fset, config.file, nil, parser.SkipObjectResolution) | |
| if err != nil { | |
| log.Fatalf("parsing %s: %v", config.file, err) | |
| } | |
| // Collect the function and import declarations. The function | |
| // declarations in the template file provide both the templates | |
| // that will be stamped out, and the functions that will be inlined | |
| // into them. The imports from the template file will be copied | |
| // straight to the output. | |
| funcDecls := map[string]*ast.FuncDecl{} | |
| importDecls := []*ast.GenDecl{} | |
| for _, decl := range f.Decls { | |
| switch decl := decl.(type) { | |
| case *ast.FuncDecl: | |
| funcDecls[decl.Name.Name] = decl | |
| case *ast.GenDecl: | |
| if decl.Tok.String() == "import" { | |
| importDecls = append(importDecls, decl) | |
| continue | |
| } | |
| } | |
| } | |
| // Write out the package and import declarations. | |
| out.WriteString("// Code generated by mkmalloc.go; DO NOT EDIT.\n") | |
| out.WriteString("// See overview in malloc_stubs.go.\n\n") | |
| out.WriteString("package " + f.Name.Name + "\n\n") | |
| for _, importDecl := range importDecls { | |
| out.Write(mustFormatNode(fset, importDecl)) | |
| out.WriteString("\n\n") | |
| } | |
| // Produce each of the inlined functions specified by specs. | |
| for _, spec := range config.specs { | |
| // Start with a renamed copy of the template function. | |
| containingFuncCopy := internalastutil.CloneNode(funcDecls[spec.templateFunc]) | |
| if containingFuncCopy == nil { | |
| log.Fatal("did not find", spec.templateFunc) | |
| } | |
| containingFuncCopy.Name.Name = spec.name | |
| // Apply each of the ops given by the specs | |
| stamped := ast.Node(containingFuncCopy) | |
| for _, repl := range spec.ops { | |
| switch repl.kind { | |
| case inlineFunc: | |
| if toDecl, ok := funcDecls[repl.to]; ok { | |
| stamped = inlineFunction(stamped, repl.from, toDecl) | |
| } | |
| case subBasicLit: | |
| stamped = substituteWithBasicLit(stamped, repl.from, repl.to) | |
| case foldCondition: | |
| stamped = foldIfCondition(stamped, repl.from, repl.to) | |
| case subIdent: | |
| stamped = substituteIdent(stamped, repl.from, repl.to) | |
| case deleteConst: | |
| stamped = deleteConstDecl(stamped, repl.from) | |
| default: | |
| log.Fatalf("unknown op kind %v", repl.kind) | |
| } | |
| } | |
| stamped = cleanLabels(stamped) | |
| out.Write(mustFormatNode(fset, stamped)) | |
| out.WriteString("\n\n") | |
| } | |
| return out.Bytes() | |
| } | |
| // substituteWithBasicLit recursively renames identifiers in the provided AST | |
| // according to 'from' and 'to'. | |
| func substituteWithBasicLit(node ast.Node, from, to string) ast.Node { | |
| // The op is a substitution of an identifier with a basic literal. | |
| toExpr, err := parser.ParseExpr(to) | |
| if err != nil { | |
| log.Fatalf("parsing expr %q: %v", to, err) | |
| } | |
| toLit, ok := toExpr.(*ast.BasicLit) | |
| if !ok { | |
| log.Fatalf("op 'to' expr %q is not a basic literal", to) | |
| } | |
| return astutil.Apply(node, func(cursor *astutil.Cursor) bool { | |
| if ident, ok := cursor.Node().(*ast.Ident); ok && ident.Name == from { | |
| replacement := *toLit | |
| replacement.ValuePos = ident.NamePos | |
| cursor.Replace(new(replacement)) | |
| } | |
| return true | |
| }, nil) | |
| } | |
| // substituteIdent replaces the ident named 'from' to 'to'. | |
| func substituteIdent(node ast.Node, from, to string) ast.Node { | |
| return astutil.Apply(node, func(cursor *astutil.Cursor) bool { | |
| if ident, ok := cursor.Node().(*ast.Ident); ok && ident.Name == from { | |
| cursor.Replace(&ast.Ident{Name: to, NamePos: ident.NamePos}) | |
| } | |
| return true | |
| }, nil) | |
| } | |
| // foldIfCondition replaces 'from' with 'to', which must be "true" or "false". | |
| // It then applies simplifications to any boolean expressions that have literal | |
| // true or false values, from the bottom up. Any if statements that have a condition | |
| // that is a literal true or false after the simplification will be replaced with | |
| // their bodies (in the true case) or deleted (in the false case). | |
| func foldIfCondition(node ast.Node, from, to string) ast.Node { | |
| boolLit := func(n ast.Expr) (v, ok bool) { | |
| if ident, ok := ast.Unparen(n).(*ast.Ident); ok { | |
| switch ident.Name { | |
| case "true": | |
| return true, true | |
| case "false": | |
| return false, true | |
| } | |
| return false, false | |
| } | |
| return false, false | |
| } | |
| handleIfs := func(cursor *astutil.Cursor) bool { | |
| switch n := cursor.Node().(type) { | |
| case *ast.Ident: | |
| // First, do the replacement. | |
| if n.Name == from { | |
| cursor.Replace(&ast.Ident{Name: to, NamePos: n.NamePos}) | |
| } | |
| case *ast.UnaryExpr: | |
| if n.Op == token.NOT { | |
| if b, ok := boolLit(n.X); ok { | |
| name := "true" | |
| if b { | |
| name = "false" | |
| } | |
| cursor.Replace(&ast.Ident{Name: name, NamePos: n.Pos()}) | |
| } | |
| } | |
| case *ast.BinaryExpr: | |
| xBool, xOk := boolLit(n.X) | |
| yBool, yOk := boolLit(n.Y) | |
| if n.Op == token.LAND { | |
| switch { | |
| case xOk && !xBool || yOk && !yBool: | |
| cursor.Replace(&ast.Ident{Name: "false", NamePos: n.Pos()}) | |
| case xOk && xBool: | |
| cursor.Replace(n.Y) | |
| case yOk && yBool: | |
| cursor.Replace(n.X) | |
| } | |
| } else if n.Op == token.LOR { | |
| switch { | |
| case xOk && xBool || yOk && yBool: | |
| cursor.Replace(&ast.Ident{Name: "true", NamePos: n.Pos()}) | |
| case xOk && !xBool: | |
| cursor.Replace(n.Y) | |
| case yOk && !yBool: | |
| cursor.Replace(n.X) | |
| } | |
| } | |
| case *ast.IfStmt: | |
| if v, ok := boolLit(n.Cond); ok { | |
| if cursor.Index() < 0 { | |
| replacement := ast.Node(&ast.EmptyStmt{}) | |
| if v { | |
| replacement = n.Body | |
| } | |
| cursor.Replace(replacement) | |
| break | |
| } | |
| if v { | |
| for _, stmt := range n.Body.List { | |
| cursor.InsertBefore(stmt) | |
| } | |
| } else if n.Else != nil { | |
| if block, ok := n.Else.(*ast.BlockStmt); ok { | |
| for i := len(block.List) - 1; i >= 0; i-- { | |
| cursor.InsertAfter(block.List[i]) | |
| } | |
| } | |
| } | |
| cursor.Delete() | |
| } | |
| case *ast.LabeledStmt: | |
| // This case isn't necessary but it moves the code | |
| // out of the block so that it looks cleaner. | |
| if inner, ok := n.Stmt.(*ast.BlockStmt); ok { | |
| if len(inner.List) == 0 { | |
| cursor.Delete() | |
| break | |
| } | |
| list := inner.List | |
| n.Stmt = list[0] | |
| for i := len(list) - 1; i > 0; i-- { | |
| cursor.InsertAfter(list[i]) | |
| } | |
| } | |
| } | |
| return true | |
| } | |
| return astutil.Apply(node, nil, handleIfs) | |
| } | |
| func cleanLabels(node ast.Node) ast.Node { | |
| found := map[string]bool{} | |
| ast.Inspect(node, func(node ast.Node) bool { | |
| if branch, ok := node.(*ast.BranchStmt); ok { | |
| if branch.Label != nil { | |
| found[branch.Label.Name] = true | |
| } | |
| } | |
| return true | |
| }) | |
| return astutil.Apply(node, nil, func(cursor *astutil.Cursor) bool { | |
| if lstmt, ok := cursor.Node().(*ast.LabeledStmt); ok { | |
| if !found[lstmt.Label.Name] { | |
| if _, ok := lstmt.Stmt.(*ast.EmptyStmt); ok { | |
| cursor.Delete() | |
| } else { | |
| cursor.Replace(lstmt.Stmt) | |
| } | |
| } | |
| } | |
| return true | |
| }) | |
| } | |
| // reports whether this is a non-grouped constant decl named 'name'. | |
| func isNamedConstDecl(node ast.Node, name string) bool { | |
| declStmt, ok := node.(*ast.DeclStmt) | |
| if !ok { | |
| return false | |
| } | |
| genDecl, ok := declStmt.Decl.(*ast.GenDecl) | |
| if !ok || genDecl.Tok != token.CONST { | |
| return false | |
| } | |
| if len(genDecl.Specs) != 1 { | |
| return false | |
| } | |
| vs, ok := genDecl.Specs[0].(*ast.ValueSpec) | |
| if !ok || len(vs.Names) != 1 || len(vs.Values) != 1 { | |
| return false | |
| } | |
| return vs.Names[0].Name == name | |
| } | |
| // deleteConstDecl removes const declarations whose name matches the given name. | |
| // It only applies to declaration statements with a single declaration. | |
| func deleteConstDecl(node ast.Node, name string) ast.Node { | |
| return astutil.Apply(node, func(cursor *astutil.Cursor) bool { | |
| if isNamedConstDecl(cursor.Node(), name) { | |
| cursor.Delete() | |
| } | |
| return true | |
| }, nil) | |
| } | |
| // inlineFunction recursively replaces calls to the function 'from' with the body of the function | |
| // 'toDecl'. All calls to 'from' must either have no return values and appear in standalone expression statements | |
| // or otherwise must appear in assignment statements. | |
| // The replacement is very simple: it doesn't substitute the arguments for the parameters, so the | |
| // arguments to the function call must be the same identifier as the parameters to the function | |
| // declared by 'toDecl'. If there are any calls to from where that's not the case there will be a fatal error. | |
| func inlineFunction(node ast.Node, from string, toDecl *ast.FuncDecl) ast.Node { | |
| return astutil.Apply(node, func(cursor *astutil.Cursor) bool { | |
| switch node := cursor.Node().(type) { | |
| case *ast.AssignStmt: | |
| // TODO(matloob) CHECK function args have same name | |
| // as parameters (or parameter is "_"). | |
| if len(node.Rhs) == 1 && isCallTo(node.Rhs[0], from) { | |
| args := node.Rhs[0].(*ast.CallExpr).Args | |
| if !argsMatchParameters(args, toDecl.Type.Params) { | |
| log.Fatalf("applying op: arguments to %v don't match parameter names of %v: %v", from, toDecl.Name, debugPrint(args...)) | |
| } | |
| replaceAssignment(cursor, node, toDecl) | |
| } | |
| return false | |
| case *ast.ExprStmt: | |
| if callExpr, ok := node.X.(*ast.CallExpr); ok && isCallTo(callExpr, from) { | |
| if !argsMatchParameters(callExpr.Args, toDecl.Type.Params) { | |
| log.Fatalf("applying op: arguments to %v don't match parameter names of %v: %v", from, toDecl.Name, debugPrint(callExpr.Args...)) | |
| } | |
| if toDecl.Type.Results != nil { | |
| log.Fatalf("applying op: call to %v, which does not appear in an assignment, is replaced with %v which has return values: %v", from, toDecl.Name, debugPrint(callExpr.Args...)) | |
| } | |
| replaceCallExprStmt(cursor, toDecl) | |
| } | |
| return false | |
| case *ast.ReturnStmt: | |
| if len(node.Results) == 1 && isCallTo(node.Results[0], from) { | |
| args := node.Results[0].(*ast.CallExpr).Args | |
| if !argsMatchParameters(args, toDecl.Type.Params) { | |
| log.Fatalf("applying op: arguments to %v don't match parameter names of %v: %v", from, toDecl.Name, debugPrint(args...)) | |
| } | |
| replaceTailCall(cursor, toDecl) | |
| } | |
| return false | |
| case *ast.CallExpr: | |
| if isCallTo(node, from) { | |
| switch cursor.Parent().(type) { | |
| case *ast.AssignStmt, *ast.ExprStmt: | |
| default: | |
| log.Fatalf("applying op: all calls to function %q being replaced must appear in an assignment or expression statement, appears in %T", from, cursor.Parent()) | |
| } | |
| } | |
| } | |
| return true | |
| }, nil) | |
| } | |
| // argsMatchParameters reports whether the arguments given by args are all identifiers | |
| // whose names are the same as the corresponding parameters in params. | |
| func argsMatchParameters(args []ast.Expr, params *ast.FieldList) bool { | |
| var paramIdents []*ast.Ident | |
| for _, f := range params.List { | |
| paramIdents = append(paramIdents, f.Names...) | |
| } | |
| if len(args) != len(paramIdents) { | |
| return false | |
| } | |
| for i := range args { | |
| if !isIdentWithName(args[i], paramIdents[i].Name) { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| // isIdentWithName reports whether the expression is an identifier with the given name. | |
| func isIdentWithName(expr ast.Node, name string) bool { | |
| ident, ok := expr.(*ast.Ident) | |
| if !ok { | |
| return false | |
| } | |
| return ident.Name == name | |
| } | |
| // isCallTo reports whether the expression is a call expression to the function with the given name. | |
| func isCallTo(expr ast.Expr, name string) bool { | |
| callexpr, ok := expr.(*ast.CallExpr) | |
| if !ok { | |
| return false | |
| } | |
| return isIdentWithName(callexpr.Fun, name) | |
| } | |
| // replaceCallExprStmt replaces a standalone expression statement calling a function with no | |
| // return values with the body of the function. | |
| func replaceCallExprStmt(cursor *astutil.Cursor, funcdecl *ast.FuncDecl) { | |
| body := internalastutil.CloneNode(funcdecl.Body) | |
| for _, stmt := range body.List { | |
| cursor.InsertBefore(stmt) | |
| } | |
| cursor.Delete() | |
| } | |
| func replaceTailCall(cursor *astutil.Cursor, funcdecl *ast.FuncDecl) { | |
| if !hasTerminatingReturn(funcdecl.Body) { | |
| log.Fatal("function being inlined must have a return at the end") | |
| } | |
| body := internalastutil.CloneNode(funcdecl.Body) | |
| if len(body.List) < 1 { | |
| log.Fatal("replacing with empty bodied function") | |
| } | |
| // The op happens in two steps: first we insert the body of the function being inlined (except for | |
| // the final return) before the assignment, and then we change the assignment statement to replace the function call | |
| // with the expressions being returned. | |
| // Insert the body up to the final return. | |
| for _, stmt := range body.List { | |
| cursor.InsertBefore(stmt) | |
| } | |
| cursor.Delete() | |
| } | |
| // replaceAssignment replaces an assignment statement where the right hand side is a function call | |
| // whose arguments have the same names as the parameters to funcdecl with the body of funcdecl. | |
| // It sets the left hand side of the assignment to the return values of the function. | |
| func replaceAssignment(cursor *astutil.Cursor, assign *ast.AssignStmt, funcdecl *ast.FuncDecl) { | |
| if !hasTerminatingReturn(funcdecl.Body) { | |
| log.Fatal("function being inlined must have a return at the end") | |
| } | |
| body := internalastutil.CloneNode(funcdecl.Body) | |
| if hasTerminatingAndNonterminatingReturn(funcdecl.Body) { | |
| // The function has multiple return points. Add the code that we'd continue with in the caller | |
| // after each of the return points. The calling function must have a terminating return | |
| // so we don't continue execution in the replaced function after we finish executing the | |
| // continue block that we add. | |
| body = addContinues(cursor, assign, body, everythingFollowingInParent(cursor)).(*ast.BlockStmt) | |
| } | |
| if len(body.List) < 1 { | |
| log.Fatal("replacing with empty bodied function") | |
| } | |
| // The op happens in two steps: first we insert the body of the function being inlined (except for | |
| // the final return) before the assignment, and then we change the assignment statement to replace the function call | |
| // with the expressions being returned. | |
| // Determine the expressions being returned. | |
| beforeReturn, ret := body.List[:len(body.List)-1], body.List[len(body.List)-1] | |
| returnStmt, ok := ret.(*ast.ReturnStmt) | |
| if !ok { | |
| log.Fatal("last stmt in function we're replacing with should be a return") | |
| } | |
| results := returnStmt.Results | |
| // Insert the body up to the final return. | |
| for _, stmt := range beforeReturn { | |
| cursor.InsertBefore(stmt) | |
| } | |
| // Rewrite the assignment statement. | |
| replaceWithAssignment(cursor, assign.Lhs, results, assign.Tok) | |
| } | |
| // hasTerminatingReturn reparts whether the block ends in a return statement. | |
| func hasTerminatingReturn(block *ast.BlockStmt) bool { | |
| _, ok := block.List[len(block.List)-1].(*ast.ReturnStmt) | |
| return ok | |
| } | |
| // hasTerminatingAndNonterminatingReturn reports whether the block ends in a return | |
| // statement, and also has a return elsewhere in it. | |
| func hasTerminatingAndNonterminatingReturn(block *ast.BlockStmt) bool { | |
| if !hasTerminatingReturn(block) { | |
| return false | |
| } | |
| var ret bool | |
| for i := range block.List[:len(block.List)-1] { | |
| ast.Inspect(block.List[i], func(node ast.Node) bool { | |
| _, ok := node.(*ast.ReturnStmt) | |
| if ok { | |
| ret = true | |
| return false | |
| } | |
| return true | |
| }) | |
| } | |
| return ret | |
| } | |
| // everythingFollowingInParent returns a block with everything in the parent block node of the cursor after | |
| // the cursor itself. The cursor must point to an element in a block node's list. | |
| func everythingFollowingInParent(cursor *astutil.Cursor) *ast.BlockStmt { | |
| parent := cursor.Parent() | |
| block, ok := parent.(*ast.BlockStmt) | |
| if !ok { | |
| log.Fatal("internal error: in everythingFollowingInParent, cursor doesn't point to element in block list") | |
| } | |
| blockcopy := internalastutil.CloneNode(block) // get a clean copy | |
| blockcopy.List = blockcopy.List[cursor.Index()+1:] // and remove everything before and including stmt | |
| if _, ok := blockcopy.List[len(blockcopy.List)-1].(*ast.ReturnStmt); !ok { | |
| log.Printf("%s", mustFormatNode(token.NewFileSet(), blockcopy)) | |
| log.Fatal("internal error: parent doesn't end in a return") | |
| } | |
| return blockcopy | |
| } | |
| // in the case that there's a return in the body being inlined (toBlock), addContinues | |
| // replaces those returns that are not at the end of the function with the code in the | |
| // caller after the function call that execution would continue with after the return. | |
| // The block being added must end in a return. | |
| func addContinues(cursor *astutil.Cursor, assignNode *ast.AssignStmt, toBlock *ast.BlockStmt, continueBlock *ast.BlockStmt) ast.Node { | |
| if !hasTerminatingReturn(continueBlock) { | |
| log.Fatal("the block being continued to in addContinues must end in a return") | |
| } | |
| applyFunc := func(cursor *astutil.Cursor) bool { | |
| ret, ok := cursor.Node().(*ast.ReturnStmt) | |
| if !ok { | |
| return true | |
| } | |
| if cursor.Parent() == toBlock && cursor.Index() == len(toBlock.List)-1 { | |
| return false | |
| } | |
| // This is the opposite of replacing a function call with the body. First | |
| // we replace the return statement with the assignment from the caller, and | |
| // then add the code we continue with. | |
| replaceWithAssignment(cursor, assignNode.Lhs, ret.Results, assignNode.Tok) | |
| cursor.InsertAfter(internalastutil.CloneNode(continueBlock)) | |
| return false | |
| } | |
| return astutil.Apply(toBlock, applyFunc, nil) | |
| } | |
| // debugPrint prints out the expressions given by nodes for debugging. | |
| func debugPrint(nodes ...ast.Expr) string { | |
| var b strings.Builder | |
| for i, node := range nodes { | |
| b.Write(mustFormatNode(token.NewFileSet(), node)) | |
| if i != len(nodes)-1 { | |
| b.WriteString(", ") | |
| } | |
| } | |
| return b.String() | |
| } | |
| // mustFormatNode produces the formatted Go code for the given node. | |
| func mustFormatNode(fset *token.FileSet, node any) []byte { | |
| var buf bytes.Buffer | |
| format.Node(&buf, fset, node) | |
| return buf.Bytes() | |
| } | |
| // mustMatchExprs makes sure that the expression lists have the same length, | |
| // and returns the lists of the expressions on the lhs and rhs where the | |
| // identifiers are not the same. These are used to produce assignment statements | |
| // where the expressions on the right are assigned to the identifiers on the left. | |
| func mustMatchExprs(lhs []ast.Expr, rhs []ast.Expr) ([]ast.Expr, []ast.Expr) { | |
| if len(lhs) != len(rhs) { | |
| log.Fatal("exprs don't match", debugPrint(lhs...), debugPrint(rhs...)) | |
| } | |
| var newLhs, newRhs []ast.Expr | |
| for i := range lhs { | |
| lhsIdent, ok1 := lhs[i].(*ast.Ident) | |
| rhsIdent, ok2 := rhs[i].(*ast.Ident) | |
| if ok1 && ok2 && lhsIdent.Name == rhsIdent.Name { | |
| continue | |
| } | |
| newLhs = append(newLhs, lhs[i]) | |
| newRhs = append(newRhs, rhs[i]) | |
| } | |
| return newLhs, newRhs | |
| } | |
| // replaceWithAssignment replaces the node pointed to by the cursor with an assignment of the | |
| // left hand side to the righthand side, removing any redundant assignments of a variable to itself, | |
| // and replacing an assignment to a single basic literal with a constant declaration. | |
| func replaceWithAssignment(cursor *astutil.Cursor, lhs, rhs []ast.Expr, tok token.Token) { | |
| newLhs, newRhs := mustMatchExprs(lhs, rhs) | |
| if len(newLhs) == 0 { | |
| cursor.Delete() | |
| return | |
| } | |
| if len(newRhs) == 1 { | |
| if lit, ok := newRhs[0].(*ast.BasicLit); ok { | |
| constDecl := &ast.DeclStmt{ | |
| Decl: &ast.GenDecl{ | |
| Tok: token.CONST, | |
| Specs: []ast.Spec{ | |
| &ast.ValueSpec{ | |
| Names: []*ast.Ident{newLhs[0].(*ast.Ident)}, | |
| Values: []ast.Expr{lit}, | |
| }, | |
| }, | |
| }, | |
| } | |
| cursor.Replace(constDecl) | |
| return | |
| } | |
| } | |
| newAssignment := &ast.AssignStmt{ | |
| Lhs: newLhs, | |
| Rhs: newRhs, | |
| Tok: tok, | |
| } | |
| cursor.Replace(newAssignment) | |
| } | |
| // generateTable generates the file with the jump tables for the specialized malloc functions. | |
| func generateTable(sizeToSizeClass []uint8) []byte { | |
| scMax := sizeToSizeClass[specializedMallocMax] | |
| var b bytes.Buffer | |
| fmt.Fprintf(&b, `// Code generated by mkmalloc.go; DO NOT EDIT. | |
| //go:build !plan9 | |
| package runtime | |
| import "unsafe" | |
| var mallocScanTable = [%d]func(size uintptr, typ *_type, needzero bool) unsafe.Pointer{`, specializedMallocMax+1) | |
| for i := range uintptr(specializedMallocMax + 1) { | |
| fmt.Fprintf(&b, "%s,\n", smallScanNoHeaderSCFuncName(sizeToSizeClass[i], scMax)) | |
| } | |
| fmt.Fprintf(&b, ` | |
| } | |
| var mallocNoScanTable = [%d]func(size uintptr, typ *_type, needzero bool) unsafe.Pointer{`, specializedMallocMax+1) | |
| for i := range uintptr(specializedMallocMax + 1) { | |
| if i < 16 { | |
| fmt.Fprintf(&b, "%s,\n", "mallocPanic") | |
| } else { | |
| fmt.Fprintf(&b, "%s,\n", smallNoScanSCFuncName(sizeToSizeClass[i], scMax)) | |
| } | |
| } | |
| fmt.Fprintln(&b, ` | |
| }`) | |
| return b.Bytes() | |
| } | |
| // Generate benchmarks for all potentially small sizes | |
| // (sizes for which smallScanNoHeader would be called) | |
| // gc.MinSizeForMallocHeader is defined as goarch.PtrSize * goarch.PtrBits. | |
| const benchmarkMax = maxPtrSize * maxPtrBits | |
| // benchmarkConfig produces an inlining config to stamp out microbenchmarks. | |
| func benchmarkConfig(classes []class, sizeToSizeClass []uint8) generatorConfig { | |
| config := generatorConfig{file: "../malloc_stubs_test.go"} | |
| scMax := sizeToSizeClass[benchmarkMax] | |
| str := fmt.Sprint | |
| for sc := uint8(1); sc <= scMax; sc++ { | |
| elemsize := classes[sc].size | |
| config.specs = append(config.specs, spec{ | |
| templateFunc: "benchmarkStub", | |
| name: fmt.Sprintf("benchmarkMallocgcNoscan%d", elemsize), | |
| ops: []op{ | |
| {subBasicLit, "size_", str(elemsize)}, | |
| {foldCondition, "noscan_", str(true)}, | |
| }, | |
| }) | |
| config.specs = append(config.specs, spec{ | |
| templateFunc: "benchmarkStub", | |
| name: fmt.Sprintf("benchmarkMallocgcScan%d", elemsize), | |
| ops: []op{ | |
| {subBasicLit, "size_", str(elemsize)}, | |
| {foldCondition, "noscan_", str(false)}, | |
| }, | |
| }) | |
| config.specs = append(config.specs, spec{ | |
| templateFunc: "benchmarkScanSliceStub", | |
| name: fmt.Sprintf("benchmarkMallocgcScanSlice%d", elemsize), | |
| ops: []op{{subBasicLit, "size_", str(elemsize)}}, | |
| }) | |
| } | |
| for size := 1; size < tinySize; size++ { | |
| config.specs = append(config.specs, spec{ | |
| templateFunc: "benchmarkStubTiny", | |
| name: fmt.Sprintf("benchmarkMallocgcTiny%d", size), | |
| ops: []op{{subBasicLit, "size_", str(size)}, {foldCondition, "noscan_", str(true)}}, | |
| }) | |
| } | |
| return config | |
| } | |
| func generateTopBenchmark(classes []class, sizeToSizeClass []uint8) string { | |
| scMax := sizeToSizeClass[benchmarkMax] | |
| bench := `func BenchmarkMallocgc(b *testing.B) { | |
| b.Run("scan=noscan", func(b *testing.B) { | |
| ` | |
| for size := 1; size < tinySize; size++ { | |
| bench += fmt.Sprintf(`b.Run("size=%d", benchmarkMallocgcTiny%d)`, size, size) + "\n" | |
| } | |
| for sc := uint8(2); sc <= scMax; sc++ { | |
| elemsize := classes[sc].size | |
| bench += fmt.Sprintf(`b.Run("size=%d", benchmarkMallocgcNoscan%d)`, elemsize, elemsize) + "\n" | |
| } | |
| bench += `}) | |
| b.Run("scan=scan", func(b *testing.B) { | |
| ` | |
| for sc := uint8(1); sc <= scMax; sc++ { | |
| elemsize := classes[sc].size | |
| bench += fmt.Sprintf(`b.Run("size=%d", benchmarkMallocgcScan%d)`, elemsize, elemsize) + "\n" | |
| } | |
| bench += `}) | |
| b.Run("scan=scanslice", func(b *testing.B) { | |
| ` | |
| for sc := uint8(1); sc <= scMax; sc++ { | |
| elemsize := classes[sc].size | |
| bench += fmt.Sprintf(`b.Run("size=%d", benchmarkMallocgcScanSlice%d)`, elemsize, elemsize) + "\n" | |
| } | |
| bench += `}) | |
| }` | |
| return bench | |
| } |