RESOLVED FIXED 190047
[JSC] Optimize Object.keys by caching own keys results in StructureRareData
https://bugs.webkit.org/show_bug.cgi?id=190047
Summary [JSC] Optimize Object.keys by caching own keys results in StructureRareData
Yusuke Suzuki
Reported 2018-09-27 11:12:19 PDT
Let's cache the own keys in StructureRareData and reuse it to optimize Object.keys. We will put JSImmutableButterfly in StructureRareData. And Object.keys will generate JSArray wrapping it!
Attachments
WIP (44.29 KB, patch)
2018-09-28 05:56 PDT, Yusuke Suzuki
no flags
WIP (44.31 KB, patch)
2018-09-28 06:11 PDT, Yusuke Suzuki
no flags
WIP (50.93 KB, patch)
2018-12-09 09:53 PST, Yusuke Suzuki
no flags
Patch (54.96 KB, patch)
2018-12-10 03:14 PST, Yusuke Suzuki
no flags
Patch (55.65 KB, patch)
2018-12-15 05:43 PST, Yusuke Suzuki
no flags
Patch (56.25 KB, patch)
2018-12-15 21:15 PST, Yusuke Suzuki
no flags
Patch (56.26 KB, patch)
2018-12-16 21:05 PST, Yusuke Suzuki
saam: review+
Yusuke Suzuki
Comment 1 2018-09-27 11:13:04 PDT
Object.keys are super heavily called in web-tooling-benchmarks.
Saam Barati
Comment 2 2018-09-27 11:32:48 PDT
When is it safe to cache this? Object.keys is just self keys, right? Not prototype chain? So I guess if we're not a dictionary we can cache?
Saam Barati
Comment 3 2018-09-27 11:33:04 PDT
(In reply to Saam Barati from comment #2) > When is it safe to cache this? Object.keys is just self keys, right? Not > prototype chain? So I guess if we're not a dictionary we can cache? We should also have some story of cleaning up this value.
Yusuke Suzuki
Comment 4 2018-09-28 01:11:02 PDT
(In reply to Saam Barati from comment #3) > (In reply to Saam Barati from comment #2) > > When is it safe to cache this? Object.keys is just self keys, right? Not > > prototype chain? So I guess if we're not a dictionary we can cache? > > We should also have some story of cleaning up this value. I think we can align the lifetime of the cache to the JSPropertyEnumerator cache (for-in cache). But currently, we do not clear JSPropertyEnumerator cache.
Yusuke Suzuki
Comment 5 2018-09-28 05:56:34 PDT
Created attachment 351069 [details] WIP Initial patch
Yusuke Suzuki
Comment 6 2018-09-28 06:08:39 PDT
(In reply to Yusuke Suzuki from comment #4) > (In reply to Saam Barati from comment #3) > > (In reply to Saam Barati from comment #2) > > > When is it safe to cache this? Object.keys is just self keys, right? Not > > > prototype chain? So I guess if we're not a dictionary we can cache? > > > > We should also have some story of cleaning up this value. > > I think we can align the lifetime of the cache to the JSPropertyEnumerator > cache (for-in cache). > But currently, we do not clear JSPropertyEnumerator cache. Possible ways. 1. Do not add clearing path. Anyway, when Structure is collected, the cache is also collected. This is the same lifetime to the existing for-in cache. 2. Use Weak. Once the cached JSImmutableButterfly is not reachable from elsewhere, it will be collected. 3. Clear cache in Heap::finalize() It would be a bit costly to iterate StructureRareData (iterating IsoHeap), but we can clear these data when finalizing the collector.
Yusuke Suzuki
Comment 7 2018-09-28 06:09:26 PDT
This is an initial attempt. It improves two benchmarks in SixSpeed since both use Object.keys. baseline patched object-assign.es5 333.2122+-4.8252 ^ 219.6000+-3.3567 ^ definitely 1.5174x faster for-of-object.es6 272.0637+-3.3468 ^ 128.9627+-3.7837 ^ definitely 2.1096x faster And it improves web-tooling-benchmark/lebab since it heavily uses Object.keys. Before: lebab: 5.76 runs/s After: lebab: 6.84 runs/s
Yusuke Suzuki
Comment 8 2018-09-28 06:11:00 PDT
Yusuke Suzuki
Comment 9 2018-12-09 09:53:05 PST
Yusuke Suzuki
Comment 10 2018-12-10 03:14:24 PST
Yusuke Suzuki
Comment 11 2018-12-11 21:19:20 PST
Ping?
Keith Miller
Comment 12 2018-12-12 10:45:39 PST
Comment on attachment 356953 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=356953&action=review r=me. Although, I agree with Saam that we should probably have a story for collecting stale enumerations. Can you file a bug? Maybe something like: clear on FullGC + Structure is only referenced via a transition? Not sure how we would determine the latter though... > Source/JavaScriptCore/runtime/StructureRareDataInlines.h:62 > + return m_cachedOwnKeys.get(); Do we have an ASSERT(!compilationOrGCThread())?
Yusuke Suzuki
Comment 13 2018-12-12 23:12:25 PST
Comment on attachment 356953 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=356953&action=review Thanks. I've opened the bug for tracking GC enhancement. https://bugs.webkit.org/show_bug.cgi?id=192659 >> Source/JavaScriptCore/runtime/StructureRareDataInlines.h:62 >> + return m_cachedOwnKeys.get(); > > Do we have an ASSERT(!compilationOrGCThread())? Sounds nice! Fixed.
Yusuke Suzuki
Comment 14 2018-12-12 23:13:43 PST
Radar WebKit Bug Importer
Comment 15 2018-12-12 23:14:27 PST
Yusuke Suzuki
Comment 16 2018-12-13 00:06:35 PST
Yusuke Suzuki
Comment 17 2018-12-13 00:37:40 PST
Saam Barati
Comment 18 2018-12-13 01:38:18 PST
Comment on attachment 356953 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=356953&action=review I have a few comments. > Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp:769 > + case ObjectKeys: { I really wish we had a way of duplicating less code between AI and constant folding > Source/JavaScriptCore/runtime/ObjectConstructor.cpp:956 > + structure->setCachedOwnKeys(vm, jsCast<JSImmutableButterfly*>(vm.sentinelImmutableButterfly.get())); Is the whole point of this just to cache after we ask for keys more than once? Why not just store a sentinel using the number 1? The getter can then just return nullptr to people when it’s really 1. You could then get a rid of a lot of pointer compares to this sentinel value too > Source/JavaScriptCore/runtime/Structure.h:331 > + WTF::loadLoadFence(); Why? All your loads are dependent loads based off of this > Source/JavaScriptCore/runtime/StructureRareDataInlines.h:68 > + WTF::loadLoadFence(); Ditto as to why > Source/JavaScriptCore/runtime/StructureRareDataInlines.h:74 > + WTF::storeStoreFence(); This one makes sense and is required
Saam Barati
Comment 19 2018-12-13 01:52:04 PST
(In reply to Keith Miller from comment #12) > Comment on attachment 356953 [details] > Patch > > View in context: > https://bugs.webkit.org/attachment.cgi?id=356953&action=review > > r=me. Although, I agree with Saam that we should probably have a story for > collecting stale enumerations. Can you file a bug? > > Maybe something like: clear on FullGC + Structure is only referenced via a > transition? Not sure how we would determine the latter though... We could do an even simpler thing: Delete it if it hasn’t been used since the last GC. We just need to store a bit when we use it. > > > Source/JavaScriptCore/runtime/StructureRareDataInlines.h:62 > > + return m_cachedOwnKeys.get(); > > Do we have an ASSERT(!compilationOrGCThread())?
WebKit Commit Bot
Comment 20 2018-12-14 13:30:33 PST
Re-opened since this is blocked by bug 192715
Ryan Haddad
Comment 21 2018-12-14 13:40:39 PST
*** Bug 192673 has been marked as a duplicate of this bug. ***
Yusuke Suzuki
Comment 22 2018-12-15 04:43:27 PST
(In reply to Ryan Haddad from comment #21) > *** Bug 192673 has been marked as a duplicate of this bug. *** OK, I think I found the issue. When executing Object.keys, we first create JSImmutableButterfly, then store JSString keys into this immutable butterfly. The problem is that we allocates JSString* after creating JSImmutableButterfly. So, when the collector is waken up in this JSString* allocation, it will see JSImmutableButterfly which holds many uninitialized hidden fields. If the marker want to mark them, then we have a bad time. I think calling `setStartingValue(JSValue())` for Contiguous shape case would be good way to fix the issue.
Yusuke Suzuki
Comment 23 2018-12-15 05:23:24 PST
Comment on attachment 356953 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=356953&action=review >> Source/JavaScriptCore/runtime/ObjectConstructor.cpp:956 >> + structure->setCachedOwnKeys(vm, jsCast<JSImmutableButterfly*>(vm.sentinelImmutableButterfly.get())); > > Is the whole point of this just to cache after we ask for keys more than once? Why not just store a sentinel using the number 1? The getter can then just return nullptr to people when it’s really 1. You could then get a rid of a lot of pointer compares to this sentinel value too This a bit simplifies the marking code in StructureRareData::visitChildren and the user code of cachedOwnKeys. I'll attempt to change it to some dummy value. >> Source/JavaScriptCore/runtime/Structure.h:331 >> + WTF::loadLoadFence(); > > Why? All your loads are dependent loads based off of this Right, removed. >> Source/JavaScriptCore/runtime/StructureRareDataInlines.h:68 >> + WTF::loadLoadFence(); > > Ditto as to why Ah, after considering about it, I think it is not necessary. No load-load barrier is required.
Yusuke Suzuki
Comment 24 2018-12-15 05:43:42 PST
Saam Barati
Comment 25 2018-12-15 12:02:49 PST
Comment on attachment 357399 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=357399&action=review > Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h:2587 > + if (structureSet.isFinite() && structureSet.size() == 1) { Do we ever think we’d see two structures with the same ownKeys array? We could serialize this across >1 structure somehow if we ever found it useful > Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h:2590 > + auto* immutableButterfly = rareData->cachedOwnKeysConcurrently(); Why not just make the getter return nullptr if it’s the sentinel? Then you could have some function like getIgnoringSentinel for the only getter you call that cares if it’s actually the sentinel value > Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp:2695 > + case ObjectKeysIntrinsic: { Is Object.keys observable if the input isn’t a Proxy? > Source/JavaScriptCore/dfg/DFGNode.cpp:229 > + setOpAndDefaultFlags(NewArrayBuffer); Does this node do the right thing if we’re having a bad time? > Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp:12394 > + slowCases.append(m_jit.branch32(CCallHelpers::Equal, CCallHelpers::Address(scratchGPR, JSCell::structureIDOffset()), TrustedImm32(bitwise_cast<int32_t>(m_jit.vm()->structureStructure->structureID())))); This is how we determine its RareData? Does StructureRareData have a a JSType? > Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp:12399 > + slowCases.append(m_jit.branchPtr(CCallHelpers::Equal, scratchGPR, TrustedImmPtr(bitwise_cast<void*>(StructureRareData::cachedOwnKeysSentinel())))); You can combine both branches here if you assert the sentinel is “1”, and branch BelowOrEqual > Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp:5531 > + m_out.branch(m_out.notEqual(cachedOwnKeys, m_out.constIntPtr(bitwise_cast<void*>(StructureRareData::cachedOwnKeysSentinel()))), unsure(useCacheCase), unsure(slowCase)); Ditto about combining branches > Source/JavaScriptCore/runtime/JSImmutableButterfly.h:123 > + if (hasContiguous(indexingType())) { Why is this needed? > Source/JavaScriptCore/runtime/ObjectConstructor.cpp:939 > + auto* cachedButterfly = structure->cachedOwnKeys(); Based on earlier comment, this would be the only call to something like “cachedOwnKeysIgnorningSentinel”
Yusuke Suzuki
Comment 26 2018-12-15 20:40:35 PST
Comment on attachment 357399 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=357399&action=review >> Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h:2587 >> + if (structureSet.isFinite() && structureSet.size() == 1) { > > Do we ever think we’d see two structures with the same ownKeys array? We could serialize this across >1 structure somehow if we ever found it useful Currently, I think we do not have the use cases that receive benefit from it. Once it is required, we need to consider the optimization. But even in that case, ObjectKeys is accelerated if the given Structure has the cache. >> Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h:2590 >> + auto* immutableButterfly = rareData->cachedOwnKeysConcurrently(); > > Why not just make the getter return nullptr if it’s the sentinel? Then you could have some function like getIgnoringSentinel for the only getter you call that cares if it’s actually the sentinel value Interesting. Basically, all the concurrent functions do not care about sentinel since they do not set a new JSImmutableButterfly. So, only the function called from the main thread matters. Fixed. >> Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp:2695 >> + case ObjectKeysIntrinsic: { > > Is Object.keys observable if the input isn’t a Proxy? No, it's not observable if the input isn't a Proxy. But as a first step, I think the current implementation is OK. We could optimize it further by adding special care to clobberizing rules in the subsequent patch. Moreover, if ObjectKeys only takes one Structure which is proven in AI, then it is converted into NewArrayBuffer, which tells us that it does not have any side effect. So, I think large part of the use cases is already covered by this. >> Source/JavaScriptCore/dfg/DFGNode.cpp:229 >> + setOpAndDefaultFlags(NewArrayBuffer); > > Does this node do the right thing if we’re having a bad time? We convert ObjectKeys into NewArrayBuffer only when we can watch the HavingABadTimeWatchpoint. >> Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp:12394 >> + slowCases.append(m_jit.branch32(CCallHelpers::Equal, CCallHelpers::Address(scratchGPR, JSCell::structureIDOffset()), TrustedImm32(bitwise_cast<int32_t>(m_jit.vm()->structureStructure->structureID())))); > > This is how we determine its RareData? Does StructureRareData have a a JSType? Yes. You can see Structure::isRareData function. And StructureRareData does not have a JSType. >> Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp:12399 >> + slowCases.append(m_jit.branchPtr(CCallHelpers::Equal, scratchGPR, TrustedImmPtr(bitwise_cast<void*>(StructureRareData::cachedOwnKeysSentinel())))); > > You can combine both branches here if you assert the sentinel is “1”, and branch BelowOrEqual Nice. fixed. >> Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp:5531 >> + m_out.branch(m_out.notEqual(cachedOwnKeys, m_out.constIntPtr(bitwise_cast<void*>(StructureRareData::cachedOwnKeysSentinel()))), unsure(useCacheCase), unsure(slowCase)); > > Ditto about combining branches Fixed. >> Source/JavaScriptCore/runtime/JSImmutableButterfly.h:123 >> + if (hasContiguous(indexingType())) { > > Why is this needed? If this does not exist, the storage of JSImmutableButterfly (Contiguous) remains uninitialized when it is created. This leads to the crash if we perform allocation after JSImmutableButterfly is created and before all the storage is filled. This patch is rolled out because of this GC issue. Two possible fixes are considered, (1) Put DeferGC on ObjectKeys' appropriate path. But since ObjectKeys would create so many JSString*, it is not so desirable. (2) Fill JSImmutableButterfly Contiguous storage fields. In this patch, I took (2). This approach can fix the above scenario, which can easily happen. And since JSValue() is JSEmpty (nullptr in 64bit), the following code could be compiled into very efficient one. >> Source/JavaScriptCore/runtime/ObjectConstructor.cpp:939 >> + auto* cachedButterfly = structure->cachedOwnKeys(); > > Based on earlier comment, this would be the only call to something like “cachedOwnKeysIgnorningSentinel” Fixed.
Yusuke Suzuki
Comment 27 2018-12-15 21:15:39 PST
Yusuke Suzuki
Comment 28 2018-12-16 21:05:00 PST
Saam Barati
Comment 29 2018-12-17 14:40:02 PST
Comment on attachment 357427 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=357427&action=review r=me. Nice! > Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h:2591 > + auto* immutableButterfly = rareData->cachedOwnKeysConcurrently(); > + if (immutableButterfly && immutableButterfly != StructureRareData::cachedOwnKeysSentinel()) { This should now just be: if (auto* immutableButterfly = rareData->cachedOwnKeysConcurrently()) { ... } > Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp:776 > + auto* immutableButterfly = rareData->cachedOwnKeysConcurrently(); > + if (immutableButterfly && immutableButterfly != StructureRareData::cachedOwnKeysSentinel()) { ditto > Source/JavaScriptCore/runtime/JSImmutableButterfly.h:126 > + if (hasContiguous(indexingType())) { > + for (unsigned index = 0; index < length; ++index) > + toButterfly()->contiguous().at(this, index).setStartingValue(JSValue()); > + } Why not only do this for Object.keys? Seems like this might affect other callers too? Seems like there should be some kind of ::create that has this in its name? > Source/JavaScriptCore/runtime/ObjectConstructor.cpp:939 > + // Cache the immutable butterfly! nit: This comment has no purpose. > Source/JavaScriptCore/runtime/StructureRareDataInlines.h:85 > + WTF::storeStoreFence(); nit: Only needs to be done on the "else"
Yusuke Suzuki
Comment 30 2018-12-17 22:51:52 PST
Comment on attachment 357427 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=357427&action=review Thank you! >> Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h:2591 >> + if (immutableButterfly && immutableButterfly != StructureRareData::cachedOwnKeysSentinel()) { > > This should now just be: > if (auto* immutableButterfly = rareData->cachedOwnKeysConcurrently()) { ... } Fixed. >> Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp:776 >> + if (immutableButterfly && immutableButterfly != StructureRareData::cachedOwnKeysSentinel()) { > > ditto Fixed. >> Source/JavaScriptCore/runtime/JSImmutableButterfly.h:126 >> + } > > Why not only do this for Object.keys? Seems like this might affect other callers too? Seems like there should be some kind of ::create that has this in its name? I think this is not bad. If the code creates JSImmutableButterfly and does some allocations before filling up the storage, then we have a bad time if this code does not exist. If we would like to remove this code, I think we should put `DisallowGC` on all the places creating JSImmutableButterfly, putting some optimized path for that use cases. >> Source/JavaScriptCore/runtime/ObjectConstructor.cpp:939 >> + // Cache the immutable butterfly! > > nit: This comment has no purpose. Removed. >> Source/JavaScriptCore/runtime/StructureRareDataInlines.h:85 >> + WTF::storeStoreFence(); > > nit: Only needs to be done on the "else" OK, fixed.
Yusuke Suzuki
Comment 31 2018-12-17 22:54:54 PST
Note You need to log in before you can comment on or make changes to this bug.