Bug 172413 - [JSC] Map and Set constructors should have fast path for cloning
Summary: [JSC] Map and Set constructors should have fast path for cloning
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: JavaScriptCore (show other bugs)
Version: WebKit Nightly Build
Hardware: Unspecified Unspecified
: P2 Normal
Assignee: Yusuke Suzuki
URL:
Keywords: InRadar
Depends on:
Blocks: 172419
  Show dependency treegraph
 
Reported: 2017-05-20 04:26 PDT by Yusuke Suzuki
Modified: 2017-05-30 20:26 PDT (History)
4 users (show)

See Also:


Attachments
Patch (50.59 KB, patch)
2017-05-21 10:33 PDT, Yusuke Suzuki
no flags Details | Formatted Diff | Diff
Patch (68.08 KB, patch)
2017-05-22 01:08 PDT, Yusuke Suzuki
no flags Details | Formatted Diff | Diff
Patch (68.06 KB, patch)
2017-05-22 01:13 PDT, Yusuke Suzuki
no flags Details | Formatted Diff | Diff
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 Details
Patch (68.06 KB, patch)
2017-05-22 04:11 PDT, Yusuke Suzuki
no flags Details | Formatted Diff | Diff
Patch (72.92 KB, patch)
2017-05-22 21:04 PDT, Yusuke Suzuki
saam: review+
Details | Formatted Diff | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Yusuke Suzuki 2017-05-20 04:26:00 PDT
We always use forEachIterable(), that's not good!
Comment 1 Yusuke Suzuki 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).
Comment 2 Yusuke Suzuki 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.
Comment 3 Yusuke Suzuki 2017-05-20 06:07:48 PDT
First, we should support cloning case.
Comment 4 Yusuke Suzuki 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.
Comment 5 Saam Barati 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.
Comment 6 Yusuke Suzuki 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.
Comment 7 Yusuke Suzuki 2017-05-21 10:33:32 PDT
Created attachment 310810 [details]
Patch

WIP: Need to add files to xcodeproj
Comment 8 Yusuke Suzuki 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.
Comment 9 Yusuke Suzuki 2017-05-22 01:08:39 PDT
Created attachment 310838 [details]
Patch
Comment 10 Yusuke Suzuki 2017-05-22 01:13:29 PDT
Created attachment 310839 [details]
Patch
Comment 11 Build Bot 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.
Comment 12 Build Bot 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
Comment 13 Yusuke Suzuki 2017-05-22 04:11:43 PDT
Created attachment 310848 [details]
Patch
Comment 14 Yusuke Suzuki 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;
}
Comment 15 Saam Barati 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)
Comment 16 Yusuke Suzuki 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.
Comment 17 Yusuke Suzuki 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.
Comment 18 Saam Barati 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
Comment 19 Saam Barati 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.
Comment 20 Yusuke Suzuki 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.
Comment 21 Saam Barati 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.
Comment 22 Saam Barati 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.
Comment 23 Yusuke Suzuki 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, """).
Comment 24 Yusuke Suzuki 2017-05-22 21:04:19 PDT
Created attachment 310976 [details]
Patch
Comment 25 Yusuke Suzuki 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.
Comment 26 Saam Barati 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
Comment 27 Yusuke Suzuki 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.
Comment 28 Yusuke Suzuki 2017-05-27 16:22:04 PDT
Committed r217525: <http://trac.webkit.org/changeset/217525>
Comment 29 Yusuke Suzuki 2017-05-28 04:33:59 PDT
Committed r217527: <http://trac.webkit.org/changeset/217527>
Comment 30 Radar WebKit Bug Importer 2017-05-30 20:26:28 PDT
<rdar://problem/32479819>