Skip to content

Commit da0161d

Browse files
aykevldeadprogram
authored andcommitted
wasm: implement a growable heap
On WebAssembly it is possible to grow the heap with the memory.grow instruction. This commit implements this feature and with that also removes the -heap-size flag that was reportedly broken (I haven't verified that). This should make it easier to use TinyGo for WebAssembly, where there was no good reason to use a fixed heap size. This commit has no effect on baremetal targets with optimizations enabled.
1 parent 5af4c07 commit da0161d

File tree

10 files changed

+115
-47
lines changed

10 files changed

+115
-47
lines changed

compileopts/config.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"fmt"
88
"path/filepath"
99
"regexp"
10-
"strconv"
1110
"strings"
1211

1312
"github.com/tinygo-org/tinygo/goenv"
@@ -203,12 +202,6 @@ func (c *Config) LDFlags() []string {
203202
ldflags = append(ldflags, strings.Replace(flag, "{root}", root, -1))
204203
}
205204
ldflags = append(ldflags, "-L", root)
206-
if c.Target.GOARCH == "wasm" {
207-
// Round heap size to next multiple of 65536 (the WebAssembly page
208-
// size).
209-
heapSize := (c.Options.HeapSize + (65536 - 1)) &^ (65536 - 1)
210-
ldflags = append(ldflags, "--initial-memory="+strconv.FormatInt(heapSize, 10))
211-
}
212205
if c.Target.LinkerScript != "" {
213206
ldflags = append(ldflags, "-T", c.Target.LinkerScript)
214207
}

compileopts/options.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ type Options struct {
3030
LDFlags []string
3131
Tags string
3232
WasmAbi string
33-
HeapSize int64
3433
TestConfig TestConfig
3534
Programmer string
3635
}

main.go

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"os/signal"
1515
"path/filepath"
1616
"runtime"
17-
"strconv"
1817
"strings"
1918
"syscall"
2019
"time"
@@ -650,30 +649,6 @@ func windowsFindUSBDrive(volume string) (string, error) {
650649
return "", errors.New("unable to locate a USB device to be flashed")
651650
}
652651

653-
// parseSize converts a human-readable size (with k/m/g suffix) into a plain
654-
// number.
655-
func parseSize(s string) (int64, error) {
656-
s = strings.ToLower(strings.TrimSpace(s))
657-
if len(s) == 0 {
658-
return 0, errors.New("no size provided")
659-
}
660-
multiply := int64(1)
661-
switch s[len(s)-1] {
662-
case 'k':
663-
multiply = 1 << 10
664-
case 'm':
665-
multiply = 1 << 20
666-
case 'g':
667-
multiply = 1 << 30
668-
}
669-
if multiply != 1 {
670-
s = s[:len(s)-1]
671-
}
672-
n, err := strconv.ParseInt(s, 0, 64)
673-
n *= multiply
674-
return n, err
675-
}
676-
677652
// getDefaultPort returns the default serial port depending on the operating system.
678653
func getDefaultPort() (port string, err error) {
679654
var portPath string
@@ -839,7 +814,6 @@ func main() {
839814
cFlags := flag.String("cflags", "", "additional cflags for compiler")
840815
ldFlags := flag.String("ldflags", "", "additional ldflags for linker")
841816
wasmAbi := flag.String("wasm-abi", "", "WebAssembly ABI conventions: js (no i64 params) or generic")
842-
heapSize := flag.String("heap-size", "1M", "default heap size in bytes (only supported by WebAssembly)")
843817

844818
var flagJSON, flagDeps *bool
845819
if command == "help" || command == "list" {
@@ -893,16 +867,9 @@ func main() {
893867
options.LDFlags = strings.Split(*ldFlags, " ")
894868
}
895869

896-
var err error
897-
if options.HeapSize, err = parseSize(*heapSize); err != nil {
898-
fmt.Fprintln(os.Stderr, "Could not read heap size:", *heapSize)
899-
usage()
900-
os.Exit(1)
901-
}
902-
903870
os.Setenv("CC", "clang -target="+*target)
904871

905-
err = options.Verify()
872+
err := options.Verify()
906873
if err != nil {
907874
fmt.Fprintln(os.Stderr, err.Error())
908875
usage()

src/runtime/arch_wasm.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ var heapStartSymbol [0]byte
1717
//export llvm.wasm.memory.size.i32
1818
func wasm_memory_size(index int32) int32
1919

20+
//export llvm.wasm.memory.grow.i32
21+
func wasm_memory_grow(index int32, delta int32) int32
22+
2023
var (
2124
heapStart = uintptr(unsafe.Pointer(&heapStartSymbol))
2225
heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize)
@@ -30,3 +33,20 @@ func align(ptr uintptr) uintptr {
3033
}
3134

3235
func getCurrentStackPointer() uintptr
36+
37+
// growHeap tries to grow the heap size. It returns true if it succeeds, false
38+
// otherwise.
39+
func growHeap() bool {
40+
// Grow memory by the available size, which means the heap size is doubled.
41+
memorySize := wasm_memory_size(0)
42+
result := wasm_memory_grow(0, memorySize)
43+
if result == -1 {
44+
// Grow failed.
45+
return false
46+
}
47+
48+
setHeapEnd(uintptr(wasm_memory_size(0) * wasmPageSize))
49+
50+
// Heap has grown successfully.
51+
return true
52+
}

src/runtime/baremetal.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ var (
2929
stackTop = uintptr(unsafe.Pointer(&stackTopSymbol))
3030
)
3131

32+
// growHeap tries to grow the heap size. It returns true if it succeeds, false
33+
// otherwise.
34+
func growHeap() bool {
35+
// On baremetal, there is no way the heap can be grown.
36+
return false
37+
}
38+
3239
//export malloc
3340
func libc_malloc(size uintptr) unsafe.Pointer {
3441
return alloc(size)

src/runtime/gc_conservative.go

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,50 @@ func (b gcBlock) unmark() {
184184
// any packages the runtime depends upon may not allocate memory during package
185185
// initialization.
186186
func initHeap() {
187+
calculateHeapAddresses()
188+
189+
// Set all block states to 'free'.
190+
metadataSize := heapEnd - uintptr(metadataStart)
191+
memzero(unsafe.Pointer(metadataStart), metadataSize)
192+
}
193+
194+
// setHeapEnd is called to expand the heap. The heap can only grow, not shrink.
195+
// Also, the heap should grow substantially each time otherwise growing the heap
196+
// will be expensive.
197+
func setHeapEnd(newHeapEnd uintptr) {
198+
if gcAsserts && newHeapEnd <= heapEnd {
199+
panic("gc: setHeapEnd didn't grow the heap")
200+
}
201+
202+
// Save some old variables we need later.
203+
oldMetadataStart := metadataStart
204+
oldMetadataSize := heapEnd - uintptr(metadataStart)
205+
206+
// Increase the heap. After setting the new heapEnd, calculateHeapAddresses
207+
// will update metadataStart and the memcpy will copy the metadata to the
208+
// new location.
209+
// The new metadata will be bigger than the old metadata, but a simple
210+
// memcpy is fine as it only copies the old metadata and the new memory will
211+
// have been zero initialized.
212+
heapEnd = newHeapEnd
213+
calculateHeapAddresses()
214+
memcpy(metadataStart, oldMetadataStart, oldMetadataSize)
215+
216+
// Note: the memcpy above assumes the heap grows enough so that the new
217+
// metadata does not overlap the old metadata. If that isn't true, memmove
218+
// should be used to avoid corruption.
219+
// This assert checks whether that's true.
220+
if gcAsserts && uintptr(metadataStart) < uintptr(oldMetadataStart)+oldMetadataSize {
221+
panic("gc: heap did not grow enough at once")
222+
}
223+
}
224+
225+
// calculateHeapAddresses initializes variables such as metadataStart and
226+
// numBlock based on heapStart and heapEnd.
227+
//
228+
// This function can be called again when the heap size increases. The caller is
229+
// responsible for copying the metadata to the new location.
230+
func calculateHeapAddresses() {
187231
totalSize := heapEnd - heapStart
188232

189233
// Allocate some memory to keep 2 bits of information about every block.
@@ -206,9 +250,6 @@ func initHeap() {
206250
// sanity check
207251
runtimePanic("gc: metadata array is too small")
208252
}
209-
210-
// Set all block states to 'free'.
211-
memzero(metadataStart, metadataSize)
212253
}
213254

214255
// alloc tries to find some free space on the heap, possibly doing a garbage
@@ -238,7 +279,16 @@ func alloc(size uintptr) unsafe.Pointer {
238279
GC()
239280
} else {
240281
// Even after garbage collection, no free memory could be found.
241-
runtimePanic("out of memory")
282+
// Try to increase heap size.
283+
if growHeap() {
284+
// Success, the heap was increased in size. Try again with a
285+
// larger heap.
286+
} else {
287+
// Unfortunately the heap could not be increased. This
288+
// happens on baremetal systems for example (where all
289+
// available RAM has already been dedicated to the heap).
290+
runtimePanic("out of memory")
291+
}
242292
}
243293
}
244294

src/runtime/gc_leaking.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ func alloc(size uintptr) unsafe.Pointer {
2020
size = align(size)
2121
addr := heapptr
2222
heapptr += size
23-
if heapptr >= heapEnd {
23+
for heapptr >= heapEnd {
24+
// Try to increase the heap and check again.
25+
if growHeap() {
26+
continue
27+
}
28+
// Failed to make the heap bigger, so we must really be out of memory.
2429
runtimePanic("out of memory")
2530
}
2631
for i := uintptr(0); i < uintptr(size); i += 4 {
@@ -49,3 +54,10 @@ func SetFinalizer(obj interface{}, finalizer interface{}) {
4954
func initHeap() {
5055
// Nothing to initialize.
5156
}
57+
58+
// setHeapEnd sets a new (larger) heapEnd pointer.
59+
func setHeapEnd(newHeapEnd uintptr) {
60+
// This "heap" is so simple that simply assigning a new value is good
61+
// enough.
62+
heapEnd = newHeapEnd
63+
}

src/runtime/gc_none.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,7 @@ func SetFinalizer(obj interface{}, finalizer interface{}) {
3131
func initHeap() {
3232
// Nothing to initialize.
3333
}
34+
35+
func setHeapEnd(newHeapEnd uintptr) {
36+
// Nothing to do here, this function is never actually called.
37+
}

src/runtime/runtime_nintendoswitch.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,13 @@ func setupHeap() {
217217
}
218218
}
219219

220+
// growHeap tries to grow the heap size. It returns true if it succeeds, false
221+
// otherwise.
222+
func growHeap() bool {
223+
// Growing the heap is unimplemented.
224+
return false
225+
}
226+
220227
// getHeapBase returns the start address of the heap
221228
// this is externally linked by gonx
222229
func getHeapBase() uintptr {

src/runtime/runtime_unix_heap.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,12 @@ func preinit() {
1313
heapStart = uintptr(malloc(heapSize))
1414
heapEnd = heapStart + heapSize
1515
}
16+
17+
// growHeap tries to grow the heap size. It returns true if it succeeds, false
18+
// otherwise.
19+
func growHeap() bool {
20+
// At the moment, this is not possible. However it shouldn't be too
21+
// difficult (at least on Linux) to allocate a large amount of virtual
22+
// memory at startup that is then slowly used.
23+
return false
24+
}

0 commit comments

Comments
 (0)