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