-
Notifications
You must be signed in to change notification settings - Fork 570
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).