Thanks to visit codestin.com
Credit goes to github.com

Skip to content

len and range do not scale well for map types. #1135

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
tomconnell-wf opened this issue Aug 4, 2022 · 3 comments
Closed

len and range do not scale well for map types. #1135

tomconnell-wf opened this issue Aug 4, 2022 · 3 comments

Comments

@tomconnell-wf
Copy link
Contributor

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).
@tomconnell-wf
Copy link
Contributor Author

Considering #1121, using ES 2015 Map should be a valid path forward.

@nevkontakte
Copy link
Member

Yes, using ES 2015 Map type is likely the best solution, and I would venture a guess that it can improve performance overall. I see you already started prototyping in #1136, I'm looking forwards to seeing the results.

reflect and reflectlite packages are some of the more arcane corners of GopherJS and I can't say I understand it fully, but if you have questions feel free to ask, and we will try to help you :)

@tomconnell-wf
Copy link
Contributor Author

#1136 Addressed the speed of len.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants