RFC : Approach to handle Allocation Merges in C2 Scalar Replacement
Cesar Soares Lucas
Divino.Cesar at microsoft.com
Fri Feb 18 17:53:18 UTC 2022
Hi Vladimir, thank you for sharing this idea.
I with the selector-based base splitting idea using some examples and I think I understand the mechanics well. It's conceptually simple and quite effective, I like it! I've another question, though. From your previous message I had understood that we needed the clustering because split_unique_types can't handle multiple bases at once, which makes sense to me. _This time I don't understand why we need to do the clustering before doing the selector-based transformation_. AFAIU this transformation won't be creating any memory slices but rather just assigning IDs to different bases and then creating specialized memory operations conditionally using the different bases.
Regards,
Cesar
________________________________________
From: Vladimir Ivanov <vladimir.x.ivanov at oracle.com>
Sent: February 17, 2022 7:37 AM
To: Vladimir Kozlov; Cesar Soares Lucas; hotspot compiler
Cc: Brian Stafford; Martijn Verburg
Subject: Re: RFC : Approach to handle Allocation Merges in C2 Scalar Replacement
> But depending on the actual shape of the base pointer (here it's
> Phi(obj1,obj2)), it may become infeasible (from performance perspective)
> or even impossible (e.g., for complex loop variant conditions) to
> separate instance slices.
I'd like to correct myself. After thinking more about the problem, I see
a universal way to scalarize clusters of interfering non-escaping
allocations even in presence of accesses with indeterminate base.
The crucial property for such cluster of interacting non-escaping
allocations is for any memory access its base is either part of the
cluster or not (it's a static property for every access). Then, when it
is part of the cluster, then the number of possible base values at
runtime is finite and is a subset of the cluster.
So, memory graph for the cluster can be scalarized as follows:
(1) separate memory graph for the cluster
(2) on the newly constructed graph:
(a) replace every base pointer with an ID ("selector id") and
recreate data graph for selector IDs from base pointers graph;
(b) for accesses with indeterminate base pointer, replace them
with a merge of states from relevant allocations selected by "selector id"
(c) after 2b, all memory accesses from the cluster memory graph
should have fixed base pointing at a particular allocation
(3) scalarize individual allocations from the cluster one by one
(since they are independent now)
- additional transformation (like, split-through-phi) should
simplify the graph by reducing the number of cases at merge points to
care about (ideally, leaving only a single one);
As an illustration:
val = Load (AddP (Phi R base1 base2) offset) mem # load A.i
==(pseudo-code)==>
selector = (Phi R #base1 #base2)
if (selector == #base1) {
val1 = Load (base1 + offset) mem
} else if (selector == #base2) {
val2 = Load (base2 + offset) mem
} else {
halt(); // impossible
}
val = Phi(R1, val1, val2)
======
Store (AddP (Phi R base1 base2) offset) mem v # store A.i v
==(pseudo-code)==>
selector = (Phi R #base1 #base2)
if (selector == #base1) {
Store base1 mem v ==> mem1
} else if (selector == #base2) {
Store base2 mem v ==> mem2
} else {
halt(); // impossible
}
mem = Phi(R1, mem1, mem2);
Best regards,
Vladimir Ivanov
>
> So, gradual approach seems the way to go:
> (1) split memory graph for clusters of interacting non-escaping
> allocations;
>
> (2) perform adhoc transformation targeted at untangling aliasing
> accesses (e.g, split-through-phi);
>
> (3) extract unique instance types where possible, thus making the
> corresponding allocation scalar replaceable
>
> =====================================================================
>
> Also, one observation: interacting allocations don't have to be of the
> same class.
>
> static int testPolluted(int r, boolean b) {
> A obj1 = new B1(r);
> A obj2 = new B2(r);
>
> A obj = (b ? obj1 : obj2);
> for (int i = 1; i < r; i++) {
> obj.i++;
> }
>
> return obj1.i + obj2.i;
> }
>
> Requiring unique instance types to perform SR (and not enhancing SR to
> handle aliasing allocations case) helps avoid some challenges in
> instance rematerialization logic at safepoints, because actual shape of
> the scalarized object (its class and exact set of fields) becomes a
> runtime property.
>
> Best regards,
> Vladimir Ivanov
>
>>>>
>>>> Otherwise, I believe wide memory states/merges would pose serious
>>>> problems. E.g., when a load is split through a phi, you need to pick
>>>> correct memory states for the new loads above the phi which quickly
>>>> becomes quite a challenging task.
>>>
>>> Especially in loops. Actually I did tried such approach before and I
>>> agree that it is hard.
>>>
>>> Thanks,
>>> Vladimir K
>>>
>>>>
>>>> Best regards,
>>>> Vladimir Ivanov
>>>>
>>>>>
>>>>> public int ex1(boolean cond, int first, int second) {
>>>>> p0 = Allocate(...);
>>>>> ...
>>>>> p0.x = first;
>>>>> p0.y = second;
>>>>>
>>>>> if (cond) {
>>>>> p1 = Allocate(...);
>>>>> ...
>>>>> p1.x = second;
>>>>> p1.y = first;
>>>>> }
>>>>>
>>>>> p = phi(p0, p1) // unused and will be removed
>>>>>
>>>>>
>>>>> return phi(p0.x,p1.x) - phi(p0.y, p1.y);
>>>>> }
>>>>>
>>>>> Thanks,
>>>>> Vladimir K
>>>>>
>>>>> On 2/10/22 11:29 AM, Vladimir Ivanov wrote:
>>>>>> (BCCing hotspot-dev and moving the discussion to
>>>>>> hotspot-compiler-dev.)
>>>>>>
>>>>>> Hi Cesar,
>>>>>>
>>>>>> Thanks for looking into enhancing EA.
>>>>>>
>>>>>> Overall, the proposal looks reasonable.
>>>>>>
>>>>>> I suggest to look more closely at split_unique_types().
>>>>>> It introduces a dedicated class of alias categories for fields of
>>>>>> the allocation being eliminated and clones memory graph. I don't
>>>>>> see why it shouldn't work for multiple allocations.
>>>>>>
>>>>>> Moreover, split_unique_types() will break if you start optimizing
>>>>>> multiple allocations at once. The notion of unique alias should be
>>>>>> adjusted and cover the union of unique aliases for all interacting
>>>>>> allocations.
>>>>>>
>>>>>> Seems like you need to enhance SR to work on non-intersecting
>>>>>> clusters of allocations.
>>>>>>
>>>>>> One thing to take care of: scalar replacement relies on
>>>>>> TypeOopPtr::instance_id().
>>>>>>
>>>>>> // If not InstanceTop or InstanceBot, indicates that this is
>>>>>> // a particular instance of this type which is distinct.
>>>>>> // This is the node index of the allocation node creating this
>>>>>> instance.
>>>>>> int _instance_id;
>>>>>>
>>>>>> It'll break when multiple allocations are in play.
>>>>>>
>>>>>> Best regards,
>>>>>> Vladimir Ivanov
>>>>>>
>>>>>> On 09.02.2022 04:45, Cesar Soares Lucas wrote:
>>>>>>> Hi there again!
>>>>>>>
>>>>>>> Can you please give me feedback on the following approach to at
>>>>>>> least partially
>>>>>>> address [1], the scalar replacement allocation merge issue?
>>>>>>>
>>>>>>> The problem that I am trying to solve arises when allocations are
>>>>>>> merged after a
>>>>>>> control flow split. The code below shows _one example_ of such a
>>>>>>> situation.
>>>>>>>
>>>>>>> public int ex1(boolean cond, int x, int y) {
>>>>>>> Point p = new Point(x, y);
>>>>>>> if (cond)
>>>>>>> p = new Point(y, x);
>>>>>>> // Allocations for p are merged here.
>>>>>>> return p.calc();
>>>>>>> }
>>>>>>>
>>>>>>> Assuming the method calls on "p" are inlined then the allocations
>>>>>>> will not
>>>>>>> escape the method. The C2 IR for this method will look like this:
>>>>>>>
>>>>>>> public int ex1(boolean cond, int first, int second) {
>>>>>>> p0 = Allocate(...);
>>>>>>> ...
>>>>>>> p0.x = first;
>>>>>>> p0.y = second;
>>>>>>>
>>>>>>> if (cond) {
>>>>>>> p1 = Allocate(...);
>>>>>>> ...
>>>>>>> p1.x = second;
>>>>>>> p1.y = first;
>>>>>>> }
>>>>>>>
>>>>>>> p = phi(p0, p1)
>>>>>>>
>>>>>>> return p.x - p.y;
>>>>>>> }
>>>>>>>
>>>>>>> However, one of the constraints implemented here [2],
>>>>>>> specifically the third
>>>>>>> one, will prevent the objects from being scalar replaced.
>>>>>>>
>>>>>>> The approach that I'm considering for solving the problem is to
>>>>>>> replace the Phi
>>>>>>> node `p = phi(p0, p1)` with new Phi nodes for each of the fields
>>>>>>> of the objects
>>>>>>> in the original Phi. The IR for `ex1` would look something like
>>>>>>> this after the
>>>>>>> transformation:
>>>>>>>
>>>>>>> public int ex1(boolean cond, int first, int second) {
>>>>>>> p0 = Allocate(...);
>>>>>>> ...
>>>>>>> p0.x = first;
>>>>>>> p0.y = second;
>>>>>>>
>>>>>>> if (cond) {
>>>>>>> p1 = Allocate(...);
>>>>>>> ...
>>>>>>> p1.x = second;
>>>>>>> p1.y = first;
>>>>>>> }
>>>>>>>
>>>>>>> pX = phi(first, second)
>>>>>>> pY = phi(second, first)
>>>>>>>
>>>>>>> return pX - pY;
>>>>>>> }
>>>>>>>
>>>>>>> I understand that this transformation might not be applicable for
>>>>>>> all cases and
>>>>>>> that it's not as simple as illustrated above. Also, it seems to
>>>>>>> me that much of
>>>>>>> what I'd have to implement is already implemented in other steps
>>>>>>> of the Scalar
>>>>>>> Replacement pipeline (which is a good thing). To work around these
>>>>>>> implementation details I plan to use as much of the existing code
>>>>>>> as possible.
>>>>>>> The algorithm for the transformation would be like this:
>>>>>>>
>>>>>>> split_phis(phi)
>>>>>>> # If output of phi escapes, or something uses its identity, etc
>>>>>>> # then we can't remove it. The conditions here might
>>>>>>> possible be the
>>>>>>> # same as the ones implemented in
>>>>>>> `PhaseMacroExpand::can_eliminate_allocation`
>>>>>>> if cant_remove_phi_output(phi)
>>>>>>> return ;
>>>>>>>
>>>>>>> # Collect a set of tuples(F,U) containing nodes U that uses
>>>>>>> field F
>>>>>>> # member of the object resulting from `phi`.
>>>>>>> fields_used = collect_fields_used_after_phi(phi)
>>>>>>>
>>>>>>> foreach field in fields_used
>>>>>>> producers = {}
>>>>>>>
>>>>>>> # Create a list with the last Store for each field
>>>>>>> "field" on the
>>>>>>> # scope of each of the Phi input objects.
>>>>>>> foreach o in phi.inputs
>>>>>>> # The function called below might re-use a lot of
>>>>>>> the code/logic in `PhaseMacroExpand::scalar_replacement`
>>>>>>> producers += last_store_to_o_field(0, field)
>>>>>>> # Create a new phi node whose inputs are the Store's to
>>>>>>> 'field'
>>>>>>> field_phi = create_new_phi(producers)
>>>>>>>
>>>>>>> update_consumers(field, field_phi)
>>>>>>>
>>>>>>> The implementation that I envisioned would be as a "pre-process"
>>>>>>> [3] step just
>>>>>>> after EA but before the constraint checks in
>>>>>>> `adjust_scalar_replaceable_state`
>>>>>>> [2]. If we agree that the overall Scalar Replacement
>>>>>>> implementation goes through
>>>>>>> the following major phases:
>>>>>>>
>>>>>>> 1. Identify the Escape Status of objects.
>>>>>>> 2. Adjust object Escape and/or Scalar Replacement status
>>>>>>> based on a set of constraints.
>>>>>>> 3. Make call to Split_unique_types [4].
>>>>>>> 4 Iterate over object and array allocations.
>>>>>>> 4.1 Check if allocation can be eliminated.
>>>>>>> 4.2 Perform scalar replacement. Replace uses of object
>>>>>>> in Safepoints.
>>>>>>> 4.3 Process users of CheckCastPP other than Safepoint:
>>>>>>> AddP, ArrayCopy and CastP2X.
>>>>>>>
>>>>>>> The transformation that I am proposing would change the overall
>>>>>>> flow to look
>>>>>>> like this:
>>>>>>>
>>>>>>> 1. Identify the Escape Status of objects.
>>>>>>> 2. ----> New: "Split phi functions" <----
>>>>>>> 2. Adjust object Escape and/or Scalar Replacement status
>>>>>>> based on a set of constraints.
>>>>>>> 3. Make call to Split_unique_types [14].
>>>>>>> 4 Iterate over object and array allocations.
>>>>>>> 4.1 ----> Moved to split_phi: "Check if allocation can
>>>>>>> be eliminated" <----
>>>>>>> 4.2 Perform scalar replacement. Replace uses of object
>>>>>>> in Safepoints.
>>>>>>> 4.3 Process users of CheckCastPP other than Safepoint:
>>>>>>> AddP, ArrayCopy and CastP2X.
>>>>>>>
>>>>>>> Please let me know what you think and thank you for taking the
>>>>>>> time to review
>>>>>>> this!
>>>>>>>
>>>>>>>
>>>>>>> Regards,
>>>>>>> Cesar
>>>>>>>
>>>>>>> Notes:
>>>>>>>
>>>>>>> [1] I am not sure yet how this approach will play with the
>>>>>>> case of a merge
>>>>>>> with NULL.
>>>>>>> [2]
>>>>>>> https://nam06.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fopenjdk%2Fjdk%2Fblob%2F2f71a6b39ed6bb869b4eb3e81bc1d87f4b3328ff%2Fsrc%2Fhotspot%2Fshare%2Fopto%2Fescape.cpp%23L1809&data=04%7C01%7CDivino.Cesar%40microsoft.com%7Ce13b0a3de30e43d7ac4408d9f22b75c5%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C637807090747649306%7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C3000&sdata=WruWqIgGVIzIFIG0gD0%2FEIQm40euLx6FZMexotJVrlE%3D&reserved=0
>>>>>>>
>>>>>>>
>>>>>>> [3] Another option would be to "patch" the current
>>>>>>> implementation to be able
>>>>>>> to handle the merges. I am not certain that the "patch"
>>>>>>> approach would be
>>>>>>> better, however, the "pre-process" approach is certainly
>>>>>>> much easier to test
>>>>>>> and more readable.
>>>>>>>
>>>>>>> [4] I cannot say I understand 100% the effects of executing
>>>>>>> split_unique_types(). Would the transformation that I am
>>>>>>> proposing need to
>>>>>>> be after the call to split_unique_types?
More information about the hotspot-compiler-dev
mailing list