From da0161d6ab29e3132153a30b10d4d12da9d9c8c6 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sat, 9 Jan 2021 01:11:36 +0100 Subject: [PATCH] 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. --- compileopts/config.go | 7 ---- compileopts/options.go | 1 - main.go | 35 +--------------- src/runtime/arch_wasm.go | 20 +++++++++ src/runtime/baremetal.go | 7 ++++ src/runtime/gc_conservative.go | 58 +++++++++++++++++++++++++-- src/runtime/gc_leaking.go | 14 ++++++- src/runtime/gc_none.go | 4 ++ src/runtime/runtime_nintendoswitch.go | 7 ++++ src/runtime/runtime_unix_heap.go | 9 +++++ 10 files changed, 115 insertions(+), 47 deletions(-) diff --git a/compileopts/config.go b/compileopts/config.go index 2803d164..e642b1f8 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -7,7 +7,6 @@ import ( "fmt" "path/filepath" "regexp" - "strconv" "strings" "github.com/tinygo-org/tinygo/goenv" @@ -203,12 +202,6 @@ func (c *Config) LDFlags() []string { ldflags = append(ldflags, strings.Replace(flag, "{root}", root, -1)) } ldflags = append(ldflags, "-L", root) - if c.Target.GOARCH == "wasm" { - // Round heap size to next multiple of 65536 (the WebAssembly page - // size). - heapSize := (c.Options.HeapSize + (65536 - 1)) &^ (65536 - 1) - ldflags = append(ldflags, "--initial-memory="+strconv.FormatInt(heapSize, 10)) - } if c.Target.LinkerScript != "" { ldflags = append(ldflags, "-T", c.Target.LinkerScript) } diff --git a/compileopts/options.go b/compileopts/options.go index 7948b71c..b5526b78 100644 --- a/compileopts/options.go +++ b/compileopts/options.go @@ -30,7 +30,6 @@ type Options struct { LDFlags []string Tags string WasmAbi string - HeapSize int64 TestConfig TestConfig Programmer string } diff --git a/main.go b/main.go index c562dc5e..84bf3f05 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,6 @@ import ( "os/signal" "path/filepath" "runtime" - "strconv" "strings" "syscall" "time" @@ -650,30 +649,6 @@ func windowsFindUSBDrive(volume string) (string, error) { return "", errors.New("unable to locate a USB device to be flashed") } -// parseSize converts a human-readable size (with k/m/g suffix) into a plain -// number. -func parseSize(s string) (int64, error) { - s = strings.ToLower(strings.TrimSpace(s)) - if len(s) == 0 { - return 0, errors.New("no size provided") - } - multiply := int64(1) - switch s[len(s)-1] { - case 'k': - multiply = 1 << 10 - case 'm': - multiply = 1 << 20 - case 'g': - multiply = 1 << 30 - } - if multiply != 1 { - s = s[:len(s)-1] - } - n, err := strconv.ParseInt(s, 0, 64) - n *= multiply - return n, err -} - // getDefaultPort returns the default serial port depending on the operating system. func getDefaultPort() (port string, err error) { var portPath string @@ -839,7 +814,6 @@ func main() { cFlags := flag.String("cflags", "", "additional cflags for compiler") ldFlags := flag.String("ldflags", "", "additional ldflags for linker") wasmAbi := flag.String("wasm-abi", "", "WebAssembly ABI conventions: js (no i64 params) or generic") - heapSize := flag.String("heap-size", "1M", "default heap size in bytes (only supported by WebAssembly)") var flagJSON, flagDeps *bool if command == "help" || command == "list" { @@ -893,16 +867,9 @@ func main() { options.LDFlags = strings.Split(*ldFlags, " ") } - var err error - if options.HeapSize, err = parseSize(*heapSize); err != nil { - fmt.Fprintln(os.Stderr, "Could not read heap size:", *heapSize) - usage() - os.Exit(1) - } - os.Setenv("CC", "clang -target="+*target) - err = options.Verify() + err := options.Verify() if err != nil { fmt.Fprintln(os.Stderr, err.Error()) usage() diff --git a/src/runtime/arch_wasm.go b/src/runtime/arch_wasm.go index 1297d102..cf123fa9 100644 --- a/src/runtime/arch_wasm.go +++ b/src/runtime/arch_wasm.go @@ -17,6 +17,9 @@ var heapStartSymbol [0]byte //export llvm.wasm.memory.size.i32 func wasm_memory_size(index int32) int32 +//export llvm.wasm.memory.grow.i32 +func wasm_memory_grow(index int32, delta int32) int32 + var ( heapStart = uintptr(unsafe.Pointer(&heapStartSymbol)) heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize) @@ -30,3 +33,20 @@ func align(ptr uintptr) uintptr { } func getCurrentStackPointer() uintptr + +// growHeap tries to grow the heap size. It returns true if it succeeds, false +// otherwise. +func growHeap() bool { + // Grow memory by the available size, which means the heap size is doubled. + memorySize := wasm_memory_size(0) + result := wasm_memory_grow(0, memorySize) + if result == -1 { + // Grow failed. + return false + } + + setHeapEnd(uintptr(wasm_memory_size(0) * wasmPageSize)) + + // Heap has grown successfully. + return true +} diff --git a/src/runtime/baremetal.go b/src/runtime/baremetal.go index f51cad2c..ce69e338 100644 --- a/src/runtime/baremetal.go +++ b/src/runtime/baremetal.go @@ -29,6 +29,13 @@ var ( stackTop = uintptr(unsafe.Pointer(&stackTopSymbol)) ) +// growHeap tries to grow the heap size. It returns true if it succeeds, false +// otherwise. +func growHeap() bool { + // On baremetal, there is no way the heap can be grown. + return false +} + //export malloc func libc_malloc(size uintptr) unsafe.Pointer { return alloc(size) diff --git a/src/runtime/gc_conservative.go b/src/runtime/gc_conservative.go index 52c6e9b5..ac9e308b 100644 --- a/src/runtime/gc_conservative.go +++ b/src/runtime/gc_conservative.go @@ -184,6 +184,50 @@ func (b gcBlock) unmark() { // any packages the runtime depends upon may not allocate memory during package // initialization. func initHeap() { + calculateHeapAddresses() + + // Set all block states to 'free'. + metadataSize := heapEnd - uintptr(metadataStart) + memzero(unsafe.Pointer(metadataStart), metadataSize) +} + +// setHeapEnd is called to expand the heap. The heap can only grow, not shrink. +// Also, the heap should grow substantially each time otherwise growing the heap +// will be expensive. +func setHeapEnd(newHeapEnd uintptr) { + if gcAsserts && newHeapEnd <= heapEnd { + panic("gc: setHeapEnd didn't grow the heap") + } + + // Save some old variables we need later. + oldMetadataStart := metadataStart + oldMetadataSize := heapEnd - uintptr(metadataStart) + + // Increase the heap. After setting the new heapEnd, calculateHeapAddresses + // will update metadataStart and the memcpy will copy the metadata to the + // new location. + // The new metadata will be bigger than the old metadata, but a simple + // memcpy is fine as it only copies the old metadata and the new memory will + // have been zero initialized. + heapEnd = newHeapEnd + calculateHeapAddresses() + memcpy(metadataStart, oldMetadataStart, oldMetadataSize) + + // Note: the memcpy above assumes the heap grows enough so that the new + // metadata does not overlap the old metadata. If that isn't true, memmove + // should be used to avoid corruption. + // This assert checks whether that's true. + if gcAsserts && uintptr(metadataStart) < uintptr(oldMetadataStart)+oldMetadataSize { + panic("gc: heap did not grow enough at once") + } +} + +// calculateHeapAddresses initializes variables such as metadataStart and +// numBlock based on heapStart and heapEnd. +// +// This function can be called again when the heap size increases. The caller is +// responsible for copying the metadata to the new location. +func calculateHeapAddresses() { totalSize := heapEnd - heapStart // Allocate some memory to keep 2 bits of information about every block. @@ -206,9 +250,6 @@ func initHeap() { // sanity check runtimePanic("gc: metadata array is too small") } - - // Set all block states to 'free'. - memzero(metadataStart, metadataSize) } // alloc tries to find some free space on the heap, possibly doing a garbage @@ -238,7 +279,16 @@ func alloc(size uintptr) unsafe.Pointer { GC() } else { // Even after garbage collection, no free memory could be found. - runtimePanic("out of memory") + // Try to increase heap size. + if growHeap() { + // Success, the heap was increased in size. Try again with a + // larger heap. + } else { + // Unfortunately the heap could not be increased. This + // happens on baremetal systems for example (where all + // available RAM has already been dedicated to the heap). + runtimePanic("out of memory") + } } } diff --git a/src/runtime/gc_leaking.go b/src/runtime/gc_leaking.go index f5e35951..bbded072 100644 --- a/src/runtime/gc_leaking.go +++ b/src/runtime/gc_leaking.go @@ -20,7 +20,12 @@ func alloc(size uintptr) unsafe.Pointer { size = align(size) addr := heapptr heapptr += size - if heapptr >= heapEnd { + for heapptr >= heapEnd { + // Try to increase the heap and check again. + if growHeap() { + continue + } + // Failed to make the heap bigger, so we must really be out of memory. runtimePanic("out of memory") } for i := uintptr(0); i < uintptr(size); i += 4 { @@ -49,3 +54,10 @@ func SetFinalizer(obj interface{}, finalizer interface{}) { func initHeap() { // Nothing to initialize. } + +// setHeapEnd sets a new (larger) heapEnd pointer. +func setHeapEnd(newHeapEnd uintptr) { + // This "heap" is so simple that simply assigning a new value is good + // enough. + heapEnd = newHeapEnd +} diff --git a/src/runtime/gc_none.go b/src/runtime/gc_none.go index c69d2a24..6528261b 100644 --- a/src/runtime/gc_none.go +++ b/src/runtime/gc_none.go @@ -31,3 +31,7 @@ func SetFinalizer(obj interface{}, finalizer interface{}) { func initHeap() { // Nothing to initialize. } + +func setHeapEnd(newHeapEnd uintptr) { + // Nothing to do here, this function is never actually called. +} diff --git a/src/runtime/runtime_nintendoswitch.go b/src/runtime/runtime_nintendoswitch.go index 6930e371..e5120d42 100644 --- a/src/runtime/runtime_nintendoswitch.go +++ b/src/runtime/runtime_nintendoswitch.go @@ -217,6 +217,13 @@ func setupHeap() { } } +// growHeap tries to grow the heap size. It returns true if it succeeds, false +// otherwise. +func growHeap() bool { + // Growing the heap is unimplemented. + return false +} + // getHeapBase returns the start address of the heap // this is externally linked by gonx func getHeapBase() uintptr { diff --git a/src/runtime/runtime_unix_heap.go b/src/runtime/runtime_unix_heap.go index 1aefd2f6..98ad8075 100644 --- a/src/runtime/runtime_unix_heap.go +++ b/src/runtime/runtime_unix_heap.go @@ -13,3 +13,12 @@ func preinit() { heapStart = uintptr(malloc(heapSize)) heapEnd = heapStart + heapSize } + +// growHeap tries to grow the heap size. It returns true if it succeeds, false +// otherwise. +func growHeap() bool { + // At the moment, this is not possible. However it shouldn't be too + // difficult (at least on Linux) to allocate a large amount of virtual + // memory at startup that is then slowly used. + return false +}