1
2
3
4
5
6
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
94 return
95 }
96
97
98
99
100 var flat []*ast.Field
101 for _, f := range node.Fields.List {
102
103
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
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
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
178 zeroi := ei.sizeof == 0
179 zeroj := ej.sizeof == 0
180 if zeroi != zeroj {
181 return zeroi
182 }
183
184
185 if ei.alignof != ej.alignof {
186 return ei.alignof > ej.alignof
187 }
188
189
190 noptrsi := ei.ptrdata == 0
191 noptrsj := ej.ptrdata == 0
192 if noptrsi != noptrsj {
193 return noptrsj
194 }
195
196 if !noptrsi {
197
198
199
200
201
202
203
204 traili := ei.sizeof - ei.ptrdata
205 trailj := ej.sizeof - ej.ptrdata
206 if traili != trailj {
207 return traili < trailj
208 }
209 }
210
211
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
229
230 type gcSizes struct {
231 WordSize int64
232 MaxAlign int64
233 }
234
235 func (s *gcSizes) Alignof(T types.Type) int64 {
236
237
238 switch t := T.Underlying().(type) {
239 case *types.Array:
240
241
242 return s.Alignof(t.Elem())
243 case *types.Struct:
244
245
246
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)
256
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
322 }
323
324
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 }
375
View as plain text