Description
What version of Go are you using (go version
)?
$ go version go version go1.14.4 linux/amd64
Does this issue reproduce with the latest release?
Yes, in go1.15beta1
What did you do?
I used js.Call
, js.New
, and js.Invoke
with multiple arguments, hundreds of times per frame, 60 frames per second in order to implement my WebGL game.
What did you expect to see?
The above functions are absolutely essential for getting work done in js/wasm, as they are analogous to a simple function call. They should be optimized as such. No excess go heap memory should be allocated, no extra go garbage collection. After implementing a hack to fix the issue I will go on to describe (see below), here is a CPU profile of memory allocation:
Importantly, memory allocation is 8% of the total time and does not include makeArgs
allocating any slices that end up on the heap
What did you see instead?
2+ new slices allocated on the heap per call to one of the above three functions, as the first few lines of the makeArgs function...
Lines 361 to 363 in 60f7876
...possibly are assumed to escape to the heap via...
Lines 405 to 406 in 60f7876
...or
valueNew
/valueInvoke
, or if go's escape analysis optimization doesn't work on the caller's stack.
A CPU profile shows the significant penalty associated with allocating too many slices (mallocgc now accounts for over 20%), and that doesn't even include the extra garbage collector load that claims a few FPS.
I thought of adding //go:noescape
before each of the above and potentially other functions implemented in javascript, but I didn't get any improvement right away. Only my hack to makeArgs
worked consistently:
const maxArgs = 9 // A better hack/fix would grow the slices automatically
var argVals = make([]Value, 0, maxArgs)
var argRefs = make([]ref, 0, maxArgs)
func makeArgs(args []interface{}) ([]Value, []ref) {
for i, _ := range argVals {
argVals[i] = Value{}
}
for i, _ := range argRefs { // I don't think this is strictly necessary
argRefs[i] = 0
}
argVals = argVals[:len(args)]
argRefs = argRefs[:len(args)]
for i, arg := range args {
v := ValueOf(arg)
argVals[i] = v
argRefs[i] = v.ref
}
return argVals, argRefs
}
Another potential solution, building on the above hack, would be to make makeArgs
take slices, to put the values/refs in, as arguments (instead of returning new slices). The caller could deal with efficiently handling the memory. Maybe this could help go's escape analysis, in conjunction with //go:noescape
.
Side note: Ideally, a solution would address the heap allocation of an interface for certain arguments. Hundreds of thousands get allocated before the garbage collector runs, causing the garbage collector to take 12-20ms.