-
Notifications
You must be signed in to change notification settings - Fork 570
Use a Javascript Map for Go Maps. Improves performance of len() calls by orders of magnitude. 🗺️ #1136
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
Use a Javascript Map for Go Maps. Improves performance of len() calls by orders of magnitude. 🗺️ #1136
Conversation
At this point, maps cannot be accessed by index, but their size can be acquired, and they can be iterated. Using the same benchmark I used to report the slowness (with 1000 entries):
The len check is as quick as I would hope. (The setup of the map is very slow, and could possibly be improved (Because I'm making a slice of maps that then get converted in $makeMap, but I don't think that is any different than master.) |
FWIW, this is with 1 million elements
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this! Looks quite promising. I look forward to seeing the rest of the implementation.
Well, I think I am almost there. It looks like I have a Sprintf problem to fix, and then done? |
As you can see in CI, there are some fmt stdlib tests that are failing. It seems like it is because I didn't wire something up to reflect/reflectlite properly. Here's one of the tests that are failing. The expected output is
The incorrect output is
I'm a bit out of my depth here, but it seems like this is because the keys and values aren't reflecting properly. My gut says it has something to do with the change from e to e.v in https://github.com/gopherjs/gopherjs/pull/1136/files#diff-70e5ca5fb5a75cb3a234c5a2caee3036426e4ffbb46a9e6b2ee556cb244eccbcR623. I'll keep taking cracks at it, but if the maintainers have a hunch, I could use the guidance. |
Or maybe it has something to do with |
I'm going with the theory that by doing this I clobbered the information needed for reflection to work properly. I'll massage it so that we can use the Map as a POJSO, with the reflection type info...I think. |
@tomconnell-wf I've looked through the diff and there is nothing obviously incorrect in it I could pin the test failure on. My best guess is that somewhere in the As a side note, I recommend you rebasing on top of the latest master, which includes Go 1.18 support. There have been some additions to the |
We can go one step further, and put it in a subdir so that its in its own package that is never even parsed during compilation (but is parsed by |
This may be considered phoning it in, but I can't figure out the reflection. As best I can understand, it's built around the keys and the elements being available on the javascript object. Rather than having a special case for Map types, and since these are all references anyway, I am thinking of leaving the key and element as they were before, but also putting them into the javascript Map api, for faster size calls. I will push commits for this later today, and some javascript heap snapshots to make sure memory usage isn't significantly higher. |
I would strongly prefer not to do this, not for performance, but for maintenance reasons. A lot of our work on gopherjs over the past year has been dedicated to simplifying and documenting the codebase, and I would very much prefer to help you figure out the reflection side of the problem over adding more cruft that will be confusing people in future. |
Side note - when it is decided that ECMA Script 2017 is supported, we can use 10000 elements: Before After When this merges, I can make an issue. Making it now would be putting the cart before the horse. |
I spent most of the day yesterday trying to figure it out. I'll give it another shot. Maybe having slept on it will unlock something. |
Current state of performance. Range is a bit worse?!? :( 10000 elements Master:
This commit:
|
Those numbers make me think I could change tack and just account for the size of the map during creating, assign and delete. That seems semantically less correct, through. 🤔 |
I haven't tested it, but I'm pretty sure this is the line you need to modify to make
it probably should be something like
gopherjs/compiler/natives/src/reflect/reflect.go Line 1382 in fa8544d
|
Is there something special I need to do to include reflect changes? If I change reflect.go or reflectlite.go, then |
I believe this is probably what you're looking for: https://github.com/gopherjs/gopherjs/wiki/Developer-Guidelines#code-generation |
Yeah, I just discovered that vfsgendev wasn't working right. 🤦🤦🤦 |
That'll make things easier, I think I'm getting some traction, now. |
I hope CI doesn't cost much. I think it may actually pass now.
|
And, makes sense that we should be cautious with this. |
2da42db
to
17eb766
Compare
My curiosity is afire wanting to know what the one potential issue might be. 🐱 |
One other potential source of corner cases we need to verify is map internalization/externalization (that is, conversions between type S structs {
*js.Object
Prop map[string]int `js:prop`
} |
Good call, I'll see if I can make a test that can demonstrate correctness there. |
What is the expectation on wrapping? It seems to me that we would still want the new map implementation to wrap to a js.Object with map keys as properties, otherwise existing code will break. In other words:
|
Yes, I think we should make sure this doesn't break existing code and that Go maps are correctly translated into objects and back. |
How do those tests look to demonstrate correctness of wrapping/internalize/externalize? |
Oh, node doesn't like that. I'm not much of a Node guy. I wonder how best to do that namespacing for both contexts. |
@tomconnell-wf see my suggestion here :) |
Fwiw, I transpiled all of our rendering code with this branch, and it seemed to operate and output normally. |
How are you all feeling about this? Is there anything additional I can do to increase comfort levels? |
I can squash again when this is close to merge |
@tomconnell-wf thanks, I left a few more comments. Please give me a day or two to rack my brain for more corner cases, but I agree that it is dangerously close to being merge-ready 😉 |
You're cooking up some good things to investigate. I'm not particularly in a rush. Now that the 1.18.5 tag is out, I will be busy getting upgraded for a while. |
Any brainstorms over the weekend about edge case problems? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay, my day job has kept my attention yesterday...
I couldn't think of any additional corner cases, so hopefully we didn't miss anything too bad :) I left what is hopefully the last round of comments, most of which are style-related. Other than that we need to:
- Rebase the PR on top of 1.18 version of master (if you haven't already).
- Fix CI failures (currently I can see that the gopherjs_tests workflow is failing).
- Squash commits into one with an informative commit description.
And that should be it! 🎉 Thanks again for your hard work and patience with my nitpicks 😅
I agree with all of your suggestions. I'll get on that today. |
87e37c7
to
7bec2d2
Compare
Okay, I think that's everything. I'll be happy to assist if any bugs come up with this in the near future. |
The main motivation for this change is to improve the performance of len() on maps. Previously, len() would compile to javascript that called `Object.keys().length`. This iterates the whole map to get its length. Using the benchmarks added in this commit, on a map with 10000 elements: ``` BenchmarkMapLen 9886 115719 ns/op ``` Now, len() just calls to javascript Map API, Map.size, which now as fast as calling len() on a slice. ``` BenchmarkMapLen 1000000000 0.5100 ns/op ``` This pull request contains changes to the compiler, prelude and standard library necessary to utilize the new API. However, when externalizing maps, they are still converted into objects to maintain backwards compatibility.
7bec2d2
to
9771b78
Compare
All looks good to me. I've edited the commit message to exclude irrelevant bits, but I think as soon as CI completes, we are good to merge \o/ |
Overview
#1135
The performance of len() on maps brought me here, because it would call js
Object.keys(myMap).Length
. It was many orders of magnitude slower than len() in Go. It was a pitfall that those writing idiomatic go would fall into.This PR switches the backing implementation of Maps in GopherJS to ECMAScript 2015 Maps. These maps provide accounting so that Map.size can be called to make len() fast. I was hopeful that iterating maps would be faster, also, because the len of the map is used, but unfortunately that is not the case. However, it should be trivial to change the loop implementation to use
[Map.entries()
](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries) when GopherJS adopts ECMAScript 2017.One thing to note: ES 2015 Maps do not return in random order. There is a pitfall, the I believe is acceptable, that someone could write GopherJS code that relies on the order of elements in a map, that does not work when run in the Go runtime.
Using the benchmarks committed as part of this PR, with a size of 10000 elements:
Master
This Branch
Map len is ~6 orders of magnitude faster. Unfortunately, it appears element access and ranging has gotten ~20% worse, and ranging ~3% worse
Testing
It seems like the tests cover many more cases than manual testing ever could. I may have tunnel vision as the author of the PR, but I believe that passing CI tests should be considered good enough.