JDK-8134609 : Allow constructors with same prototoype map to share the allocator map
  • Type: Enhancement
  • Component: core-libs
  • Sub-Component: jdk.nashorn
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: generic
  • CPU: generic
  • Submitted: 2015-08-27
  • Updated: 2016-01-14
  • Resolved: 2015-09-16
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 8 JDK 9
8u72Fixed 9 b83Fixed
Description
As shown by JDK-8133925 we want to be able to allow different instances of a constructor function to use the same allocator map as long as their prototypes have the same maps as well. This helps us keep callsites monomorphic in a couple of circumstances, such as the various non-global constructor code patterns, or scripts with global constructors being evaluated multiple times with different globals.

This means we have to keep track of what a map's expected prototype map is, and change the map if the actual prototype map differs or has been modified on any of the (prototype) objects using it. We also need to introduce a switchpoint for callsites involving shared prototype maps, as the prototype may have changed on any object not directly involved in the linking of the callsite.

Basically, we make an assumption about the JavaScript code that prototypes are usually not modified after objects have been constructed. This makes property maps a bit more like classes, allowing us to assume objects with the same property map also have prototypes with the same map.

Comments
A few explanations for this change, because many things are not very obvious. Why is this needed at all? There are two major use cases for this feature: 1) Many JS libraries come with their own "class system", building some kind of class/inheritance scheme on top of prototypal inheritance. Very often these frameworks use constructor functions to build prototypes and instances that are not top-level functions. This means that on each invocation, a new script function with a new allocator prototype is created for the constructor. 2) When scripts are re-evaluated (for whatever reason - it may occur with the same global object or different global objects), new instances of the top level functions need to be created, which also means they get new allocator prototypes with the same maps as their previous versions. The regression of JDK-8133925 is actually a combination of both cases as the Sunspider tests were wrapped in a function that was called repeatedly, turning global functions into local ones. In both of the cases above, even when prototypes use the same map, objects inheriting from them can't simply do the same as our current prototype listener system cannot track changes to the prototype that occur before the prototype is used in a callsite. Therefore it is possible that these seemingly identical prototype objects diverge (change maps) before being used in a callsite, and we have no way of tracking or noticing it. However, in most cases, prototypes are not modified after creation, i.e. prototypes behave more or less like immutable classes. It would therefore be desirable to use the same instance map for these instances as well. Because we cannot track changes to individual prototype objects with our current map listener system, we need to add a special mechanism to make sure none of the objects using the shared map has a prototype that has evolved from the original shared shape. I implemented this with an additional "shared prototype" switch point which is added to call sites whenever the property lookup involves a shared prototype. This switch points is only created for shared maps, and shared maps are only used by objects that are actually used as allocator prototype in a constructor function. They are invalidated as soon as any change is made to the map by any of the prototype objects using it. This may seem overly conservative, but it is sufficient in almost all of the use cases I encountered. In addition to adding a "shared proto" switch point to callsites we need some way to deal with shared prototypes that have been invalidated, because if one prototype object evolved than basically all objects using the shared map became invalid. This is done by the new ScriptObject#checkSharedProtoMap method which is invoked from ScriptObject#findProperty. If it finds an invalidated prototype map (even because its own prototype evolved, or because its map was invalidated by some other prototype) it will replace its own map with a copy that is no longer shared. The reason we do this in findProperty is that the property lookup is always the first step in any operation involving inherited lookups. If we did it for example in ScriptObject#getProtoSwitchpoints then any map guards we already obtained would still operate on the old, invalidated maps. When switching from a "normal" to a shared prototype map, both the prototype map and the allocator map are replaced. This means we add a bit of polymorphism here. Why not use keep the current prototype map and make it shared? Ideally we should be able to keep the prototype and allocator maps when transitioning between single and shared proto maps (i.e. there should be no transition or switching of maps at all). The biggest problem with this approach is that we'd have to create and use the shared prototype switch point for all objects coming from a constructor function, because every allocator prototype could become shared at some point in time. This means we'd add lots of switch points that are only needed in very rare cases. There are also other problems with "keeping shared proto maps out in the open". For example, some scripts use the same prototype map for different constructors, or have different constructor's prototypes evolve through the same maps. These problems could be solved by moving the invalidation mechanism from the map to the script object, meaning that a shared map is invalidated only if it is changed by an object actually using it as prototype. However, the problem with the many unnecessary switch points mentioned above remains. Another problem here is that we add overhead to map changes for all ScriptObjects, and of course above mentioned problem with many unnecessary prototype switch point also applies. In AllocatorStrategy I only keep record of the last prototype/map pair used by a given constructor. Why not keep a map of all previously allocated prototype/instance map pairs? This would indeed reduce polymorphism to the minimum. However, this whole feature only serves very special use cases described above. By just remembering the last allocation map, we can solve all the "normally behaved" re-evaluation based cases, and in the local class constructor case maps can't be shared anyway because they are different from class to class. So the current design solves most of the problems and keeps overhead low for common single-prototype constructors. Another thing I tried was putting the invalidation functionality into PrototypeObject (the default class for constructor prototype objects) to keep it out of ScriptObject. It seems that a lot of the above objections could be worked around this way. Unfortunately there's no guarantee that other ScriptObject classes won't be used as prototypes, especially with above mentioned "class systems". We need this to work with all kinds of prototype objects.
11-09-2015