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.
Этот коммит содержится в:
родитель
5af4c073cd
коммит
da0161d6ab
10 изменённых файлов: 115 добавлений и 47 удалений
|
@ -7,7 +7,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tinygo-org/tinygo/goenv"
|
"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, strings.Replace(flag, "{root}", root, -1))
|
||||||
}
|
}
|
||||||
ldflags = append(ldflags, "-L", root)
|
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 != "" {
|
if c.Target.LinkerScript != "" {
|
||||||
ldflags = append(ldflags, "-T", c.Target.LinkerScript)
|
ldflags = append(ldflags, "-T", c.Target.LinkerScript)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,6 @@ type Options struct {
|
||||||
LDFlags []string
|
LDFlags []string
|
||||||
Tags string
|
Tags string
|
||||||
WasmAbi string
|
WasmAbi string
|
||||||
HeapSize int64
|
|
||||||
TestConfig TestConfig
|
TestConfig TestConfig
|
||||||
Programmer string
|
Programmer string
|
||||||
}
|
}
|
||||||
|
|
35
main.go
35
main.go
|
@ -14,7 +14,6 @@ import (
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
@ -650,30 +649,6 @@ func windowsFindUSBDrive(volume string) (string, error) {
|
||||||
return "", errors.New("unable to locate a USB device to be flashed")
|
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.
|
// getDefaultPort returns the default serial port depending on the operating system.
|
||||||
func getDefaultPort() (port string, err error) {
|
func getDefaultPort() (port string, err error) {
|
||||||
var portPath string
|
var portPath string
|
||||||
|
@ -839,7 +814,6 @@ func main() {
|
||||||
cFlags := flag.String("cflags", "", "additional cflags for compiler")
|
cFlags := flag.String("cflags", "", "additional cflags for compiler")
|
||||||
ldFlags := flag.String("ldflags", "", "additional ldflags for linker")
|
ldFlags := flag.String("ldflags", "", "additional ldflags for linker")
|
||||||
wasmAbi := flag.String("wasm-abi", "", "WebAssembly ABI conventions: js (no i64 params) or generic")
|
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
|
var flagJSON, flagDeps *bool
|
||||||
if command == "help" || command == "list" {
|
if command == "help" || command == "list" {
|
||||||
|
@ -893,16 +867,9 @@ func main() {
|
||||||
options.LDFlags = strings.Split(*ldFlags, " ")
|
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)
|
os.Setenv("CC", "clang -target="+*target)
|
||||||
|
|
||||||
err = options.Verify()
|
err := options.Verify()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err.Error())
|
fmt.Fprintln(os.Stderr, err.Error())
|
||||||
usage()
|
usage()
|
||||||
|
|
|
@ -17,6 +17,9 @@ var heapStartSymbol [0]byte
|
||||||
//export llvm.wasm.memory.size.i32
|
//export llvm.wasm.memory.size.i32
|
||||||
func wasm_memory_size(index int32) int32
|
func wasm_memory_size(index int32) int32
|
||||||
|
|
||||||
|
//export llvm.wasm.memory.grow.i32
|
||||||
|
func wasm_memory_grow(index int32, delta int32) int32
|
||||||
|
|
||||||
var (
|
var (
|
||||||
heapStart = uintptr(unsafe.Pointer(&heapStartSymbol))
|
heapStart = uintptr(unsafe.Pointer(&heapStartSymbol))
|
||||||
heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize)
|
heapEnd = uintptr(wasm_memory_size(0) * wasmPageSize)
|
||||||
|
@ -30,3 +33,20 @@ func align(ptr uintptr) uintptr {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCurrentStackPointer() 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))
|
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
|
//export malloc
|
||||||
func libc_malloc(size uintptr) unsafe.Pointer {
|
func libc_malloc(size uintptr) unsafe.Pointer {
|
||||||
return alloc(size)
|
return alloc(size)
|
||||||
|
|
|
@ -184,6 +184,50 @@ func (b gcBlock) unmark() {
|
||||||
// any packages the runtime depends upon may not allocate memory during package
|
// any packages the runtime depends upon may not allocate memory during package
|
||||||
// initialization.
|
// initialization.
|
||||||
func initHeap() {
|
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
|
totalSize := heapEnd - heapStart
|
||||||
|
|
||||||
// Allocate some memory to keep 2 bits of information about every block.
|
// Allocate some memory to keep 2 bits of information about every block.
|
||||||
|
@ -206,9 +250,6 @@ func initHeap() {
|
||||||
// sanity check
|
// sanity check
|
||||||
runtimePanic("gc: metadata array is too small")
|
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
|
// 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()
|
GC()
|
||||||
} else {
|
} else {
|
||||||
// Even after garbage collection, no free memory could be found.
|
// 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)
|
size = align(size)
|
||||||
addr := heapptr
|
addr := heapptr
|
||||||
heapptr += size
|
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")
|
runtimePanic("out of memory")
|
||||||
}
|
}
|
||||||
for i := uintptr(0); i < uintptr(size); i += 4 {
|
for i := uintptr(0); i < uintptr(size); i += 4 {
|
||||||
|
@ -49,3 +54,10 @@ func SetFinalizer(obj interface{}, finalizer interface{}) {
|
||||||
func initHeap() {
|
func initHeap() {
|
||||||
// Nothing to initialize.
|
// 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() {
|
func initHeap() {
|
||||||
// Nothing to initialize.
|
// 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
|
// getHeapBase returns the start address of the heap
|
||||||
// this is externally linked by gonx
|
// this is externally linked by gonx
|
||||||
func getHeapBase() uintptr {
|
func getHeapBase() uintptr {
|
||||||
|
|
|
@ -13,3 +13,12 @@ func preinit() {
|
||||||
heapStart = uintptr(malloc(heapSize))
|
heapStart = uintptr(malloc(heapSize))
|
||||||
heapEnd = heapStart + 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
|
||||||
|
}
|
||||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче