Skip to content

Commit d606315

Browse files
aykevldeadprogram
authored andcommitted
builder: try to determine stack size information at compile time
For now, this is just an extra flag that can be used to print stack frame information, but this is intended to provide a way to determine stack sizes for goroutines at compile time in many cases. Stack sizes are often somewhere around 350 bytes so are in fact not all that big usually. Once this can be determined at compile time in many cases, it is possible to use this information when available and as a result increase the fallback stack size if the size cannot be determined at compile time. This should reduce stack overflows while at the same time reducing RAM consumption in many cases. Interesting output for testdata/channel.go: function stack usage (in bytes) Reset_Handler 332 .Lcommand-line-arguments.fastreceiver 220 .Lcommand-line-arguments.fastsender 192 .Lcommand-line-arguments.iterator 192 .Lcommand-line-arguments.main$1 184 .Lcommand-line-arguments.main$2 200 .Lcommand-line-arguments.main$3 200 .Lcommand-line-arguments.main$4 328 .Lcommand-line-arguments.receive 176 .Lcommand-line-arguments.selectDeadlock 72 .Lcommand-line-arguments.selectNoOp 72 .Lcommand-line-arguments.send 184 .Lcommand-line-arguments.sendComplex 192 .Lcommand-line-arguments.sender 192 .Lruntime.run$1 548 This shows that the stack size (if these numbers are correct) can in fact be determined automatically in many cases, especially for small goroutines. One of the great things about Go is lightweight goroutines, and reducing stack sizes is very important to make goroutines lightweight on microcontrollers.
1 parent 60fdf81 commit d606315

File tree

9 files changed

+681
-0
lines changed

9 files changed

+681
-0
lines changed

builder/build.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@
44
package builder
55

66
import (
7+
"debug/elf"
78
"errors"
89
"fmt"
910
"io/ioutil"
1011
"os"
1112
"path/filepath"
13+
"sort"
1214
"strconv"
1315
"strings"
1416

1517
"github.com/tinygo-org/tinygo/compileopts"
1618
"github.com/tinygo-org/tinygo/compiler"
1719
"github.com/tinygo-org/tinygo/goenv"
1820
"github.com/tinygo-org/tinygo/interp"
21+
"github.com/tinygo-org/tinygo/stacksize"
1922
"github.com/tinygo-org/tinygo/transform"
2023
"tinygo.org/x/go-llvm"
2124
)
@@ -216,6 +219,11 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(stri
216219
}
217220
}
218221

222+
// Print goroutine stack sizes, as far as possible.
223+
if config.Options.PrintStacks {
224+
printStacks(mod, executable)
225+
}
226+
219227
// Get an Intel .hex file or .bin file from the .elf file.
220228
if outext == ".hex" || outext == ".bin" || outext == ".gba" {
221229
tmppath = filepath.Join(dir, "main"+outext)
@@ -234,3 +242,89 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(stri
234242
return action(tmppath)
235243
}
236244
}
245+
246+
// printStacks prints the maximum stack depth for functions that are started as
247+
// goroutines. Stack sizes cannot always be determined statically, in particular
248+
// recursive functions and functions that call interface methods or function
249+
// pointers may have an unknown stack depth (depending on what the optimizer
250+
// manages to optimize away).
251+
//
252+
// It might print something like the following:
253+
//
254+
// function stack usage (in bytes)
255+
// Reset_Handler 316
256+
// .Lexamples/blinky2.led1 92
257+
// .Lruntime.run$1 300
258+
func printStacks(mod llvm.Module, executable string) {
259+
// Determine which functions call a function pointer.
260+
var callsIndirectFunction []string
261+
for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) {
262+
for bb := fn.FirstBasicBlock(); !bb.IsNil(); bb = llvm.NextBasicBlock(bb) {
263+
for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) {
264+
if inst.IsACallInst().IsNil() {
265+
continue
266+
}
267+
if callee := inst.CalledValue(); callee.IsAFunction().IsNil() && callee.IsAInlineAsm().IsNil() {
268+
callsIndirectFunction = append(callsIndirectFunction, fn.Name())
269+
}
270+
}
271+
}
272+
}
273+
274+
// Load the ELF binary.
275+
f, err := elf.Open(executable)
276+
if err != nil {
277+
fmt.Fprintln(os.Stderr, "could not load executable for stack size analysis:", err)
278+
return
279+
}
280+
defer f.Close()
281+
282+
// Determine the frame size of each function (if available) and the callgraph.
283+
functions, err := stacksize.CallGraph(f, callsIndirectFunction)
284+
if err != nil {
285+
fmt.Fprintln(os.Stderr, "could not parse executable for stack size analysis:", err)
286+
return
287+
}
288+
289+
// Get a list of "go wrappers", small wrapper functions that decode
290+
// parameters when starting a new goroutine.
291+
var gowrappers []string
292+
for name := range functions {
293+
if strings.HasSuffix(name, "$gowrapper") {
294+
gowrappers = append(gowrappers, name)
295+
}
296+
}
297+
sort.Strings(gowrappers)
298+
299+
switch f.Machine {
300+
case elf.EM_ARM:
301+
// Add the reset handler, which runs startup code and is the
302+
// interrupt/scheduler stack with -scheduler=tasks.
303+
// Note that because interrupts happen on this stack, the stack needed
304+
// by just the Reset_Handler is not enough. Stacks needed by interrupt
305+
// handlers should also be taken into account.
306+
gowrappers = append([]string{"Reset_Handler"}, gowrappers...)
307+
}
308+
309+
// Print the sizes of all stacks.
310+
fmt.Printf("%-32s %s\n", "function", "stack usage (in bytes)")
311+
for _, name := range gowrappers {
312+
for _, fn := range functions[name] {
313+
stackSize, stackSizeType, missingStackSize := fn.StackSize()
314+
strippedName := name
315+
if strings.HasSuffix(name, "$gowrapper") {
316+
strippedName = name[:len(name)-len("$gowrapper")]
317+
}
318+
switch stackSizeType {
319+
case stacksize.Bounded:
320+
fmt.Printf("%-32s %d\n", strippedName, stackSize)
321+
case stacksize.Unknown:
322+
fmt.Printf("%-32s unknown, %s does not have stack frame information\n", strippedName, missingStackSize)
323+
case stacksize.Recursive:
324+
fmt.Printf("%-32s recursive, %s may call itself\n", strippedName, missingStackSize)
325+
case stacksize.IndirectCall:
326+
fmt.Printf("%-32s unknown, %s calls a function pointer\n", strippedName, missingStackSize)
327+
}
328+
}
329+
}
330+
}

compileopts/options.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type Options struct {
2525
VerifyIR bool
2626
Debug bool
2727
PrintSizes string
28+
PrintStacks bool
2829
CFlags []string
2930
LDFlags []string
3031
Tags string

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ func main() {
796796
tags := flag.String("tags", "", "a space-separated list of extra build tags")
797797
target := flag.String("target", "", "LLVM target | .json file with TargetSpec")
798798
printSize := flag.String("size", "", "print sizes (none, short, full)")
799+
printStacks := flag.Bool("print-stacks", false, "print stack sizes of goroutines")
799800
nodebug := flag.Bool("no-debug", false, "disable DWARF debug symbol generation")
800801
ocdOutput := flag.Bool("ocd-output", false, "print OCD daemon output during debug")
801802
port := flag.String("port", "", "flash port")
@@ -835,6 +836,7 @@ func main() {
835836
VerifyIR: *verifyIR,
836837
Debug: !*nodebug,
837838
PrintSizes: *printSize,
839+
PrintStacks: *printStacks,
838840
Tags: *tags,
839841
WasmAbi: *wasmAbi,
840842
Programmer: *programmer,

src/device/arm/cortexm.s

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ HardFault_Handler:
1919

2020
// Continue handling this error in Go.
2121
bl handleHardFault
22+
.size HardFault_Handler, .-HardFault_Handler
2223

2324
// This is a convenience function for semihosting support.
2425
// At some point, this should be replaced by inline assembly.

src/runtime/scheduler_cortexm.S

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ tinygo_startTask:
1818

1919
// After return, exit this goroutine. This is a tail call.
2020
bl tinygo_pause
21+
.size tinygo_startTask, .-tinygo_startTask
2122

2223
.section .text.tinygo_getSystemStackPointer
2324
.global tinygo_getSystemStackPointer
@@ -44,6 +45,7 @@ tinygo_switchToScheduler:
4445
str r1, [r0]
4546

4647
b tinygo_swapTask
48+
.size tinygo_switchToScheduler, .-tinygo_switchToScheduler
4749

4850
.global tinygo_switchToTask
4951
.type tinygo_switchToTask, %function
@@ -56,6 +58,7 @@ tinygo_switchToTask:
5658

5759
// Continue executing in the swapTask function, which swaps the stack
5860
// pointer.
61+
.size tinygo_switchToTask, .-tinygo_switchToTask
5962

6063
.global tinygo_swapTask
6164
.type tinygo_swapTask, %function
@@ -111,6 +114,7 @@ tinygo_swapTask:
111114
mov r11, r3
112115
pop {pc}
113116
#endif
117+
.size tinygo_swapTask, .-tinygo_swapTask
114118

115119
.section .text.tinygo_scanCurrentStack
116120
.global tinygo_scanCurrentStack
@@ -135,3 +139,4 @@ tinygo_scanCurrentStack:
135139
// Restore stack state and return.
136140
add sp, #32
137141
pop {pc}
142+
.size tinygo_scanCurrentStack, .-tinygo_scanCurrentStack

0 commit comments

Comments
 (0)