Skip to content
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

Type sharing for __proto__ #1489

Merged
merged 1 commit into from
Sep 15, 2016
Merged

Conversation

kunalspathak
Copy link
Contributor

@kunalspathak kunalspathak commented Aug 25, 2016

Type sharing for proto scenario

Problem :
Today everytime a prototype of an object obj is changed, we convert its type handler to SimpleDictionaryTypeHandler, create a new DynamicType and assign it to the object. In future, every single time there is a property access on this object (obj.a = or = obj.a), profile data would see a new type for obj and would conclude that the call site has polymorphic types and would create optimization based on caching those types. But even after JIT, the cache won't help because it would still get new type and thus we end up doing bunch of checks in JIT after which either we bailout or take slowpath to extract the property from typehandler. It would be nice if while executing obj.__proto__ = protoObj; we detect if obj's current type is X and protoObj's current type is Y, then always assign type Z to obj. That way in future, wherever obj is used for property access, it would see same type Z and profile data / JIT would be optimized to take advantage of object type specialization.

Solution :
Today, in creating an object from prototype, we make use of TypeOfPrototypeObject internal property id. This internal property is used on prototype object to cache the dynamicType that is assigned to an object using that prototype object. Later if another object is created with same prototype object, it would get the cached type instead of creating a new type, thus leads to sharing types between two objects.
I have extended the logic to __proto__ scenario by caching the dynamicType inside the new prototype object that we are about to assign to an obj. If we find a pre-existing type, we would just assign it to obj instead of creating a new type. Only difference is that we have to assign a type to obj such that it contains all the properties that obj had before execution of __proto__. To address that, after fetching the type from cache (internal property id of new prototype object), I evolve that type based on existing properties of an object. If the evolution on cached type is happening for first time, PathTypeHandler would perform the acutal evolution of type and assign the evolved type to obj. Next time if we try to set same prototype object to a different object obj2 (but having same properties as obj), PathTypeHandler would evolve cached type to same type that was assigned to obj. Thus we would always assign same type to obj and obj2.

For example, lets say we cache the type T1 in prototype object protoObj. When we execute obj.__proto__ = protoObj we would evolve T1 to T2 and assign to obj. Later, if we are executing obj2.__proto__ = protoObj, we would start with cached type T1 and by based on properties present on obj2, PathTypeHandler would evolve to T2. Thus obj2 will get type T2 which is same as that of obj.

We update the cache with new type for below conditions:

  • When cached type's inlineSlotCapacity is different than that of current object's type.
  • When cached type's offsetOfInlineSlot is different than that of current object's type.

However there might be scenarios where same prototype protoObj is used to set prototype of 2 different types of objects alternatively. One that has inlineSlotCapacity zero (e.g. objects having ExternalTypes) and other that has inlineSlotCapacity non-zero. In this case, we would keep updating prototype's cached type with a new type which would defeat the purpose of type sharing. Hence I have splitted the existing TypeOfPrototypeObject in ZTypeOfPrototypeObject (cached types that has zero inlineSlots) and NZTypeOfPrototypeObject (cached types that has non-zero inlineSlots).

Test: Unit-test passes, automated test in progress
Perf: This gives _25%_ win in acme-air. On my 2 minutes run, turned out that __proto__ was called 85292 times. That means we would have created 85292 new types. After my change, we just create 14 types and they are shared 85,278 objects. Other benchmarks had no change because they don't use __proto__.

@@ -344,6 +344,7 @@ PHASE(All)
PHASE(StackFramesEvent)
#endif
PHASE(PerfHint)
PHASE(TypeShare)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give this a less generic name? It doesn't control type-sharing, only a very specific instance of type-sharing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Will change it to TypeSharingForChangePrototype.

// Use TypeOfPrototypeObjectInlined slot only if this is DynamicType and typeId is TypeIds_Object
// For everything else, i.e. (DynamicType, other typeIds) and (JsrtExternalType, TypeIds_Object) use
// TypeOfPrototypeObjectDictionary
Js::InternalPropertyIds propertyIdHoldingCache = (!isJsrtExternalType
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious have you tried to allocate inline slot for jsrtexternalobject as well?

@kunalspathak
Copy link
Contributor Author

@pleath, I have updated the code with new design we discussed.

: DynamicTypeHandler::RoundUpInlineSlotCapacity(requestedInlineSlotCapacity);
swprintf_s(reason, 1024, _u("InlineSlotCapacity mismatch. Required = %d, Cached = %d"), requiredCapacity, cachedDynamicTypeHandler->GetInlineSlotCapacity());
#endif
Assert(cachedDynamicTypeHandler->GetInlineSlotCapacity() >= roundedInlineSlotCapacity);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also confirm that it's safe to shrink the capacity in this way by verifying that the total number of properties <= the current inlineSlotCapacity?

@pleath
Copy link
Contributor

pleath commented Sep 14, 2016

LGTM. Thanks.

Implementation of type sharing using PathTypeHandler for __proto__ scenario.
1. InlineSlot - Used for caching type in CreateObject() scenario like before when object doesn't have a type to start with.
2. DictionarySlot - Used for __proto__ cases. This is a BaseDictionary from oldType (DynamicType) to promoted newType (again DynamicType).
The idea is that we will cache the promoted type instead of promoting the base type everytime.

Added unit test
Added `-trace:TypeShareForChangePrototype` for testing
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants