-
Notifications
You must be signed in to change notification settings - Fork 569
Description
I'm using go 1.17.3 and gopherjs 1.17.1+go1.17.3
I have noticed that calling len()
or range
on a map is implemented in JS to call length on $keys
.
var $keys = function(m) { return m ? Object.keys(m) : []; };
This appears to scale linearly as a map grows in size.
Consider the following benchmarks:
import "testing"
func makeMap(size int) map[int]string {
myMap := make(map[int]string, size)
for i := 0; i < size; i++ {
myMap[i] = `data`
}
return myMap
}
func makeInt(size int) []int {
slice := make([]int, size)
for i := 0; i < size; i++ {
slice[i] = i
}
return slice
}
func BenchmarkSliceLen(b *testing.B) {
slice := makeInt(1000000)
for i := 0; i < b.N; i++ {
if len(slice) > 0 {
}
}
}
func BenchmarkMapLen(b *testing.B) {
myMap := makeMap(1000000)
for i := 0; i < b.N; i++ {
if len(myMap) > 0 {
}
}
}
func BenchmarkMapNilCheck(b *testing.B) {
myMap := makeMap(1000000)
for i := 0; i < b.N; i++ {
if myMap != nil {
}
}
}
func BenchmarkMapNilElementCheck(b *testing.B) {
myMap := makeMap(1000000)
for i := 0; i < b.N; i++ {
if myMap[0] != `` {
}
}
}
In Go:
✗ go test ./... -benchmem -bench "^Benchmark.*"
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkSliceLen-12 1000000000 0.2519 ns/op 0 B/op 0 allocs/op
BenchmarkMapLen-12 1000000000 0.5852 ns/op 0 B/op 0 allocs/op
BenchmarkMapNilCheck-12 1000000000 0.5832 ns/op 0 B/op 0 allocs/op
BenchmarkMapNilElementCheck-12 162799234 6.268 ns/op 0 B/op 0 allocs/op
PASS
In Gopherjs:
✗ GOOS=linux gopherjs test ./... --bench="Benchmark.*"
goos: linux
goarch: js
BenchmarkSliceLen 1000000000 0.2540 ns/op
BenchmarkMapLen 8 154125000 ns/op
BenchmarkMapNilCheck 1000000000 0.6020 ns/op
BenchmarkMapNilElementCheck 115060722 9.943 ns/op
PASS
So, the map size of 1000000 may be considered ridiculously big, so, I'll back off to 1000.
goos: linux
goarch: js
BenchmarkSliceLen 1000000000 0.2520 ns/op
BenchmarkMapLen 97560 11583 ns/op
BenchmarkMapNilCheck 1000000000 0.5020 ns/op
BenchmarkMapNilElementCheck 145278450 8.281 ns/op
PASS
The main point is that len is so fast in Go I would consider it "free", and would never blink an eye using it. In GopherJS, it can have dire consequences on the performance of code as a map grows in size. I find many of our uses of len look like if len(myMap) > 0 { doStuff}
. Even if I change those to if myMap ~= nil
, at some point iteration may happen, and then the cost must be paid. I believe it is unavoidable to use range to iterate a map.
I'm not entirely sure how to approach this, but it seems like implementing go maps as a javascript map could be a way to go, instead of a plain old javascript object. I thought briefly about implementing bookkeeping when adding or deleting an element to the map, but that would only solve for the len()
case and leave range
.
I'm not sure if there are other cases to handle (it looks like $keys is used a few places in reflect.go
and reflectlite.go
, but I don't understand that code right now.
My desires:
- Make it fast to determine the length of a map, regardless of size.
- Make it fast to get the keys of a map (range).