Open Bug 1162665 Opened 9 years ago Updated 8 months ago

ES6 Map performance for large numbers of elements is deficient

Categories

(Core :: JavaScript: GC, defect)

40 Branch
defect

Tracking

()

Performance Impact low
Tracking Status
firefox40 --- affected

People

(Reporter: kael, Unassigned, NeedInfo)

References

(Blocks 1 open bug)

Details

Attachments

(2 files)

While investigating a performance issue with Map in v8, I discovered that its performance in SpiderMonkey is relatively deficient in my test case as well.

The test case compares using a JS Object as a dictionary vs a Map and measures the cost of adding a large number of key-value pairs (~12 million) to each container type.

The performance of Object is good in both engines and relatively stable. In SpiderMonkey, the performance of Map is relatively stable, but the overall run time is something like 10x worse than Object. It appears that a good portion of the time lost here is due to GC pauses - so it seems Map is creating an extreme amount of GC pressure when compared to Object or simply increasing the cost of doing the collections.

This test is not entirely like-for-like because the Object's keys are ints and the Map's keys are objects. (My real-world scenario is using arbitrary object instances as keys, so these are the two implementations I have to choose from.)

This kinda sucks since Map is the only way to key things on arbitrary values unless you're willing to cram numbered properties onto objects and change all your code to handle it.

Timing data follows. Test case attached (you can run the .js file in the spidermonkey shell, or the .html file in Firefox.)

I think it's probably important to mitigate the GC issues here for large-scale JS apps (if possible), and it would be great if the overall performance profile could be closer to Object.

---- firefox nightly 40.0a1 (2015-05-07) ----

No triggered garbage collection available; this may negatively impact results.

// Object.create(null)
0: 4ms
350000: 5ms
700000: 6ms
1050000: 3ms
1400000: 2ms
1750000: 10ms
2100000: 2ms
2450000: 2ms
2800000: 13ms
3150000: 3ms
3500000: 2ms
3850000: 17ms
4200000: 3ms
4550000: 2ms
4900000: 19ms
5250000: 2ms
5600000: 2ms
5950000: 23ms
6300000: 2ms
6650000: 2ms
7000000: 27ms
7350000: 2ms
7700000: 2ms
8050000: 30ms
8400000: 2ms
8750000: 2ms
9100000: 34ms
9450000: 2ms
9800000: 2ms
10150000: 2ms
10500000: 2ms
10850000: 2ms
11200000: 41ms
11550000: 2ms
11900000: 1ms
js object: 288ms

// new Map()
0: 50ms
350000: 58ms
700000: 48ms
1050000: 978ms
1400000: 57ms
1750000: 54ms
2100000: 58ms
2450000: 1158ms
2800000: 65ms
3150000: 63ms
3500000: 71ms
3850000: 71ms
4200000: 74ms
4550000: 78ms
4900000: 80ms
5250000: 1383ms
5600000: 64ms
5950000: 66ms
6300000: 79ms
6650000: 75ms
7000000: 72ms
7350000: 71ms
7700000: 70ms
8050000: 74ms
8400000: 76ms
8750000: 74ms
9100000: 82ms
9450000: 86ms
9800000: 90ms
10150000: 93ms
10500000: 91ms
10850000: 1921ms
11200000: 72ms
11550000: 63ms
11900000: 18ms
js Map: 7589ms
So I see two things going on here, in a profile.

First of all, lots of time spent in the put() on the hashtable.  It's all self time (except for the rehash) bits.  I'm _guessing_ it's something to do with barriers if we resize the hashtable, but not sure.

The second thing is marking.  This is almost entirely self time in js::GCMarker::processMarkStackTop, though there is some time under MapObject::mark as well.

> This test is not entirely like-for-like because the Object's keys are ints and the
> Map's keys are objects.

And the values are ints in both cases, yes?  So the Map case simply has a lot more marking to do...

That said, making this more of an apples to apples comparison by making the Map use the integers as keys doesn't seem to change things much.
Component: JavaScript Engine → JavaScript: GC
Flags: needinfo?(terrence)
Moving ni? to Jon.
Flags: needinfo?(terrence) → needinfo?(jcoppeard)
I experimented with a couple of GC-related approaches:

 - Making Map/Set use unique IDs for objects made the code much simpler but the benchmark results twice as slow.

 - Maintaining a list of nursery keys on the Map/Set object rather than adding a generic store buffer entry for each key made no different to the results.  This approach would probably help a different usage pattern where the objects were created on the fly though, so that may be worth pursuing elsewhere.

Marking is slow because it's map containing 12000000 entries.  I tried prodding the compiler to inline more of the marking path but it made no difference.

I think a big factor is that we have to call into C++ from the JIT for ever set operation on the map, whereas operations on objects are heavily optimised.
Blocks: es6perf
A data point is that it seems on V8, Map accessing is much faster than object accessing based on https://jsperf.com/javascript-objects-vs-map-performance

From my local test, while we have a better performance on object accessing, our Map accessing is up to 13x slower than V8 on the benchmark above, which is a big gap.

Since Map has a much better performance on Chrome, it can be expected that people may start preferring Map over object in many cases, and that could affect our user experience.
Whiteboard: [qf]
Whiteboard: [qf] → [qf:p3]

Hello.
I note that Map performance in latest stable FF is still less than on regular Object. Please refer to this test: https://jsperf.com/es6-map-vs-object-properties/2

Performance Impact: --- → P3
Whiteboard: [qf:p3]
Severity: normal → S3
See Also: → 1800806
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: