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 }