Skip to content

Commit d00036c

Browse files
committed
compiler: add object layout information to heap allocations
This commit adds object layout information to new heap allocations. It is not yet used anywhere: the next commit will make use of it. Object layout information will eventually be used for a (mostly) precise garbage collector. This is what the data is made for. However, it is also useful in the interp package which can work better if it knows the memory layout and thus the approximate LLVM type of heap-allocated objects.
1 parent 30a92bb commit d00036c

File tree

9 files changed

+391
-13
lines changed

9 files changed

+391
-13
lines changed

compiler/compiler.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
// Version of the compiler pacakge. Must be incremented each time the compiler
2424
// package changes in a way that affects the generated LLVM module.
2525
// This version is independent of the TinyGo version number.
26-
const Version = 20 // last change: fix export math functions issue
26+
const Version = 21 // last change: add layout param to runtime.alloc calls
2727

2828
func init() {
2929
llvm.InitializeAllTargets()
@@ -1518,8 +1518,8 @@ func (b *builder) createExpr(expr ssa.Value) (llvm.Value, error) {
15181518
return llvm.Value{}, b.makeError(expr.Pos(), fmt.Sprintf("value is too big (%v bytes)", size))
15191519
}
15201520
sizeValue := llvm.ConstInt(b.uintptrType, size, false)
1521-
nilPtr := llvm.ConstNull(b.i8ptrType)
1522-
buf := b.createRuntimeCall("alloc", []llvm.Value{sizeValue, nilPtr}, expr.Comment)
1521+
layoutValue := b.createObjectLayout(typ, expr.Pos())
1522+
buf := b.createRuntimeCall("alloc", []llvm.Value{sizeValue, layoutValue}, expr.Comment)
15231523
buf = b.CreateBitCast(buf, llvm.PointerType(typ, 0), "")
15241524
return buf, nil
15251525
} else {
@@ -1731,8 +1731,8 @@ func (b *builder) createExpr(expr ssa.Value) (llvm.Value, error) {
17311731
return llvm.Value{}, err
17321732
}
17331733
sliceSize := b.CreateBinOp(llvm.Mul, elemSizeValue, sliceCapCast, "makeslice.cap")
1734-
nilPtr := llvm.ConstNull(b.i8ptrType)
1735-
slicePtr := b.createRuntimeCall("alloc", []llvm.Value{sliceSize, nilPtr}, "makeslice.buf")
1734+
layoutValue := b.createObjectLayout(llvmElemType, expr.Pos())
1735+
slicePtr := b.createRuntimeCall("alloc", []llvm.Value{sliceSize, layoutValue}, "makeslice.buf")
17361736
slicePtr = b.CreateBitCast(slicePtr, llvm.PointerType(llvmElemType, 0), "makeslice.array")
17371737

17381738
// Extend or truncate if necessary. This is safe as we've already done

compiler/compiler_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func TestCompiler(t *testing.T) {
5454
{"channel.go", ""},
5555
{"intrinsics.go", "cortex-m-qemu"},
5656
{"intrinsics.go", "wasm"},
57+
{"gc.go", ""},
5758
}
5859

5960
_, minor, err := goenv.GetGorootVersion(goenv.Get("GOROOT"))

compiler/llvm.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package compiler
22

33
import (
4+
"fmt"
5+
"go/token"
6+
"go/types"
7+
"math/big"
8+
49
"github.com/tinygo-org/tinygo/compiler/llvmutil"
510
"tinygo.org/x/go-llvm"
611
)
@@ -52,3 +57,199 @@ func (c *compilerContext) makeGlobalArray(buf []byte, name string, elementType l
5257
global.SetInitializer(value)
5358
return global
5459
}
60+
61+
// createObjectLayout returns a LLVM value (of type i8*) that describes where
62+
// there are pointers in the type t. If all the data fits in a word, it is
63+
// returned as a word. Otherwise it will store the data in a global.
64+
//
65+
// The value contains two pieces of information: the length of the object and
66+
// which words contain a pointer (indicated by setting the given bit to 1). For
67+
// arrays, only the element is stored. This works because the GC knows the
68+
// object size and can therefore know how this value is repeated in the object.
69+
func (c *compilerContext) createObjectLayout(t llvm.Type, pos token.Pos) llvm.Value {
70+
// Use the element type for arrays. This works even for nested arrays.
71+
for {
72+
kind := t.TypeKind()
73+
if kind == llvm.ArrayTypeKind {
74+
t = t.ElementType()
75+
continue
76+
}
77+
if kind == llvm.StructTypeKind {
78+
fields := t.StructElementTypes()
79+
if len(fields) == 1 {
80+
t = fields[0]
81+
continue
82+
}
83+
}
84+
break
85+
}
86+
87+
// Do a few checks to see whether we need to generate any object layout
88+
// information at all.
89+
objectSizeBytes := c.targetData.TypeAllocSize(t)
90+
pointerSize := c.targetData.TypeAllocSize(c.i8ptrType)
91+
pointerAlignment := c.targetData.PrefTypeAlignment(c.i8ptrType)
92+
if objectSizeBytes < pointerSize {
93+
// Too small to contain a pointer.
94+
layout := (uint64(1) << 1) | 1
95+
return llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, layout, false), c.i8ptrType)
96+
}
97+
bitmap := c.getPointerBitmap(t, pos)
98+
if bitmap.BitLen() == 0 {
99+
// There are no pointers in this type, so we can simplify the layout.
100+
// TODO: this can be done in many other cases, e.g. when allocating an
101+
// array (like [4][]byte, which repeats a slice 4 times).
102+
layout := (uint64(1) << 1) | 1
103+
return llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, layout, false), c.i8ptrType)
104+
}
105+
if objectSizeBytes%uint64(pointerAlignment) != 0 {
106+
// This shouldn't happen except for packed structs, which aren't
107+
// currently used.
108+
c.addError(pos, "internal error: unexpected object size for object with pointer field")
109+
return llvm.ConstNull(c.i8ptrType)
110+
}
111+
objectSizeWords := objectSizeBytes / uint64(pointerAlignment)
112+
113+
pointerBits := pointerSize * 8
114+
var sizeFieldBits uint64
115+
switch pointerBits {
116+
case 16:
117+
sizeFieldBits = 4
118+
case 32:
119+
sizeFieldBits = 5
120+
case 64:
121+
sizeFieldBits = 6
122+
default:
123+
panic("unknown pointer size")
124+
}
125+
layoutFieldBits := pointerBits - 1 - sizeFieldBits
126+
127+
// Try to emit the value as an inline integer. This is possible in most
128+
// cases.
129+
if objectSizeWords < layoutFieldBits {
130+
// If it can be stored directly in the pointer value, do so.
131+
// The runtime knows that if the least significant bit of the pointer is
132+
// set, the pointer contains the value itself.
133+
layout := bitmap.Uint64()<<(sizeFieldBits+1) | (objectSizeWords << 1) | 1
134+
return llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, layout, false), c.i8ptrType)
135+
}
136+
137+
// Unfortunately, the object layout is too big to fit in a pointer-sized
138+
// integer. Store it in a global instead.
139+
140+
// Try first whether the global already exists. All objects with a
141+
// particular name have the same type, so this is possible.
142+
globalName := "runtime/gc.layout:" + fmt.Sprintf("%d-%0*x", objectSizeWords, (objectSizeWords+15)/16, bitmap)
143+
global := c.mod.NamedGlobal(globalName)
144+
if !global.IsNil() {
145+
return llvm.ConstBitCast(global, c.i8ptrType)
146+
}
147+
148+
// Create the global initializer.
149+
bitmapBytes := make([]byte, int(objectSizeWords+7)/8)
150+
copy(bitmapBytes, bitmap.Bytes())
151+
var bitmapByteValues []llvm.Value
152+
for _, b := range bitmapBytes {
153+
bitmapByteValues = append(bitmapByteValues, llvm.ConstInt(c.ctx.Int8Type(), uint64(b), false))
154+
}
155+
initializer := c.ctx.ConstStruct([]llvm.Value{
156+
llvm.ConstInt(c.uintptrType, objectSizeWords, false),
157+
llvm.ConstArray(c.ctx.Int8Type(), bitmapByteValues),
158+
}, false)
159+
160+
global = llvm.AddGlobal(c.mod, initializer.Type(), globalName)
161+
global.SetInitializer(initializer)
162+
global.SetUnnamedAddr(true)
163+
global.SetGlobalConstant(true)
164+
global.SetLinkage(llvm.LinkOnceODRLinkage)
165+
if c.targetData.PrefTypeAlignment(c.uintptrType) < 2 {
166+
// AVR doesn't have alignment by default.
167+
global.SetAlignment(2)
168+
}
169+
if c.Debug && pos != token.NoPos {
170+
// Creating a fake global so that the value can be inspected in GDB.
171+
// For example, the layout for strings.stringFinder (as of Go version
172+
// 1.15) has the following type according to GDB:
173+
// type = struct {
174+
// uintptr numBits;
175+
// uint8 data[33];
176+
// }
177+
// ...that's sort of a mixed C/Go type, but it is readable. More
178+
// importantly, these object layout globals can be read and printed by
179+
// GDB which may be useful for debugging.
180+
position := c.program.Fset.Position(pos)
181+
diglobal := c.dibuilder.CreateGlobalVariableExpression(c.difiles[position.Filename], llvm.DIGlobalVariableExpression{
182+
Name: globalName,
183+
File: c.getDIFile(position.Filename),
184+
Line: position.Line,
185+
Type: c.getDIType(types.NewStruct([]*types.Var{
186+
types.NewVar(pos, nil, "numBits", types.Typ[types.Uintptr]),
187+
types.NewVar(pos, nil, "data", types.NewArray(types.Typ[types.Byte], int64(len(bitmapByteValues)))),
188+
}, nil)),
189+
LocalToUnit: false,
190+
Expr: c.dibuilder.CreateExpression(nil),
191+
})
192+
global.AddMetadata(0, diglobal)
193+
}
194+
195+
return llvm.ConstBitCast(global, c.i8ptrType)
196+
}
197+
198+
// getPointerBitmap scans the given LLVM type for pointers and sets bits in a
199+
// bigint at the word offset that contains a pointer. This scan is recursive.
200+
func (c *compilerContext) getPointerBitmap(typ llvm.Type, pos token.Pos) *big.Int {
201+
alignment := c.targetData.PrefTypeAlignment(c.i8ptrType)
202+
switch typ.TypeKind() {
203+
case llvm.IntegerTypeKind, llvm.FloatTypeKind, llvm.DoubleTypeKind:
204+
return big.NewInt(0)
205+
case llvm.PointerTypeKind:
206+
return big.NewInt(1)
207+
case llvm.StructTypeKind:
208+
ptrs := big.NewInt(0)
209+
if typ.StructName() == "runtime.funcValue" {
210+
// Hack: the type runtime.funcValue contains an 'id' field which is
211+
// of type uintptr, but before the LowerFuncValues pass it actually
212+
// contains a pointer (ptrtoint) to a global. This trips up the
213+
// interp package. Therefore, make the id field a pointer for now.
214+
typ = c.ctx.StructType([]llvm.Type{c.i8ptrType, c.i8ptrType}, false)
215+
}
216+
for i, subtyp := range typ.StructElementTypes() {
217+
subptrs := c.getPointerBitmap(subtyp, pos)
218+
if subptrs.BitLen() == 0 {
219+
continue
220+
}
221+
offset := c.targetData.ElementOffset(typ, i)
222+
if offset%uint64(alignment) != 0 {
223+
// This error will let the compilation fail, but by continuing
224+
// the error can still easily be shown.
225+
c.addError(pos, "internal error: allocated struct contains unaligned pointer")
226+
continue
227+
}
228+
subptrs.Lsh(subptrs, uint(offset)/uint(alignment))
229+
ptrs.Or(ptrs, subptrs)
230+
}
231+
return ptrs
232+
case llvm.ArrayTypeKind:
233+
subtyp := typ.ElementType()
234+
subptrs := c.getPointerBitmap(subtyp, pos)
235+
ptrs := big.NewInt(0)
236+
if subptrs.BitLen() == 0 {
237+
return ptrs
238+
}
239+
elementSize := c.targetData.TypeAllocSize(subtyp)
240+
if elementSize%uint64(alignment) != 0 {
241+
// This error will let the compilation fail (but continues so that
242+
// other errors can be shown).
243+
c.addError(pos, "internal error: allocated array contains unaligned pointer")
244+
return ptrs
245+
}
246+
for i := 0; i < typ.ArrayLength(); i++ {
247+
ptrs.Lsh(ptrs, uint(elementSize)/uint(alignment))
248+
ptrs.Or(ptrs, subptrs)
249+
}
250+
return ptrs
251+
default:
252+
// Should not happen.
253+
panic("unknown LLVM type")
254+
}
255+
}

compiler/testdata/gc.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package main
2+
3+
var (
4+
scalar1 *byte
5+
scalar2 *int32
6+
scalar3 *int64
7+
scalar4 *float32
8+
9+
array1 *[3]byte
10+
array2 *[71]byte
11+
array3 *[3]*byte
12+
13+
struct1 *struct{}
14+
struct2 *struct {
15+
x int
16+
y int
17+
}
18+
struct3 *struct {
19+
x *byte
20+
y [60]uintptr
21+
z *byte
22+
}
23+
24+
slice1 []byte
25+
slice2 []*int
26+
slice3 [][]byte
27+
)
28+
29+
func newScalar() {
30+
scalar1 = new(byte)
31+
scalar2 = new(int32)
32+
scalar3 = new(int64)
33+
scalar4 = new(float32)
34+
}
35+
36+
func newArray() {
37+
array1 = new([3]byte)
38+
array2 = new([71]byte)
39+
array3 = new([3]*byte)
40+
}
41+
42+
func newStruct() {
43+
struct1 = new(struct{})
44+
struct2 = new(struct {
45+
x int
46+
y int
47+
})
48+
struct3 = new(struct {
49+
x *byte
50+
y [60]uintptr
51+
z *byte
52+
})
53+
}
54+
55+
func newFuncValue() *func() {
56+
// On some platforms that use runtime.funcValue ("switch" style) function
57+
// values, a func value is allocated as having two pointer words while the
58+
// struct looks like {unsafe.Pointer; uintptr}. This is so that the interp
59+
// package won't get confused, see getPointerBitmap in compiler/llvm.go for
60+
// details.
61+
return new(func())
62+
}
63+
64+
func makeSlice() {
65+
slice1 = make([]byte, 5)
66+
slice2 = make([]*int, 5)
67+
slice3 = make([][]byte, 5)
68+
}
69+
70+
func makeInterface(v complex128) interface{} {
71+
return v // always stored in an allocation
72+
}

0 commit comments

Comments
 (0)