Skip to content

Commit 312f5d3

Browse files
aykevldeadprogram
authored andcommitted
builder: run interp per package
This results in a significant speedup in some cases. For example, this runs over twice as fast with a warm cache: tinygo build -o test.elf ./testdata/stdlib.go This should help a lot with edit-compile-test cycles, that typically only modify a single package. This required some changes to the interp package to deal with globals created in a previous run of the interp package and to deal with external globals (that can't be loaded from or stored to).
1 parent 35bf074 commit 312f5d3

File tree

5 files changed

+188
-10
lines changed

5 files changed

+188
-10
lines changed

builder/build.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type BuildResult struct {
5555
type packageAction struct {
5656
ImportPath string
5757
CompilerVersion int // compiler.Version
58+
InterpVersion int // interp.Version
5859
LLVMVersion string
5960
Config *compiler.Config
6061
CFlags []string
@@ -135,6 +136,7 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil
135136
actionID := packageAction{
136137
ImportPath: pkg.ImportPath,
137138
CompilerVersion: compiler.Version,
139+
InterpVersion: interp.Version,
138140
LLVMVersion: llvm.Version,
139141
Config: compilerConfig,
140142
CFlags: pkg.CFlags,
@@ -190,6 +192,21 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil
190192
return errors.New("verification error after compiling package " + pkg.ImportPath)
191193
}
192194

195+
// Try to interpret package initializers at compile time.
196+
// It may only be possible to do this partially, in which case
197+
// it is completed after all IR files are linked.
198+
pkgInit := mod.NamedFunction(pkg.Pkg.Path() + ".init")
199+
if pkgInit.IsNil() {
200+
panic("init not found for " + pkg.Pkg.Path())
201+
}
202+
err := interp.RunFunc(pkgInit, config.DumpSSA())
203+
if err != nil {
204+
return err
205+
}
206+
if err := llvm.VerifyModule(mod, llvm.PrintMessageAction); err != nil {
207+
return errors.New("verification error after interpreting " + pkgInit.Name())
208+
}
209+
193210
// Serialize the LLVM module as a bitcode file.
194211
// Write to a temporary path that is renamed to the destination
195212
// file to avoid race conditions with other TinyGo invocatiosn
@@ -575,8 +592,17 @@ func optimizeProgram(mod llvm.Module, config *compileopts.Config) error {
575592
if err != nil {
576593
return err
577594
}
578-
if err := llvm.VerifyModule(mod, llvm.PrintMessageAction); err != nil {
579-
return errors.New("verification error after interpreting runtime.initAll")
595+
if config.VerifyIR() {
596+
// Only verify if we really need it.
597+
// The IR has already been verified before writing the bitcode to disk
598+
// and the interp function above doesn't need to do a lot as most of the
599+
// package initializers have already run. Additionally, verifying this
600+
// linked IR is _expensive_ because dead code hasn't been removed yet,
601+
// easily costing a few hundred milliseconds. Therefore, only do it when
602+
// specifically requested.
603+
if err := llvm.VerifyModule(mod, llvm.PrintMessageAction); err != nil {
604+
return errors.New("verification error after interpreting runtime.initAll")
605+
}
580606
}
581607

582608
if config.GOOS() != "darwin" {

interp/errors.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ var (
2020
errMapAlreadyCreated = errors.New("interp: map already created")
2121
)
2222

23+
// This is one of the errors that can be returned from toLLVMValue when the
24+
// passed type does not fit the data to serialize. It is recoverable by
25+
// serializing without a type (using rawValue.rawLLVMValue).
26+
var errInvalidPtrToIntSize = errors.New("interp: ptrtoint integer size does not equal pointer size")
27+
2328
func isRecoverableError(err error) bool {
2429
return err == errIntegerAsPointer || err == errUnsupportedInst || err == errUnsupportedRuntimeInst || err == errMapAlreadyCreated
2530
}

interp/interp.go

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ import (
1111
"tinygo.org/x/go-llvm"
1212
)
1313

14+
// Version of the interp package. It must be incremented whenever the interp
15+
// package is changed in a way that affects the output so that cached package
16+
// builds will be invalidated.
17+
// This version is independent of the TinyGo version number.
18+
const Version = 1
19+
1420
// Enable extra checks, which should be disabled by default.
1521
// This may help track down bugs by adding a few more sanity checks.
1622
const checks = true
@@ -32,9 +38,7 @@ type runner struct {
3238
callsExecuted uint64
3339
}
3440

35-
// Run evaluates runtime.initAll function as much as possible at compile time.
36-
// Set debug to true if it should print output while running.
37-
func Run(mod llvm.Module, debug bool) error {
41+
func newRunner(mod llvm.Module, debug bool) *runner {
3842
r := runner{
3943
mod: mod,
4044
targetData: llvm.NewTargetData(mod.DataLayout()),
@@ -47,6 +51,13 @@ func Run(mod llvm.Module, debug bool) error {
4751
r.pointerSize = uint32(r.targetData.PointerSize())
4852
r.i8ptrType = llvm.PointerType(mod.Context().Int8Type(), 0)
4953
r.maxAlign = r.targetData.PrefTypeAlignment(r.i8ptrType) // assume pointers are maximally aligned (this is not always the case)
54+
return &r
55+
}
56+
57+
// Run evaluates runtime.initAll function as much as possible at compile time.
58+
// Set debug to true if it should print output while running.
59+
func Run(mod llvm.Module, debug bool) error {
60+
r := newRunner(mod, debug)
5061

5162
initAll := mod.NamedFunction("runtime.initAll")
5263
bb := initAll.EntryBasicBlock()
@@ -117,7 +128,104 @@ func Run(mod llvm.Module, debug bool) error {
117128
r.pkgName = ""
118129

119130
// Update all global variables in the LLVM module.
120-
mem := memoryView{r: &r}
131+
mem := memoryView{r: r}
132+
for _, obj := range r.objects {
133+
if obj.llvmGlobal.IsNil() {
134+
continue
135+
}
136+
if obj.buffer == nil {
137+
continue
138+
}
139+
initializer, err := obj.buffer.toLLVMValue(obj.llvmGlobal.Type().ElementType(), &mem)
140+
if err == errInvalidPtrToIntSize {
141+
// This can happen when a previous interp run did not have the
142+
// correct LLVM type for a global and made something up. In that
143+
// case, some fields could be written out as a series of (null)
144+
// bytes even though they actually contain a pointer value.
145+
// As a fallback, use asRawValue to get something of the correct
146+
// memory layout.
147+
initializer, err := obj.buffer.asRawValue(r).rawLLVMValue(&mem)
148+
if err != nil {
149+
return err
150+
}
151+
initializerType := initializer.Type()
152+
newGlobal := llvm.AddGlobal(mod, initializerType, obj.llvmGlobal.Name()+".tmp")
153+
newGlobal.SetInitializer(initializer)
154+
newGlobal.SetLinkage(obj.llvmGlobal.Linkage())
155+
newGlobal.SetAlignment(obj.llvmGlobal.Alignment())
156+
// TODO: copy debug info, unnamed_addr, ...
157+
bitcast := llvm.ConstBitCast(newGlobal, obj.llvmGlobal.Type())
158+
obj.llvmGlobal.ReplaceAllUsesWith(bitcast)
159+
name := obj.llvmGlobal.Name()
160+
obj.llvmGlobal.EraseFromParentAsGlobal()
161+
newGlobal.SetName(name)
162+
continue
163+
}
164+
if err != nil {
165+
return err
166+
}
167+
if checks && initializer.Type() != obj.llvmGlobal.Type().ElementType() {
168+
panic("initializer type mismatch")
169+
}
170+
obj.llvmGlobal.SetInitializer(initializer)
171+
}
172+
173+
return nil
174+
}
175+
176+
// RunFunc evaluates a single package initializer at compile time.
177+
// Set debug to true if it should print output while running.
178+
func RunFunc(fn llvm.Value, debug bool) error {
179+
// Create and initialize *runner object.
180+
mod := fn.GlobalParent()
181+
r := newRunner(mod, debug)
182+
initName := fn.Name()
183+
if !strings.HasSuffix(initName, ".init") {
184+
return errorAt(fn, "interp: unexpected function name (expected *.init)")
185+
}
186+
r.pkgName = initName[:len(initName)-len(".init")]
187+
188+
// Create new function with the interp result.
189+
newFn := llvm.AddFunction(mod, fn.Name()+".tmp", fn.Type().ElementType())
190+
newFn.SetLinkage(fn.Linkage())
191+
newFn.SetVisibility(fn.Visibility())
192+
entry := mod.Context().AddBasicBlock(newFn, "entry")
193+
194+
// Create a builder, to insert instructions that could not be evaluated at
195+
// compile time.
196+
r.builder = mod.Context().NewBuilder()
197+
defer r.builder.Dispose()
198+
r.builder.SetInsertPointAtEnd(entry)
199+
200+
// Copy debug information.
201+
subprogram := fn.Subprogram()
202+
if !subprogram.IsNil() {
203+
newFn.SetSubprogram(subprogram)
204+
r.builder.SetCurrentDebugLocation(subprogram.SubprogramLine(), 0, subprogram, llvm.Metadata{})
205+
}
206+
207+
// Run the initializer, filling the .init.tmp function.
208+
if r.debug {
209+
fmt.Fprintln(os.Stderr, "interp:", fn.Name())
210+
}
211+
_, pkgMem, callErr := r.run(r.getFunction(fn), nil, nil, " ")
212+
if callErr != nil {
213+
if isRecoverableError(callErr.Err) {
214+
// Could not finish, but could recover from it.
215+
if r.debug {
216+
fmt.Fprintln(os.Stderr, "not interpreting", r.pkgName, "because of error:", callErr.Error())
217+
}
218+
newFn.EraseFromParentAsFunction()
219+
return nil
220+
}
221+
return callErr
222+
}
223+
for index, obj := range pkgMem.objects {
224+
r.objects[index] = obj
225+
}
226+
227+
// Update globals with values determined while running the initializer above.
228+
mem := memoryView{r: r}
121229
for _, obj := range r.objects {
122230
if obj.llvmGlobal.IsNil() {
123231
continue
@@ -135,6 +243,14 @@ func Run(mod llvm.Module, debug bool) error {
135243
obj.llvmGlobal.SetInitializer(initializer)
136244
}
137245

246+
// Finalize: remove the old init function and replace it with the new
247+
// (.init.tmp) function.
248+
r.builder.CreateRetVoid()
249+
fnName := fn.Name()
250+
fn.ReplaceAllUsesWith(newFn)
251+
fn.EraseFromParentAsFunction()
252+
newFn.SetName(fnName)
253+
138254
return nil
139255
}
140256

interp/interpreter.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,13 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent
548548
continue
549549
}
550550
result := mem.load(ptr, uint32(size))
551+
if result == nil {
552+
err := r.runAtRuntime(fn, inst, locals, &mem, indent)
553+
if err != nil {
554+
return nil, mem, err
555+
}
556+
continue
557+
}
551558
if r.debug {
552559
fmt.Fprintln(os.Stderr, indent+"load:", ptr, "->", result)
553560
}
@@ -570,7 +577,14 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent
570577
if r.debug {
571578
fmt.Fprintln(os.Stderr, indent+"store:", val, ptr)
572579
}
573-
mem.store(val, ptr)
580+
ok := mem.store(val, ptr)
581+
if !ok {
582+
// Could not store the value, do it at runtime.
583+
err := r.runAtRuntime(fn, inst, locals, &mem, indent)
584+
if err != nil {
585+
return nil, mem, err
586+
}
587+
}
574588
case llvm.Alloca:
575589
// Alloca normally allocates some stack memory. In the interpreter,
576590
// it allocates a global instead.

interp/memory.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,17 @@ func (mv *memoryView) put(index uint32, obj object) {
259259
mv.objects[index] = obj
260260
}
261261

262-
// Load the value behind the given pointer.
262+
// Load the value behind the given pointer. Returns nil if the pointer points to
263+
// an external global.
263264
func (mv *memoryView) load(p pointerValue, size uint32) value {
264265
if checks && mv.hasExternalStore(p) {
265266
panic("interp: load from object with external store")
266267
}
267268
obj := mv.get(p.index())
269+
if obj.buffer == nil {
270+
// External global, return nil.
271+
return nil
272+
}
268273
if p.offset() == 0 && size == obj.size {
269274
return obj.buffer.clone()
270275
}
@@ -280,12 +285,17 @@ func (mv *memoryView) load(p pointerValue, size uint32) value {
280285

281286
// Store to the value behind the given pointer. This overwrites the value in the
282287
// memory view, so that the changed value is discarded when the memory view is
283-
// reverted.
284-
func (mv *memoryView) store(v value, p pointerValue) {
288+
// reverted. Returns true on success, false if the object to store to is
289+
// external.
290+
func (mv *memoryView) store(v value, p pointerValue) bool {
285291
if checks && mv.hasExternalLoadOrStore(p) {
286292
panic("interp: store to object with external load/store")
287293
}
288294
obj := mv.get(p.index())
295+
if obj.buffer == nil {
296+
// External global, return false (for a failure).
297+
return false
298+
}
289299
if checks && p.offset()+v.len(mv.r) > obj.size {
290300
panic("interp: store out of bounds")
291301
}
@@ -301,6 +311,7 @@ func (mv *memoryView) store(v value, p pointerValue) {
301311
}
302312
}
303313
mv.put(p.index(), obj)
314+
return true // success
304315
}
305316

306317
// value is some sort of value, comparable to a LLVM constant. It can be
@@ -1105,6 +1116,12 @@ func (v rawValue) toLLVMValue(llvmType llvm.Type, mem *memoryView) (llvm.Value,
11051116
if err != nil {
11061117
panic(err)
11071118
}
1119+
if checks && mem.r.targetData.TypeAllocSize(llvmType) != mem.r.targetData.TypeAllocSize(mem.r.i8ptrType) {
1120+
// Probably trying to serialize a pointer to a byte array,
1121+
// perhaps as a result of rawLLVMValue() in a previous interp
1122+
// run.
1123+
return llvm.Value{}, errInvalidPtrToIntSize
1124+
}
11081125
v, err := ptr.toLLVMValue(llvm.Type{}, mem)
11091126
if err != nil {
11101127
return llvm.Value{}, err

0 commit comments

Comments
 (0)