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.
Этот коммит содержится в:
Ayke van Laethem 2021-01-09 01:11:36 +01:00 коммит произвёл Ron Evans
родитель 5af4c073cd
коммит da0161d6ab
10 изменённых файлов: 115 добавлений и 47 удалений

Просмотреть файл

@ -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)
}

Просмотреть файл

@ -30,7 +30,6 @@ type Options struct {
LDFlags []string
Tags string
WasmAbi string
HeapSize int64
TestConfig TestConfig
Programmer string
}

35
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()

Просмотреть файл

@ -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
}

Просмотреть файл

@ -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)

Просмотреть файл

@ -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")
}
}
}

Просмотреть файл

@ -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
}

Просмотреть файл

@ -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.
}

Просмотреть файл

@ -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 {

Просмотреть файл

@ -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
}