github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/go/analysis/passes/fieldalignment/fieldalignment.go (about)

     1  // Copyright 2020 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 fieldalignment defines an Analyzer that detects structs that would use less
     6  // memory if their fields were sorted.
     7  package fieldalignment
     8  
     9  import (
    10  	"bytes"
    11  	"fmt"
    12  	"go/ast"
    13  	"go/format"
    14  	"go/token"
    15  	"go/types"
    16  	"sort"
    17  
    18  	"github.com/powerman/golang-tools/go/analysis"
    19  	"github.com/powerman/golang-tools/go/analysis/passes/inspect"
    20  	"github.com/powerman/golang-tools/go/ast/inspector"
    21  )
    22  
    23  const Doc = `find structs that would use less memory if their fields were sorted
    24  
    25  This analyzer find structs that can be rearranged to use less memory, and provides
    26  a suggested edit with the optimal order.
    27  
    28  Note that there are two different diagnostics reported. One checks struct size,
    29  and the other reports "pointer bytes" used. Pointer bytes is how many bytes of the
    30  object that the garbage collector has to potentially scan for pointers, for example:
    31  
    32  	struct { uint32; string }
    33  
    34  have 16 pointer bytes because the garbage collector has to scan up through the string's
    35  inner pointer.
    36  
    37  	struct { string; *uint32 }
    38  
    39  has 24 pointer bytes because it has to scan further through the *uint32.
    40  
    41  	struct { string; uint32 }
    42  
    43  has 8 because it can stop immediately after the string pointer.
    44  `
    45  
    46  var Analyzer = &analysis.Analyzer{
    47  	Name:     "fieldalignment",
    48  	Doc:      Doc,
    49  	Requires: []*analysis.Analyzer{inspect.Analyzer},
    50  	Run:      run,
    51  }
    52  
    53  func run(pass *analysis.Pass) (interface{}, error) {
    54  	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
    55  	nodeFilter := []ast.Node{
    56  		(*ast.StructType)(nil),
    57  	}
    58  	inspect.Preorder(nodeFilter, func(node ast.Node) {
    59  		var s *ast.StructType
    60  		var ok bool
    61  		if s, ok = node.(*ast.StructType); !ok {
    62  			return
    63  		}
    64  		if tv, ok := pass.TypesInfo.Types[s]; ok {
    65  			fieldalignment(pass, s, tv.Type.(*types.Struct))
    66  		}
    67  	})
    68  	return nil, nil
    69  }
    70  
    71  var unsafePointerTyp = types.Unsafe.Scope().Lookup("Pointer").(*types.TypeName).Type()
    72  
    73  func fieldalignment(pass *analysis.Pass, node *ast.StructType, typ *types.Struct) {
    74  	wordSize := pass.TypesSizes.Sizeof(unsafePointerTyp)
    75  	maxAlign := pass.TypesSizes.Alignof(unsafePointerTyp)
    76  
    77  	s := gcSizes{wordSize, maxAlign}
    78  	optimal, indexes := optimalOrder(typ, &s)
    79  	optsz, optptrs := s.Sizeof(optimal), s.ptrdata(optimal)
    80  
    81  	var message string
    82  	if sz := s.Sizeof(typ); sz != optsz {
    83  		message = fmt.Sprintf("struct of size %d could be %d", sz, optsz)
    84  	} else if ptrs := s.ptrdata(typ); ptrs != optptrs {
    85  		message = fmt.Sprintf("struct with %d pointer bytes could be %d", ptrs, optptrs)
    86  	} else {
    87  		// Already optimal order.
    88  		return
    89  	}
    90  
    91  	// Flatten the ast node since it could have multiple field names per list item while
    92  	// *types.Struct only have one item per field.
    93  	// TODO: Preserve multi-named fields instead of flattening.
    94  	var flat []*ast.Field
    95  	for _, f := range node.Fields.List {
    96  		// TODO: Preserve comment, for now get rid of them.
    97  		//       See https://github.com/golang/go/issues/20744
    98  		f.Comment = nil
    99  		f.Doc = nil
   100  		if len(f.Names) <= 1 {
   101  			flat = append(flat, f)
   102  			continue
   103  		}
   104  		for _, name := range f.Names {
   105  			flat = append(flat, &ast.Field{
   106  				Names: []*ast.Ident{name},
   107  				Type:  f.Type,
   108  			})
   109  		}
   110  	}
   111  
   112  	// Sort fields according to the optimal order.
   113  	var reordered []*ast.Field
   114  	for _, index := range indexes {
   115  		reordered = append(reordered, flat[index])
   116  	}
   117  
   118  	newStr := &ast.StructType{
   119  		Fields: &ast.FieldList{
   120  			List: reordered,
   121  		},
   122  	}
   123  
   124  	// Write the newly aligned struct node to get the content for suggested fixes.
   125  	var buf bytes.Buffer
   126  	if err := format.Node(&buf, token.NewFileSet(), newStr); err != nil {
   127  		return
   128  	}
   129  
   130  	pass.Report(analysis.Diagnostic{
   131  		Pos:     node.Pos(),
   132  		End:     node.Pos() + token.Pos(len("struct")),
   133  		Message: message,
   134  		SuggestedFixes: []analysis.SuggestedFix{{
   135  			Message: "Rearrange fields",
   136  			TextEdits: []analysis.TextEdit{{
   137  				Pos:     node.Pos(),
   138  				End:     node.End(),
   139  				NewText: buf.Bytes(),
   140  			}},
   141  		}},
   142  	})
   143  }
   144  
   145  func optimalOrder(str *types.Struct, sizes *gcSizes) (*types.Struct, []int) {
   146  	nf := str.NumFields()
   147  
   148  	type elem struct {
   149  		index   int
   150  		alignof int64
   151  		sizeof  int64
   152  		ptrdata int64
   153  	}
   154  
   155  	elems := make([]elem, nf)
   156  	for i := 0; i < nf; i++ {
   157  		field := str.Field(i)
   158  		ft := field.Type()
   159  		elems[i] = elem{
   160  			i,
   161  			sizes.Alignof(ft),
   162  			sizes.Sizeof(ft),
   163  			sizes.ptrdata(ft),
   164  		}
   165  	}
   166  
   167  	sort.Slice(elems, func(i, j int) bool {
   168  		ei := &elems[i]
   169  		ej := &elems[j]
   170  
   171  		// Place zero sized objects before non-zero sized objects.
   172  		zeroi := ei.sizeof == 0
   173  		zeroj := ej.sizeof == 0
   174  		if zeroi != zeroj {
   175  			return zeroi
   176  		}
   177  
   178  		// Next, place more tightly aligned objects before less tightly aligned objects.
   179  		if ei.alignof != ej.alignof {
   180  			return ei.alignof > ej.alignof
   181  		}
   182  
   183  		// Place pointerful objects before pointer-free objects.
   184  		noptrsi := ei.ptrdata == 0
   185  		noptrsj := ej.ptrdata == 0
   186  		if noptrsi != noptrsj {
   187  			return noptrsj
   188  		}
   189  
   190  		if !noptrsi {
   191  			// If both have pointers...
   192  
   193  			// ... then place objects with less trailing
   194  			// non-pointer bytes earlier. That is, place
   195  			// the field with the most trailing
   196  			// non-pointer bytes at the end of the
   197  			// pointerful section.
   198  			traili := ei.sizeof - ei.ptrdata
   199  			trailj := ej.sizeof - ej.ptrdata
   200  			if traili != trailj {
   201  				return traili < trailj
   202  			}
   203  		}
   204  
   205  		// Lastly, order by size.
   206  		if ei.sizeof != ej.sizeof {
   207  			return ei.sizeof > ej.sizeof
   208  		}
   209  
   210  		return false
   211  	})
   212  
   213  	fields := make([]*types.Var, nf)
   214  	indexes := make([]int, nf)
   215  	for i, e := range elems {
   216  		fields[i] = str.Field(e.index)
   217  		indexes[i] = e.index
   218  	}
   219  	return types.NewStruct(fields, nil), indexes
   220  }
   221  
   222  // Code below based on go/types.StdSizes.
   223  
   224  type gcSizes struct {
   225  	WordSize int64
   226  	MaxAlign int64
   227  }
   228  
   229  func (s *gcSizes) Alignof(T types.Type) int64 {
   230  	// For arrays and structs, alignment is defined in terms
   231  	// of alignment of the elements and fields, respectively.
   232  	switch t := T.Underlying().(type) {
   233  	case *types.Array:
   234  		// spec: "For a variable x of array type: unsafe.Alignof(x)
   235  		// is the same as unsafe.Alignof(x[0]), but at least 1."
   236  		return s.Alignof(t.Elem())
   237  	case *types.Struct:
   238  		// spec: "For a variable x of struct type: unsafe.Alignof(x)
   239  		// is the largest of the values unsafe.Alignof(x.f) for each
   240  		// field f of x, but at least 1."
   241  		max := int64(1)
   242  		for i, nf := 0, t.NumFields(); i < nf; i++ {
   243  			if a := s.Alignof(t.Field(i).Type()); a > max {
   244  				max = a
   245  			}
   246  		}
   247  		return max
   248  	}
   249  	a := s.Sizeof(T) // may be 0
   250  	// spec: "For a variable x of any type: unsafe.Alignof(x) is at least 1."
   251  	if a < 1 {
   252  		return 1
   253  	}
   254  	if a > s.MaxAlign {
   255  		return s.MaxAlign
   256  	}
   257  	return a
   258  }
   259  
   260  var basicSizes = [...]byte{
   261  	types.Bool:       1,
   262  	types.Int8:       1,
   263  	types.Int16:      2,
   264  	types.Int32:      4,
   265  	types.Int64:      8,
   266  	types.Uint8:      1,
   267  	types.Uint16:     2,
   268  	types.Uint32:     4,
   269  	types.Uint64:     8,
   270  	types.Float32:    4,
   271  	types.Float64:    8,
   272  	types.Complex64:  8,
   273  	types.Complex128: 16,
   274  }
   275  
   276  func (s *gcSizes) Sizeof(T types.Type) int64 {
   277  	switch t := T.Underlying().(type) {
   278  	case *types.Basic:
   279  		k := t.Kind()
   280  		if int(k) < len(basicSizes) {
   281  			if s := basicSizes[k]; s > 0 {
   282  				return int64(s)
   283  			}
   284  		}
   285  		if k == types.String {
   286  			return s.WordSize * 2
   287  		}
   288  	case *types.Array:
   289  		return t.Len() * s.Sizeof(t.Elem())
   290  	case *types.Slice:
   291  		return s.WordSize * 3
   292  	case *types.Struct:
   293  		nf := t.NumFields()
   294  		if nf == 0 {
   295  			return 0
   296  		}
   297  
   298  		var o int64
   299  		max := int64(1)
   300  		for i := 0; i < nf; i++ {
   301  			ft := t.Field(i).Type()
   302  			a, sz := s.Alignof(ft), s.Sizeof(ft)
   303  			if a > max {
   304  				max = a
   305  			}
   306  			if i == nf-1 && sz == 0 && o != 0 {
   307  				sz = 1
   308  			}
   309  			o = align(o, a) + sz
   310  		}
   311  		return align(o, max)
   312  	case *types.Interface:
   313  		return s.WordSize * 2
   314  	}
   315  	return s.WordSize // catch-all
   316  }
   317  
   318  // align returns the smallest y >= x such that y % a == 0.
   319  func align(x, a int64) int64 {
   320  	y := x + a - 1
   321  	return y - y%a
   322  }
   323  
   324  func (s *gcSizes) ptrdata(T types.Type) int64 {
   325  	switch t := T.Underlying().(type) {
   326  	case *types.Basic:
   327  		switch t.Kind() {
   328  		case types.String, types.UnsafePointer:
   329  			return s.WordSize
   330  		}
   331  		return 0
   332  	case *types.Chan, *types.Map, *types.Pointer, *types.Signature, *types.Slice:
   333  		return s.WordSize
   334  	case *types.Interface:
   335  		return 2 * s.WordSize
   336  	case *types.Array:
   337  		n := t.Len()
   338  		if n == 0 {
   339  			return 0
   340  		}
   341  		a := s.ptrdata(t.Elem())
   342  		if a == 0 {
   343  			return 0
   344  		}
   345  		z := s.Sizeof(t.Elem())
   346  		return (n-1)*z + a
   347  	case *types.Struct:
   348  		nf := t.NumFields()
   349  		if nf == 0 {
   350  			return 0
   351  		}
   352  
   353  		var o, p int64
   354  		for i := 0; i < nf; i++ {
   355  			ft := t.Field(i).Type()
   356  			a, sz := s.Alignof(ft), s.Sizeof(ft)
   357  			fp := s.ptrdata(ft)
   358  			o = align(o, a)
   359  			if fp != 0 {
   360  				p = o + fp
   361  			}
   362  			o += sz
   363  		}
   364  		return p
   365  	}
   366  
   367  	panic("impossible")
   368  }