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.
This commit is contained in:
Ayke van Laethem
2021-01-09 01:11:36 +01:00
committed by Ron Evans
parent 5af4c073cd
commit da0161d6ab
10 changed files with 115 additions and 47 deletions
+20
View File
@@ -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
}
+7
View File
@@ -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)
+54 -4
View File
@@ -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")
}
}
}
+13 -1
View File
@@ -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
}
+4
View File
@@ -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.
}
+7
View File
@@ -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 {
+9
View File
@@ -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
}