github.com/bir3/gocompiler@v0.3.205/src/cmd/compile/internal/pgo/irgraph.go (about)

     1  // Copyright 2022 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  // WORK IN PROGRESS
     6  
     7  // A note on line numbers: when working with line numbers, we always use the
     8  // binary-visible relative line number. i.e., the line number as adjusted by
     9  // //line directives (ctxt.InnermostPos(ir.Node.Pos()).RelLine()). Use
    10  // NodeLineOffset to compute line offsets.
    11  //
    12  // If you are thinking, "wait, doesn't that just make things more complex than
    13  // using the real line number?", then you are 100% correct. Unfortunately,
    14  // pprof profiles generated by the runtime always contain line numbers as
    15  // adjusted by //line directives (because that is what we put in pclntab). Thus
    16  // for the best behavior when attempting to match the source with the profile
    17  // it makes sense to use the same line number space.
    18  //
    19  // Some of the effects of this to keep in mind:
    20  //
    21  //  - For files without //line directives there is no impact, as RelLine() ==
    22  //    Line().
    23  //  - For functions entirely covered by the same //line directive (i.e., a
    24  //    directive before the function definition and no directives within the
    25  //    function), there should also be no impact, as line offsets within the
    26  //    function should be the same as the real line offsets.
    27  //  - Functions containing //line directives may be impacted. As fake line
    28  //    numbers need not be monotonic, we may compute negative line offsets. We
    29  //    should accept these and attempt to use them for best-effort matching, as
    30  //    these offsets should still match if the source is unchanged, and may
    31  //    continue to match with changed source depending on the impact of the
    32  //    changes on fake line numbers.
    33  //  - Functions containing //line directives may also contain duplicate lines,
    34  //    making it ambiguous which call the profile is referencing. This is a
    35  //    similar problem to multiple calls on a single real line, as we don't
    36  //    currently track column numbers.
    37  //
    38  // Long term it would be best to extend pprof profiles to include real line
    39  // numbers. Until then, we have to live with these complexities. Luckily,
    40  // //line directives that change line numbers in strange ways should be rare,
    41  // and failing PGO matching on these files is not too big of a loss.
    42  
    43  package pgo
    44  
    45  import (
    46  	"github.com/bir3/gocompiler/src/cmd/compile/internal/base"
    47  	"github.com/bir3/gocompiler/src/cmd/compile/internal/ir"
    48  	"github.com/bir3/gocompiler/src/cmd/compile/internal/typecheck"
    49  	"github.com/bir3/gocompiler/src/cmd/compile/internal/types"
    50  	"fmt"
    51  	"github.com/bir3/gocompiler/src/internal/profile"
    52  	"log"
    53  	"os"
    54  )
    55  
    56  // IRGraph is the key datastrcture that is built from profile. It is
    57  // essentially a call graph with nodes pointing to IRs of functions and edges
    58  // carrying weights and callsite information. The graph is bidirectional that
    59  // helps in removing nodes efficiently.
    60  type IRGraph struct {
    61  	// Nodes of the graph
    62  	IRNodes  map[string]*IRNode
    63  	OutEdges IREdgeMap
    64  	InEdges  IREdgeMap
    65  }
    66  
    67  // IRNode represents a node in the IRGraph.
    68  type IRNode struct {
    69  	// Pointer to the IR of the Function represented by this node.
    70  	AST *ir.Func
    71  	// Flat weight of the IRNode, obtained from profile.
    72  	Flat int64
    73  	// Cumulative weight of the IRNode.
    74  	Cum int64
    75  }
    76  
    77  // IREdgeMap maps an IRNode to its successors.
    78  type IREdgeMap map[*IRNode][]*IREdge
    79  
    80  // IREdge represents a call edge in the IRGraph with source, destination,
    81  // weight, callsite, and line number information.
    82  type IREdge struct {
    83  	// Source and destination of the edge in IRNode.
    84  	Src, Dst       *IRNode
    85  	Weight         int64
    86  	CallSiteOffset int // Line offset from function start line.
    87  }
    88  
    89  // NodeMapKey represents a hash key to identify unique call-edges in profile
    90  // and in IR. Used for deduplication of call edges found in profile.
    91  type NodeMapKey struct {
    92  	CallerName     string
    93  	CalleeName     string
    94  	CallSiteOffset int // Line offset from function start line.
    95  }
    96  
    97  // Weights capture both node weight and edge weight.
    98  type Weights struct {
    99  	NFlat   int64
   100  	NCum    int64
   101  	EWeight int64
   102  }
   103  
   104  // CallSiteInfo captures call-site information and its caller/callee.
   105  type CallSiteInfo struct {
   106  	LineOffset int // Line offset from function start line.
   107  	Caller     *ir.Func
   108  	Callee     *ir.Func
   109  }
   110  
   111  // Profile contains the processed PGO profile and weighted call graph used for
   112  // PGO optimizations.
   113  type Profile struct {
   114  	// Aggregated NodeWeights and EdgeWeights across the profile. This
   115  	// helps us determine the percentage threshold for hot/cold
   116  	// partitioning.
   117  	TotalNodeWeight int64
   118  	TotalEdgeWeight int64
   119  
   120  	// NodeMap contains all unique call-edges in the profile and their
   121  	// aggregated weight.
   122  	NodeMap map[NodeMapKey]*Weights
   123  
   124  	// WeightedCG represents the IRGraph built from profile, which we will
   125  	// update as part of inlining.
   126  	WeightedCG *IRGraph
   127  }
   128  
   129  // New generates a profile-graph from the profile.
   130  func New(profileFile string) *Profile {
   131  	f, err := os.Open(profileFile)
   132  	if err != nil {
   133  		log.Fatal("failed to open file " + profileFile)
   134  		return nil
   135  	}
   136  	defer f.Close()
   137  	profile, err := profile.Parse(f)
   138  	if err != nil {
   139  		log.Fatal("failed to Parse profile file.")
   140  		return nil
   141  	}
   142  
   143  	if len(profile.Sample) == 0 {
   144  		// We accept empty profiles, but there is nothing to do.
   145  		return nil
   146  	}
   147  
   148  	valueIndex := -1
   149  	for i, s := range profile.SampleType {
   150  		// Samples count is the raw data collected, and CPU nanoseconds is just
   151  		// a scaled version of it, so either one we can find is fine.
   152  		if (s.Type == "samples" && s.Unit == "count") ||
   153  			(s.Type == "cpu" && s.Unit == "nanoseconds") {
   154  			valueIndex = i
   155  			break
   156  		}
   157  	}
   158  
   159  	if valueIndex == -1 {
   160  		log.Fatal("failed to find CPU samples count or CPU nanoseconds value-types in profile.")
   161  		return nil
   162  	}
   163  
   164  	g := newGraph(profile, &Options{
   165  		CallTree:    false,
   166  		SampleValue: func(v []int64) int64 { return v[valueIndex] },
   167  	})
   168  
   169  	p := &Profile{
   170  		NodeMap: make(map[NodeMapKey]*Weights),
   171  		WeightedCG: &IRGraph{
   172  			IRNodes: make(map[string]*IRNode),
   173  		},
   174  	}
   175  
   176  	// Build the node map and totals from the profile graph.
   177  	if !p.processprofileGraph(g) {
   178  		return nil
   179  	}
   180  
   181  	// Create package-level call graph with weights from profile and IR.
   182  	p.initializeIRGraph()
   183  
   184  	return p
   185  }
   186  
   187  // processprofileGraph builds various maps from the profile-graph.
   188  //
   189  // It initializes NodeMap and Total{Node,Edge}Weight based on the name and
   190  // callsite to compute node and edge weights which will be used later on to
   191  // create edges for WeightedCG.
   192  // Returns whether it successfully processed the profile.
   193  func (p *Profile) processprofileGraph(g *Graph) bool {
   194  	nFlat := make(map[string]int64)
   195  	nCum := make(map[string]int64)
   196  	seenStartLine := false
   197  
   198  	// Accummulate weights for the same node.
   199  	for _, n := range g.Nodes {
   200  		canonicalName := n.Info.Name
   201  		nFlat[canonicalName] += n.FlatValue()
   202  		nCum[canonicalName] += n.CumValue()
   203  	}
   204  
   205  	// Process graph and build various node and edge maps which will
   206  	// be consumed by AST walk.
   207  	for _, n := range g.Nodes {
   208  		seenStartLine = seenStartLine || n.Info.StartLine != 0
   209  
   210  		p.TotalNodeWeight += n.FlatValue()
   211  		canonicalName := n.Info.Name
   212  		// Create the key to the nodeMapKey.
   213  		nodeinfo := NodeMapKey{
   214  			CallerName:     canonicalName,
   215  			CallSiteOffset: n.Info.Lineno - n.Info.StartLine,
   216  		}
   217  
   218  		for _, e := range n.Out {
   219  			p.TotalEdgeWeight += e.WeightValue()
   220  			nodeinfo.CalleeName = e.Dest.Info.Name
   221  			if w, ok := p.NodeMap[nodeinfo]; ok {
   222  				w.EWeight += e.WeightValue()
   223  			} else {
   224  				weights := new(Weights)
   225  				weights.NFlat = nFlat[canonicalName]
   226  				weights.NCum = nCum[canonicalName]
   227  				weights.EWeight = e.WeightValue()
   228  				p.NodeMap[nodeinfo] = weights
   229  			}
   230  		}
   231  	}
   232  
   233  	if p.TotalNodeWeight == 0 || p.TotalEdgeWeight == 0 {
   234  		return false // accept but ignore profile with no sample
   235  	}
   236  
   237  	if !seenStartLine {
   238  		// TODO(prattic): If Function.start_line is missing we could
   239  		// fall back to using absolute line numbers, which is better
   240  		// than nothing.
   241  		log.Fatal("PGO profile missing Function.start_line data (Go version of profiled application too old? Go 1.20+ automatically adds this to profiles)")
   242  	}
   243  
   244  	return true
   245  }
   246  
   247  // initializeIRGraph builds the IRGraph by visting all the ir.Func in decl list
   248  // of a package.
   249  func (p *Profile) initializeIRGraph() {
   250  	// Bottomup walk over the function to create IRGraph.
   251  	ir.VisitFuncsBottomUp(typecheck.Target.Decls, func(list []*ir.Func, recursive bool) {
   252  		for _, n := range list {
   253  			p.VisitIR(n, recursive)
   254  		}
   255  	})
   256  }
   257  
   258  // VisitIR traverses the body of each ir.Func and use NodeMap to determine if
   259  // we need to add an edge from ir.Func and any node in the ir.Func body.
   260  func (p *Profile) VisitIR(fn *ir.Func, recursive bool) {
   261  	g := p.WeightedCG
   262  
   263  	if g.IRNodes == nil {
   264  		g.IRNodes = make(map[string]*IRNode)
   265  	}
   266  	if g.OutEdges == nil {
   267  		g.OutEdges = make(map[*IRNode][]*IREdge)
   268  	}
   269  	if g.InEdges == nil {
   270  		g.InEdges = make(map[*IRNode][]*IREdge)
   271  	}
   272  	name := ir.PkgFuncName(fn)
   273  	node := new(IRNode)
   274  	node.AST = fn
   275  	if g.IRNodes[name] == nil {
   276  		g.IRNodes[name] = node
   277  	}
   278  	// Create the key for the NodeMapKey.
   279  	nodeinfo := NodeMapKey{
   280  		CallerName:     name,
   281  		CalleeName:     "",
   282  		CallSiteOffset: 0,
   283  	}
   284  	// If the node exists, then update its node weight.
   285  	if weights, ok := p.NodeMap[nodeinfo]; ok {
   286  		g.IRNodes[name].Flat = weights.NFlat
   287  		g.IRNodes[name].Cum = weights.NCum
   288  	}
   289  
   290  	// Recursively walk over the body of the function to create IRGraph edges.
   291  	p.createIRGraphEdge(fn, g.IRNodes[name], name)
   292  }
   293  
   294  // NodeLineOffset returns the line offset of n in fn.
   295  func NodeLineOffset(n ir.Node, fn *ir.Func) int {
   296  	// See "A note on line numbers" at the top of the file.
   297  	line := int(base.Ctxt.InnermostPos(n.Pos()).RelLine())
   298  	startLine := int(base.Ctxt.InnermostPos(fn.Pos()).RelLine())
   299  	return line - startLine
   300  }
   301  
   302  // addIREdge adds an edge between caller and new node that points to `callee`
   303  // based on the profile-graph and NodeMap.
   304  func (p *Profile) addIREdge(caller *IRNode, callername string, call ir.Node, callee *ir.Func) {
   305  	g := p.WeightedCG
   306  
   307  	// Create an IRNode for the callee.
   308  	calleenode := new(IRNode)
   309  	calleenode.AST = callee
   310  	calleename := ir.PkgFuncName(callee)
   311  
   312  	// Create key for NodeMapKey.
   313  	nodeinfo := NodeMapKey{
   314  		CallerName:     callername,
   315  		CalleeName:     calleename,
   316  		CallSiteOffset: NodeLineOffset(call, caller.AST),
   317  	}
   318  
   319  	// Create the callee node with node weight.
   320  	if g.IRNodes[calleename] == nil {
   321  		g.IRNodes[calleename] = calleenode
   322  		nodeinfo2 := NodeMapKey{
   323  			CallerName:     calleename,
   324  			CalleeName:     "",
   325  			CallSiteOffset: 0,
   326  		}
   327  		if weights, ok := p.NodeMap[nodeinfo2]; ok {
   328  			g.IRNodes[calleename].Flat = weights.NFlat
   329  			g.IRNodes[calleename].Cum = weights.NCum
   330  		}
   331  	}
   332  
   333  	if weights, ok := p.NodeMap[nodeinfo]; ok {
   334  		caller.Flat = weights.NFlat
   335  		caller.Cum = weights.NCum
   336  
   337  		// Add edge in the IRGraph from caller to callee.
   338  		info := &IREdge{Src: caller, Dst: g.IRNodes[calleename], Weight: weights.EWeight, CallSiteOffset: nodeinfo.CallSiteOffset}
   339  		g.OutEdges[caller] = append(g.OutEdges[caller], info)
   340  		g.InEdges[g.IRNodes[calleename]] = append(g.InEdges[g.IRNodes[calleename]], info)
   341  	} else {
   342  		nodeinfo.CalleeName = ""
   343  		nodeinfo.CallSiteOffset = 0
   344  		if weights, ok := p.NodeMap[nodeinfo]; ok {
   345  			caller.Flat = weights.NFlat
   346  			caller.Cum = weights.NCum
   347  			info := &IREdge{Src: caller, Dst: g.IRNodes[calleename], Weight: 0, CallSiteOffset: nodeinfo.CallSiteOffset}
   348  			g.OutEdges[caller] = append(g.OutEdges[caller], info)
   349  			g.InEdges[g.IRNodes[calleename]] = append(g.InEdges[g.IRNodes[calleename]], info)
   350  		} else {
   351  			info := &IREdge{Src: caller, Dst: g.IRNodes[calleename], Weight: 0, CallSiteOffset: nodeinfo.CallSiteOffset}
   352  			g.OutEdges[caller] = append(g.OutEdges[caller], info)
   353  			g.InEdges[g.IRNodes[calleename]] = append(g.InEdges[g.IRNodes[calleename]], info)
   354  		}
   355  	}
   356  }
   357  
   358  // createIRGraphEdge traverses the nodes in the body of ir.Func and add edges between callernode which points to the ir.Func and the nodes in the body.
   359  func (p *Profile) createIRGraphEdge(fn *ir.Func, callernode *IRNode, name string) {
   360  	var doNode func(ir.Node) bool
   361  	doNode = func(n ir.Node) bool {
   362  		switch n.Op() {
   363  		default:
   364  			ir.DoChildren(n, doNode)
   365  		case ir.OCALLFUNC:
   366  			call := n.(*ir.CallExpr)
   367  			// Find the callee function from the call site and add the edge.
   368  			callee := inlCallee(call.X)
   369  			if callee != nil {
   370  				p.addIREdge(callernode, name, n, callee)
   371  			}
   372  		case ir.OCALLMETH:
   373  			call := n.(*ir.CallExpr)
   374  			// Find the callee method from the call site and add the edge.
   375  			callee := ir.MethodExprName(call.X).Func
   376  			p.addIREdge(callernode, name, n, callee)
   377  		}
   378  		return false
   379  	}
   380  	doNode(fn)
   381  }
   382  
   383  // WeightInPercentage converts profile weights to a percentage.
   384  func WeightInPercentage(value int64, total int64) float64 {
   385  	return (float64(value) / float64(total)) * 100
   386  }
   387  
   388  // PrintWeightedCallGraphDOT prints IRGraph in DOT format.
   389  func (p *Profile) PrintWeightedCallGraphDOT(edgeThreshold float64) {
   390  	fmt.Printf("\ndigraph G {\n")
   391  	fmt.Printf("forcelabels=true;\n")
   392  
   393  	// List of functions in this package.
   394  	funcs := make(map[string]struct{})
   395  	ir.VisitFuncsBottomUp(typecheck.Target.Decls, func(list []*ir.Func, recursive bool) {
   396  		for _, f := range list {
   397  			name := ir.PkgFuncName(f)
   398  			funcs[name] = struct{}{}
   399  		}
   400  	})
   401  
   402  	// Determine nodes of DOT.
   403  	nodes := make(map[string]*ir.Func)
   404  	for name, _ := range funcs {
   405  		if n, ok := p.WeightedCG.IRNodes[name]; ok {
   406  			for _, e := range p.WeightedCG.OutEdges[n] {
   407  				if _, ok := nodes[ir.PkgFuncName(e.Src.AST)]; !ok {
   408  					nodes[ir.PkgFuncName(e.Src.AST)] = e.Src.AST
   409  				}
   410  				if _, ok := nodes[ir.PkgFuncName(e.Dst.AST)]; !ok {
   411  					nodes[ir.PkgFuncName(e.Dst.AST)] = e.Dst.AST
   412  				}
   413  			}
   414  			if _, ok := nodes[ir.PkgFuncName(n.AST)]; !ok {
   415  				nodes[ir.PkgFuncName(n.AST)] = n.AST
   416  			}
   417  		}
   418  	}
   419  
   420  	// Print nodes.
   421  	for name, ast := range nodes {
   422  		if n, ok := p.WeightedCG.IRNodes[name]; ok {
   423  			nodeweight := WeightInPercentage(n.Flat, p.TotalNodeWeight)
   424  			color := "black"
   425  			if ast.Inl != nil {
   426  				fmt.Printf("\"%v\" [color=%v,label=\"%v,freq=%.2f,inl_cost=%d\"];\n", ir.PkgFuncName(ast), color, ir.PkgFuncName(ast), nodeweight, ast.Inl.Cost)
   427  			} else {
   428  				fmt.Printf("\"%v\" [color=%v, label=\"%v,freq=%.2f\"];\n", ir.PkgFuncName(ast), color, ir.PkgFuncName(ast), nodeweight)
   429  			}
   430  		}
   431  	}
   432  	// Print edges.
   433  	ir.VisitFuncsBottomUp(typecheck.Target.Decls, func(list []*ir.Func, recursive bool) {
   434  		for _, f := range list {
   435  			name := ir.PkgFuncName(f)
   436  			if n, ok := p.WeightedCG.IRNodes[name]; ok {
   437  				for _, e := range p.WeightedCG.OutEdges[n] {
   438  					edgepercent := WeightInPercentage(e.Weight, p.TotalEdgeWeight)
   439  					if edgepercent > edgeThreshold {
   440  						fmt.Printf("edge [color=red, style=solid];\n")
   441  					} else {
   442  						fmt.Printf("edge [color=black, style=solid];\n")
   443  					}
   444  
   445  					fmt.Printf("\"%v\" -> \"%v\" [label=\"%.2f\"];\n", ir.PkgFuncName(n.AST), ir.PkgFuncName(e.Dst.AST), edgepercent)
   446  				}
   447  			}
   448  		}
   449  	})
   450  	fmt.Printf("}\n")
   451  }
   452  
   453  // RedirectEdges deletes and redirects out-edges from node cur based on
   454  // inlining information via inlinedCallSites.
   455  //
   456  // CallSiteInfo.Callee must be nil.
   457  func (p *Profile) RedirectEdges(cur *IRNode, inlinedCallSites map[CallSiteInfo]struct{}) {
   458  	g := p.WeightedCG
   459  
   460  	for i, outEdge := range g.OutEdges[cur] {
   461  		if _, found := inlinedCallSites[CallSiteInfo{LineOffset: outEdge.CallSiteOffset, Caller: cur.AST}]; !found {
   462  			for _, InEdge := range g.InEdges[cur] {
   463  				if _, ok := inlinedCallSites[CallSiteInfo{LineOffset: InEdge.CallSiteOffset, Caller: InEdge.Src.AST}]; ok {
   464  					weight := g.calculateWeight(InEdge.Src, cur)
   465  					g.redirectEdge(InEdge.Src, cur, outEdge, weight, i)
   466  				}
   467  			}
   468  		} else {
   469  			g.remove(cur, i)
   470  		}
   471  	}
   472  }
   473  
   474  // redirectEdges deletes the cur node out-edges and redirect them so now these
   475  // edges are the parent node out-edges.
   476  func (g *IRGraph) redirectEdges(parent *IRNode, cur *IRNode) {
   477  	for _, outEdge := range g.OutEdges[cur] {
   478  		outEdge.Src = parent
   479  		g.OutEdges[parent] = append(g.OutEdges[parent], outEdge)
   480  	}
   481  	delete(g.OutEdges, cur)
   482  }
   483  
   484  // redirectEdge deletes the cur-node's out-edges and redirect them so now these
   485  // edges are the parent node out-edges.
   486  func (g *IRGraph) redirectEdge(parent *IRNode, cur *IRNode, outEdge *IREdge, weight int64, idx int) {
   487  	outEdge.Src = parent
   488  	outEdge.Weight = weight * outEdge.Weight
   489  	g.OutEdges[parent] = append(g.OutEdges[parent], outEdge)
   490  	g.remove(cur, idx)
   491  }
   492  
   493  // remove deletes the cur-node's out-edges at index idx.
   494  func (g *IRGraph) remove(cur *IRNode, i int) {
   495  	if len(g.OutEdges[cur]) >= 2 {
   496  		g.OutEdges[cur][i] = g.OutEdges[cur][len(g.OutEdges[cur])-1]
   497  		g.OutEdges[cur] = g.OutEdges[cur][:len(g.OutEdges[cur])-1]
   498  	} else {
   499  		delete(g.OutEdges, cur)
   500  	}
   501  }
   502  
   503  // calculateWeight calculates the weight of the new redirected edge.
   504  func (g *IRGraph) calculateWeight(parent *IRNode, cur *IRNode) int64 {
   505  	sum := int64(0)
   506  	pw := int64(0)
   507  	for _, InEdge := range g.InEdges[cur] {
   508  		sum = sum + InEdge.Weight
   509  		if InEdge.Src == parent {
   510  			pw = InEdge.Weight
   511  		}
   512  	}
   513  	weight := int64(0)
   514  	if sum != 0 {
   515  		weight = pw / sum
   516  	} else {
   517  		weight = pw
   518  	}
   519  	return weight
   520  }
   521  
   522  // inlCallee is same as the implementation for inl.go with one change. The change is that we do not invoke CanInline on a closure.
   523  func inlCallee(fn ir.Node) *ir.Func {
   524  	fn = ir.StaticValue(fn)
   525  	switch fn.Op() {
   526  	case ir.OMETHEXPR:
   527  		fn := fn.(*ir.SelectorExpr)
   528  		n := ir.MethodExprName(fn)
   529  		// Check that receiver type matches fn.X.
   530  		// TODO(mdempsky): Handle implicit dereference
   531  		// of pointer receiver argument?
   532  		if n == nil || !types.Identical(n.Type().Recv().Type, fn.X.Type()) {
   533  			return nil
   534  		}
   535  		return n.Func
   536  	case ir.ONAME:
   537  		fn := fn.(*ir.Name)
   538  		if fn.Class == ir.PFUNC {
   539  			return fn.Func
   540  		}
   541  	case ir.OCLOSURE:
   542  		fn := fn.(*ir.ClosureExpr)
   543  		c := fn.Func
   544  		return c
   545  	}
   546  	return nil
   547  }