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.
5 // Package fieldalignment defines an Analyzer that detects structs that would take less
6 // memory if their fields were sorted.
18 "golang.org/x/tools/go/analysis"
19 "golang.org/x/tools/go/analysis/passes/inspect"
20 "golang.org/x/tools/go/ast/inspector"
23 const Doc = `find structs that would take less memory if their fields were sorted
25 This analyzer find structs that can be rearranged to take less memory, and provides
26 a suggested edit with the optimal order.
29 var Analyzer = &analysis.Analyzer{
30 Name: "fieldalignment",
32 Requires: []*analysis.Analyzer{inspect.Analyzer},
36 func run(pass *analysis.Pass) (interface{}, error) {
37 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
38 nodeFilter := []ast.Node{
39 (*ast.StructType)(nil),
41 inspect.Preorder(nodeFilter, func(node ast.Node) {
44 if s, ok = node.(*ast.StructType); !ok {
47 if tv, ok := pass.TypesInfo.Types[s]; ok {
48 fieldalignment(pass, s, tv.Type.(*types.Struct))
54 var unsafePointerTyp = types.Unsafe.Scope().Lookup("Pointer").(*types.TypeName).Type()
56 func fieldalignment(pass *analysis.Pass, node *ast.StructType, typ *types.Struct) {
57 wordSize := pass.TypesSizes.Sizeof(unsafePointerTyp)
58 maxAlign := pass.TypesSizes.Alignof(unsafePointerTyp)
60 s := gcSizes{wordSize, maxAlign}
61 optimal, indexes := optimalOrder(typ, &s)
62 optsz, optptrs := s.Sizeof(optimal), s.ptrdata(optimal)
65 if sz := s.Sizeof(typ); sz != optsz {
66 message = fmt.Sprintf("struct of size %d could be %d", sz, optsz)
67 } else if ptrs := s.ptrdata(typ); ptrs != optptrs {
68 message = fmt.Sprintf("struct with %d pointer bytes could be %d", ptrs, optptrs)
70 // Already optimal order.
74 // Flatten the ast node since it could have multiple field names per list item while
75 // *types.Struct only have one item per field.
76 // TODO: Preserve multi-named fields instead of flattening.
78 for _, f := range node.Fields.List {
79 // TODO: Preserve comment, for now get rid of them.
80 // See https://github.com/golang/go/issues/20744
83 if len(f.Names) <= 1 {
84 flat = append(flat, f)
87 for _, name := range f.Names {
88 flat = append(flat, &ast.Field{
89 Names: []*ast.Ident{name},
95 // Sort fields according to the optimal order.
96 var reordered []*ast.Field
97 for _, index := range indexes {
98 reordered = append(reordered, flat[index])
101 newStr := &ast.StructType{
102 Fields: &ast.FieldList{
107 // Write the newly aligned struct node to get the content for suggested fixes.
109 if err := format.Node(&buf, token.NewFileSet(), newStr); err != nil {
113 pass.Report(analysis.Diagnostic{
115 End: node.Pos() + token.Pos(len("struct")),
117 SuggestedFixes: []analysis.SuggestedFix{{
118 Message: "Rearrange fields",
119 TextEdits: []analysis.TextEdit{{
122 NewText: buf.Bytes(),
128 func optimalOrder(str *types.Struct, sizes *gcSizes) (*types.Struct, []int) {
129 nf := str.NumFields()
138 elems := make([]elem, nf)
139 for i := 0; i < nf; i++ {
140 field := str.Field(i)
150 sort.Slice(elems, func(i, j int) bool {
154 // Place zero sized objects before non-zero sized objects.
155 zeroi := ei.sizeof == 0
156 zeroj := ej.sizeof == 0
161 // Next, place more tightly aligned objects before less tightly aligned objects.
162 if ei.alignof != ej.alignof {
163 return ei.alignof > ej.alignof
166 // Place pointerful objects before pointer-free objects.
167 noptrsi := ei.ptrdata == 0
168 noptrsj := ej.ptrdata == 0
169 if noptrsi != noptrsj {
174 // If both have pointers...
176 // ... then place objects with less trailing
177 // non-pointer bytes earlier. That is, place
178 // the field with the most trailing
179 // non-pointer bytes at the end of the
180 // pointerful section.
181 traili := ei.sizeof - ei.ptrdata
182 trailj := ej.sizeof - ej.ptrdata
183 if traili != trailj {
184 return traili < trailj
188 // Lastly, order by size.
189 if ei.sizeof != ej.sizeof {
190 return ei.sizeof > ej.sizeof
196 fields := make([]*types.Var, nf)
197 indexes := make([]int, nf)
198 for i, e := range elems {
199 fields[i] = str.Field(e.index)
202 return types.NewStruct(fields, nil), indexes
205 // Code below based on go/types.StdSizes.
207 type gcSizes struct {
212 func (s *gcSizes) Alignof(T types.Type) int64 {
213 // For arrays and structs, alignment is defined in terms
214 // of alignment of the elements and fields, respectively.
215 switch t := T.Underlying().(type) {
217 // spec: "For a variable x of array type: unsafe.Alignof(x)
218 // is the same as unsafe.Alignof(x[0]), but at least 1."
219 return s.Alignof(t.Elem())
221 // spec: "For a variable x of struct type: unsafe.Alignof(x)
222 // is the largest of the values unsafe.Alignof(x.f) for each
223 // field f of x, but at least 1."
225 for i, nf := 0, t.NumFields(); i < nf; i++ {
226 if a := s.Alignof(t.Field(i).Type()); a > max {
232 a := s.Sizeof(T) // may be 0
233 // spec: "For a variable x of any type: unsafe.Alignof(x) is at least 1."
243 var basicSizes = [...]byte{
256 types.Complex128: 16,
259 func (s *gcSizes) Sizeof(T types.Type) int64 {
260 switch t := T.Underlying().(type) {
263 if int(k) < len(basicSizes) {
264 if s := basicSizes[k]; s > 0 {
268 if k == types.String {
269 return s.WordSize * 2
272 return t.Len() * s.Sizeof(t.Elem())
274 return s.WordSize * 3
283 for i := 0; i < nf; i++ {
284 ft := t.Field(i).Type()
285 a, sz := s.Alignof(ft), s.Sizeof(ft)
289 if i == nf-1 && sz == 0 && o != 0 {
295 case *types.Interface:
296 return s.WordSize * 2
298 return s.WordSize // catch-all
301 // align returns the smallest y >= x such that y % a == 0.
302 func align(x, a int64) int64 {
307 func (s *gcSizes) ptrdata(T types.Type) int64 {
308 switch t := T.Underlying().(type) {
311 case types.String, types.UnsafePointer:
315 case *types.Chan, *types.Map, *types.Pointer, *types.Signature, *types.Slice:
317 case *types.Interface:
318 return 2 * s.WordSize
324 a := s.ptrdata(t.Elem())
328 z := s.Sizeof(t.Elem())
337 for i := 0; i < nf; i++ {
338 ft := t.Field(i).Type()
339 a, sz := s.Alignof(ft), s.Sizeof(ft)