Bug 172413

Summary: [JSC] Map and Set constructors should have fast path for cloning
Product: WebKit Reporter: Yusuke Suzuki <ysuzuki>
Component: JavaScriptCoreAssignee: Yusuke Suzuki <ysuzuki>
Status: RESOLVED FIXED    
Severity: Normal CC: buildbot, fpizlo, saam, webkit-bug-importer
Priority: P2 Keywords: InRadar
Version: WebKit Nightly Build   
Hardware: Unspecified   
OS: Unspecified   
Bug Depends on:    
Bug Blocks: 172419    
Attachments:
Description Flags
Patch
none
Patch
none
Patch
none
Archive of layout-test-results from ews117 for mac-elcapitan
none
Patch
none
Patch saam: review+

Yusuke Suzuki
Reported 2017-05-20 04:26:00 PDT
We always use forEachIterable(), that's not good!
Attachments
Patch (50.59 KB, patch)
2017-05-21 10:33 PDT, Yusuke Suzuki
no flags
Patch (68.08 KB, patch)
2017-05-22 01:08 PDT, Yusuke Suzuki
no flags
Patch (68.06 KB, patch)
2017-05-22 01:13 PDT, Yusuke Suzuki
no flags
Archive of layout-test-results from ews117 for mac-elcapitan (2.00 MB, application/zip)
2017-05-22 02:47 PDT, Build Bot
no flags
Patch (68.06 KB, patch)
2017-05-22 04:11 PDT, Yusuke Suzuki
no flags
Patch (72.92 KB, patch)
2017-05-22 21:04 PDT, Yusuke Suzuki
saam: review+
Yusuke Suzuki
Comment 1 2017-05-20 04:30:20 PDT
And now, we always use adder function from C++, that's not good. If we know add function is not replaced, we should add values from C++ interface directly. (This can be easily figured out by setting watchpoint).
Yusuke Suzuki
Comment 2 2017-05-20 04:30:53 PDT
I believe this is related to ARES-6 Air, since we can see that Set() constructor takes too much time in sampling results.
Yusuke Suzuki
Comment 3 2017-05-20 06:07:48 PDT
First, we should support cloning case.
Yusuke Suzuki
Comment 4 2017-05-20 07:12:32 PDT
ARES-6 Air frequently executes LocalCalc(). In which, we perform `new Set(set)` cloning. This can be optimized super well if 1. we know Set.prototype.add is not changed. 2. we know the given set iterator protocol is not changed. I've just hacked JSC a bit. My rough implementation does not have correct watchpoint for the above ones. But setting a watchpoint is easy. ARES-6 Air shows another great performance improvement! Before: Running... Air ( 1 to go) firstIteration: 75.96 +- 21.43 ms averageWorstCase: 40.30 +- 7.67 ms steadyState: 9.25 +- 0.22 ms summary: 35.66 +- 2.18 ms After: Running... Air ( 1 to go) firstIteration: 75.07 +- 22.41 ms averageWorstCase: 38.89 +- 8.26 ms steadyState: 8.89 +- 0.18 ms Yay! SteadyState shows another 4% improvement! I'm now WIP.
Saam Barati
Comment 5 2017-05-20 15:28:14 PDT
(In reply to Yusuke Suzuki from comment #4) > ARES-6 Air frequently executes LocalCalc(). In which, we perform `new > Set(set)` cloning. This can be optimized super well if > > 1. we know Set.prototype.add is not changed. > 2. we know the given set iterator protocol is not changed. And that there is no add property on the base set. > > I've just hacked JSC a bit. My rough implementation does not have correct > watchpoint for the above ones. But setting a watchpoint is easy. > ARES-6 Air shows another great performance improvement! > > Before: > Running... Air ( 1 to go) > firstIteration: 75.96 +- 21.43 ms > averageWorstCase: 40.30 +- 7.67 ms > steadyState: 9.25 +- 0.22 ms > summary: 35.66 +- 2.18 ms > > After: > Running... Air ( 1 to go) > firstIteration: 75.07 +- 22.41 ms > averageWorstCase: 38.89 +- 8.26 ms > steadyState: 8.89 +- 0.18 ms > > Yay! SteadyState shows another 4% improvement! > > I'm now WIP.
Yusuke Suzuki
Comment 6 2017-05-20 15:38:37 PDT
(In reply to Saam Barati from comment #5) > And that there is no add property on the base set. Right. This should be done as is the similar to the iterator protocol's check. (getDirect() etc.). In this bug, I'll just attempt to optimize `new Set(set)` case. Broader optimization (for generic iterators etc.) will be done with https://bugs.webkit.org/show_bug.cgi?id=172419 change.
Yusuke Suzuki
Comment 7 2017-05-21 10:33:32 PDT
Created attachment 310810 [details] Patch WIP: Need to add files to xcodeproj
Yusuke Suzuki
Comment 8 2017-05-21 23:59:54 PDT
(In reply to Yusuke Suzuki from comment #6) > (In reply to Saam Barati from comment #5) > > And that there is no add property on the base set. > > Right. This should be done as is the similar to the iterator protocol's > check. (getDirect() etc.). > In this bug, I'll just attempt to optimize `new Set(set)` case. > Broader optimization (for generic iterators etc.) will be done with > https://bugs.webkit.org/show_bug.cgi?id=172419 change. After considering about it, I think we do not need to check "add" / "set" existence. This is because, 1. Our check ensures that generated set / map's [[Prototype]] is Set.prototype or Map.prototype. 2. This constructor newly creates an object. Thus object instance itself does not have any properties.
Yusuke Suzuki
Comment 9 2017-05-22 01:08:39 PDT
Yusuke Suzuki
Comment 10 2017-05-22 01:13:29 PDT
Build Bot
Comment 11 2017-05-22 02:47:34 PDT
Comment on attachment 310839 [details] Patch Attachment 310839 [details] did not pass mac-debug-ews (mac): Output: http://webkit-queues.webkit.org/results/3793108 Number of test failures exceeded the failure limit.
Build Bot
Comment 12 2017-05-22 02:47:35 PDT
Created attachment 310844 [details] Archive of layout-test-results from ews117 for mac-elcapitan The attached test failures were seen while running run-webkit-tests on the mac-debug-ews. Bot: ews117 Port: mac-elcapitan Platform: Mac OS X 10.11.6
Yusuke Suzuki
Comment 13 2017-05-22 04:11:43 PDT
Yusuke Suzuki
Comment 14 2017-05-22 17:53:45 PDT
(In reply to Yusuke Suzuki from comment #13) > Created attachment 310848 [details] > Patch BTW, later, after @nakedConstructor thing is landed, we can extend this Map / Set constructors something like, function Map() { "use strict"; var iterable = @argument(0); if (iterable == null) return @constructMap(new.target); var iteratorFunction = @tryGetById(iterable, Symbol.iterator); if (iteratorFunction === @mapIteratorFunction) return @constructMap(new.target, iterable); // Set watchpoint in DFG var result = @constructMap(new.target); var adder = result.add; for (var value of iterable) adder.@call(result, value); return result; }
Saam Barati
Comment 15 2017-05-22 18:06:47 PDT
Comment on attachment 310848 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=310848&action=review Mostly LGTM, but I think I found one bug, and have some overall comments. r- b/c the bug. > Source/JavaScriptCore/runtime/HashMapImpl.h:348 > + if ((Checked<uint32_t>(base->m_keyCount) * 2).unsafeGet() >= powerOf2) > + powerOf2 = (Checked<uint32_t>(powerOf2) * 2).unsafeGet(); How is this condition ever not true? Is it only when keyCount is 0 or 1? Also, why *2 here, and not *4? After creating this set, if anybody adds anything to it, we'll rehash and grow the table. Are we just guessing that nobody adds to it? > Source/JavaScriptCore/runtime/HashMapImpl.h:360 > + makeAndSetNewBuffer(exec, vm); > + RETURN_IF_EXCEPTION(scope, void()); > + > + m_head.set(vm, this, HashMapBucketType::create(vm)); > + m_tail.set(vm, this, HashMapBucketType::create(vm)); > + > + m_head->setNext(vm, m_tail.get()); > + m_tail->setPrev(vm, m_head.get()); > + m_head->setDeleted(true); > + m_tail->setDeleted(true); Nit: This is identical to the other finishCreation. It's probably worth having a helper function. > Source/JavaScriptCore/runtime/HashMapImpl.h:460 > + ALWAYS_INLINE void addNormalizedNonExistingForCloning(ExecState* exec, JSValue key, JSValue value = JSValue()) Why does just calling add not work? This function seems nearly identical to the regular add, it feels like there should be a way to combine them. Perhaps the hash lookup loop could take a lambda that does returns a boolean indicating if we can store to the current bucket. > Source/JavaScriptCore/runtime/InternalFunction.h:51 > + ALWAYS_INLINE static Structure* createSubclassStructure(ExecState*, JSValue newTarget, Structure*); I thought this needed to be marked ALWAYS_INLINE on the implementation of the function, not declaration? > Source/JavaScriptCore/runtime/InternalFunction.h:52 > + JS_EXPORT_PRIVATE static Structure* createSubclassStructureSlow(ExecState*, JSValue newTarget, Structure*); this should be private > Source/JavaScriptCore/runtime/JSSet.cpp:48 > +bool JSSet::isIteratorProtocolFastAndNonObservable() The name of this function is misleading since returning true does not mean the protocol is fast and non observable. Maybe this doesn't even need to be a function. We can just combine this and "addFastAndNonObservable" into a single function since they both have just one caller, and it's the same function. (Same comments for the JSMap variant) > Source/JavaScriptCore/runtime/JSSet.cpp:81 > + // This is the fast case. Many sets will be an original set. > + if (structure == globalObject->setStructure()) > + return true; > + > + if (structure->storedPrototype() != globalObject->jsSetPrototype()) > + return false; > + > + return true; This proof does not look correct. Consider: ``` let x = new Set; x.add = 42; ``` > Source/JavaScriptCore/runtime/JSSet.h:85 > +template<typename To, typename From> > +inline typename std::enable_if<std::is_same<typename std::remove_pointer<To>::type, JSSet>::value, To>::type jsDynamicCast(VM&, From* from) > +{ > + if (LIKELY(from->type() == JSSetType)) > + return static_cast<To>(from); > + return nullptr; > +} > + > +template<> > +inline JSSet* jsDynamicCast<JSSet*>(VM&, JSValue from) > +{ > + if (LIKELY(from.isCell() && from.asCell()->type() == JSSetType)) > + return static_cast<JSSet*>(from.asCell()); > + return nullptr; > +} I'd mark JSSet as "final", and static assert that it's final here, otherwise we could have a bad time if anybody ever inherits from them. This will future proof the code with a static assert. (Same for JSMap)
Yusuke Suzuki
Comment 16 2017-05-22 19:11:12 PDT
Comment on attachment 310848 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=310848&action=review Thanks! >> Source/JavaScriptCore/runtime/HashMapImpl.h:348 >> + powerOf2 = (Checked<uint32_t>(powerOf2) * 2).unsafeGet(); > > How is this condition ever not true? Is it only when keyCount is 0 or 1? > > Also, why *2 here, and not *4? After creating this set, if anybody adds anything to it, we'll rehash and grow the table. Are we just guessing that nobody adds to it? Right. When keyCount is 0 or 1, the above becomes false. *4 may be ok. But I'm also worried about making this set too big. Because we perform roundUpToPowerOfTwo, powerOf2 * 4 may become very large. How about changing the above `WTF::roundUpToPowerOfTwo(base->m_keyCount)` to `WTF::roundUpToPowerOfTwo(base->m_keyCount + 1)` and * 2? >> Source/JavaScriptCore/runtime/HashMapImpl.h:360 >> + m_tail->setDeleted(true); > > Nit: This is identical to the other finishCreation. It's probably worth having a helper function. Nice. Fixed. >> Source/JavaScriptCore/runtime/HashMapImpl.h:460 >> + ALWAYS_INLINE void addNormalizedNonExistingForCloning(ExecState* exec, JSValue key, JSValue value = JSValue()) > > Why does just calling add not work? This function seems nearly identical to the regular add, it feels like there should be a way to combine them. Perhaps the hash lookup loop could take a lambda that does returns a boolean indicating if we can store to the current bucket. Looks fine. This function also does not check `shouldRehashAfterAdd()`, but maybe, it's not a big deal since rehash() never happens. >> Source/JavaScriptCore/runtime/InternalFunction.h:51 >> + ALWAYS_INLINE static Structure* createSubclassStructure(ExecState*, JSValue newTarget, Structure*); > > I thought this needed to be marked ALWAYS_INLINE on the implementation of the function, not declaration? OK, changed. >> Source/JavaScriptCore/runtime/InternalFunction.h:52 >> + JS_EXPORT_PRIVATE static Structure* createSubclassStructureSlow(ExecState*, JSValue newTarget, Structure*); > > this should be private Right. Fixed. >> Source/JavaScriptCore/runtime/JSSet.cpp:48 >> +bool JSSet::isIteratorProtocolFastAndNonObservable() > > The name of this function is misleading since returning true does not mean the protocol is fast and non observable. Maybe this doesn't even need to be a function. We can just combine this and "addFastAndNonObservable" into a single function since they both have just one caller, and it's the same function. > (Same comments for the JSMap variant) OK, I'll merge them to canCloneFastAndNonObservable(). >> Source/JavaScriptCore/runtime/JSSet.cpp:81 >> + return true; > > This proof does not look correct. Consider: > ``` > let x = new Set; > x.add = 42; > ``` Discussed with Saam at IRC. This condition is valid since newly cloned Set never has its instance variables. I'll address the other comments, like unifying the above functions to canCloneFastAndNoObservable(). >> Source/JavaScriptCore/runtime/JSSet.h:85 >> +} > > I'd mark JSSet as "final", and static assert that it's final here, otherwise we could have a bad time if anybody ever inherits from them. This will future proof the code with a static assert. (Same for JSMap) Marking JSSet and JSMap as final is good. But this implementation is OK as long as a derived class also uses the JSSetType. While `asCell()->info() == JSMap::info()` is problematic, using type() is ok. You can create a derived JSMap C++ class which has JSMapType as its JS type.
Yusuke Suzuki
Comment 17 2017-05-22 19:39:27 PDT
Comment on attachment 310848 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=310848&action=review >>> Source/JavaScriptCore/runtime/HashMapImpl.h:348 >>> + powerOf2 = (Checked<uint32_t>(powerOf2) * 2).unsafeGet(); >> >> How is this condition ever not true? Is it only when keyCount is 0 or 1? >> >> Also, why *2 here, and not *4? After creating this set, if anybody adds anything to it, we'll rehash and grow the table. Are we just guessing that nobody adds to it? > > Right. When keyCount is 0 or 1, the above becomes false. > > *4 may be ok. But I'm also worried about making this set too big. Because we perform roundUpToPowerOfTwo, powerOf2 * 4 may become very large. > How about changing the above `WTF::roundUpToPowerOfTwo(base->m_keyCount)` to `WTF::roundUpToPowerOfTwo(base->m_keyCount + 1)` and * 2? For example, if you have 10 keys, capacity now becomes 16 * 2 = 32. 64 is too much big.
Saam Barati
Comment 18 2017-05-22 19:40:49 PDT
Comment on attachment 310848 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=310848&action=review >>> Source/JavaScriptCore/runtime/JSSet.h:85 >>> +} >> >> I'd mark JSSet as "final", and static assert that it's final here, otherwise we could have a bad time if anybody ever inherits from them. This will future proof the code with a static assert. (Same for JSMap) > > Marking JSSet and JSMap as final is good. But this implementation is OK as long as a derived class also uses the JSSetType. > While `asCell()->info() == JSMap::info()` is problematic, using type() is ok. > You can create a derived JSMap C++ class which has JSMapType as its JS type. I agree. But marking it final, and statically asserting that here will make anybody who creates such a subclass *have* to change this function. That could be through: 1. Removing the static assert and making sure that subclass's structure uses JSSetType 2. Removing this function entirely 3. something else altogether
Saam Barati
Comment 19 2017-05-22 19:41:55 PDT
(In reply to Yusuke Suzuki from comment #17) > Comment on attachment 310848 [details] > Patch > > View in context: > https://bugs.webkit.org/attachment.cgi?id=310848&action=review > > >>> Source/JavaScriptCore/runtime/HashMapImpl.h:348 > >>> + powerOf2 = (Checked<uint32_t>(powerOf2) * 2).unsafeGet(); > >> > >> How is this condition ever not true? Is it only when keyCount is 0 or 1? > >> > >> Also, why *2 here, and not *4? After creating this set, if anybody adds anything to it, we'll rehash and grow the table. Are we just guessing that nobody adds to it? > > > > Right. When keyCount is 0 or 1, the above becomes false. > > > > *4 may be ok. But I'm also worried about making this set too big. Because we perform roundUpToPowerOfTwo, powerOf2 * 4 may become very large. > > How about changing the above `WTF::roundUpToPowerOfTwo(base->m_keyCount)` to `WTF::roundUpToPowerOfTwo(base->m_keyCount + 1)` and * 2? > > For example, if you have 10 keys, capacity now becomes 16 * 2 = 32. 64 is > too much big. I see. This makes sense. Ignore my *4 comment.
Yusuke Suzuki
Comment 20 2017-05-22 19:50:05 PDT
Comment on attachment 310848 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=310848&action=review >>>> Source/JavaScriptCore/runtime/JSSet.h:85 >>>> +} >>> >>> I'd mark JSSet as "final", and static assert that it's final here, otherwise we could have a bad time if anybody ever inherits from them. This will future proof the code with a static assert. (Same for JSMap) >> >> Marking JSSet and JSMap as final is good. But this implementation is OK as long as a derived class also uses the JSSetType. >> While `asCell()->info() == JSMap::info()` is problematic, using type() is ok. >> You can create a derived JSMap C++ class which has JSMapType as its JS type. > > I agree. But marking it final, and statically asserting that here will make anybody who creates such a subclass *have* to change this function. That could be through: > 1. Removing the static assert and making sure that subclass's structure uses JSSetType > 2. Removing this function entirely > 3. something else altogether Can we add a static_assert for that? We do not have static relation between JSSetType and JSSet C++ class.
Saam Barati
Comment 21 2017-05-22 19:54:14 PDT
All I'm propsing is if we add a static assert that the class is final, if anybody creates a subclass, they'll need to consider how to change this jsDynamicCast function.
Saam Barati
Comment 22 2017-05-22 19:55:14 PDT
(In reply to Saam Barati from comment #21) > All I'm propsing is if we add a static assert that the class is final, if > anybody creates a subclass, they'll need to consider how to change this > jsDynamicCast function. To clarify, this isn't the most important thing in the world. There are plenty of ways we can break things and we can't always statically assert them. It's up to you if you want to do this or not.
Yusuke Suzuki
Comment 23 2017-05-22 20:20:55 PDT
(In reply to Saam Barati from comment #21) > All I'm propsing is if we add a static assert that the class is final, if > anybody creates a subclass, they'll need to consider how to change this > jsDynamicCast function. Ah, ok. I've just added static_assert(std::is_final<JSSet>::value, """).
Yusuke Suzuki
Comment 24 2017-05-22 21:04:19 PDT
Yusuke Suzuki
Comment 25 2017-05-22 21:08:16 PDT
Comment on attachment 310976 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=310976&action=review OK, I've updated the patch. > Source/JavaScriptCore/runtime/HashMapImpl.h:341 > + capacity = std::max<uint32_t>(WTF::roundUpToPowerOfTwo(capacity), 4U); shouldRehashAfterAdd() condition must be met.
Saam Barati
Comment 26 2017-05-27 15:43:12 PDT
Comment on attachment 310976 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=310976&action=review Nice. r=me > Source/JavaScriptCore/runtime/HashMapImpl.h:526 > + ALWAYS_INLINE void addNormalizedInternal(ExecState* exec, JSValue key, JSValue value, const CanUseBucket& canUseBucket) Nit: I’d add ASSERT(key == normalizedKey(key)) > Source/JavaScriptCore/runtime/JSMap.cpp:75 > + if (structure == globalObject->mapStructure()) Nit: this check is essentially no faster than the below branch. Perhaps it can be removed in favor of just the branch below. I understand the check in the nice case since it prevents a hahsmap lookup. But that’s not the case here. > Source/JavaScriptCore/runtime/JSSet.cpp:75 > + if (structure == globalObject->setStructure()) Ditto
Yusuke Suzuki
Comment 27 2017-05-27 15:52:12 PDT
Comment on attachment 310976 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=310976&action=review Thanks! >> Source/JavaScriptCore/runtime/HashMapImpl.h:526 >> + ALWAYS_INLINE void addNormalizedInternal(ExecState* exec, JSValue key, JSValue value, const CanUseBucket& canUseBucket) > > Nit: I’d add ASSERT(key == normalizedKey(key)) Nice. Added. >> Source/JavaScriptCore/runtime/JSMap.cpp:75 >> + if (structure == globalObject->mapStructure()) > > Nit: this check is essentially no faster than the below branch. Perhaps it can be removed in favor of just the branch below. I understand the check in the nice case since it prevents a hahsmap lookup. But that’s not the case here. Yeah, right. We do not have hash lookup here. Dropped. >> Source/JavaScriptCore/runtime/JSSet.cpp:75 >> + if (structure == globalObject->setStructure()) > > Ditto Dropped.
Yusuke Suzuki
Comment 28 2017-05-27 16:22:04 PDT
Yusuke Suzuki
Comment 29 2017-05-28 04:33:59 PDT
Radar WebKit Bug Importer
Comment 30 2017-05-30 20:26:28 PDT
Note You need to log in before you can comment on or make changes to this bug.