Repository navigation
JIT: limitations in hoisting (loop invariant code motion) #35735
Description
Have been looking into #13811 and have found that the current implementation of loop invariant code motion has some awkward limitations.
In particular if the invariant computations are distributed across statements connected by temps, only the first computation in the chain ends up getting hoisted. In the particular example from #13811 the invariant chain was:
Vector128<byte> result = CreateScalarUnsafe(value);
return Avx2.BroadcastScalarToVector128(result);where value was constant. This ended up in a loop after some inlining. Only the CreateScalarUnsafe gets hoisted.
Note the chains can be arbitrary computation and involve more than two statements.
When hoisting we walk statement by statement looking for hoistable subtrees. Local assignments are not considered hoistable -- only their right hand sides. If we hoist a tree we produce an unconsumed copy in the preheader and let CSE come along later and clean things up.
When the analysis gets to the second statement in a dependent chain, it sees the def for the local conveying the value from the first statement as loop varying, and so does not hoist.
We could try fixing this in a variety of ways:
- forward substitution might be able to glue together trees connected by single-def single use temps, however it is a big hammer, potentially tricky to get right, and costly to run in full generality
- we could try and fuse these trees in the importer, say if we see back to back stloc/ldloc and no other references to the local
- we could fix hoisting to handle this case, with a few options:
- we could check if the subtree's VN is already hoisted, and so effectively do forward sub for the temp -- then let CSE clean all this up like we do now; this would potentially end up with quadratic amounts of cloning, though in practice, it might be acceptable;
- we could hoist assignments; this requires some care and rewiring of SSA which might be risky
- we could introduce new temps and/or modify the unconsumed hoisted tree to write to a temp, and use that to propagate the hoisted value from the first clone to second clone.
I am trying to assess how often we see this; it is a bit tricky because while I can spot the second link being blocked I can't easily tell how long the chains are so anything beyond that is harder to spot.
Rough guess based on some crude prototyping is around 2700 hoistable expressions that are second links in the usual FX diff set. There are 152 in the crossgen of SPC, including some sort and span methods.
I'm encouraged enough that I will build a more realistic prototype.
category:cq
theme:loop-opt
skill-level:expert
cost:large
Activity
EgorBo commentedon May 2, 2020
I guess BCL developers try to hoist such simd expressions by hands everywhere so the diff could be way higher.
Btw, is there an issue for constant folding for vectors? e,g.
Sse.Add(Vector128<float>.One, Vector128<float>.One)(not sure it's profitable though)AndyAyersMS commentedon May 2, 2020
Perhaps? This is the kind of thing that is easy to overlook or take for granted. I haven't looked at the types of computations being hoisted.
I was curious how much hoisting was going on in general (even with current limitations) to try and so did a bit more instrumentation. There are
So second level hoisting would increased the amount of hoistable expressions by around 5%.
The unprofitable percentage seems high and is worth deeper exploration. There are hits in some of the key BCL routines, eg (random sample from my instrumentation run):
Also realized there are a few other issues to consider here:
fgOptWhileLoop). In the double nest below each loop is rotated and so creates the proper conditions for one level of hoisting, but at the time we run hoisting, we still see a path in the outer loop that avoids the inner loop; this path blocks hoisting of fully invariant expressions from inner all the way to top level.AndyAyersMS commentedon May 18, 2020
Some examples.
Computation invariant in both inner and outer loop:
The multiply is only hoisted up one level, not two:
Computations invariant in inner and outer loop, second one dependent on the first.
Here the independent statment is hoisted one level, the dependent statement is not hoisted.
Same invariant computation in both nested loops:
Computations are hoisted, but not CSE'd:
AndyAyersMS commentedon May 19, 2020
See also related issues:
AndyAyersMS commentedon Mar 30, 2021
Going to reassign to Bruce. Guessing we won't address this in 6.0.
kunalspathak commentedon Aug 16, 2021
Example where we don't hoist constants out of the loop - example 1 and example 2.
am11 commentedon Aug 19, 2021
LICM / LIH doesn't kick in for pure methods, even when invariant is self-assigned, e.g.
EgorBo commentedon Aug 19, 2021
@am11 do you mean you expect
GetXmethod to be recognized as pure and all callers should hoist calls toGetX?am11 commentedon Aug 19, 2021
Inlining 42 at callsites would be an additional bonus. :)
but I was expecting that in this isolated example, the method body would first reduce to just:
EgorBo commentedon Aug 19, 2021
There is a separate issue here - we currently never remove empty loops: https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzgWwB8ABAJgEYBYAKGIGYACMhgYQYG8aHunHiUGAWQAUASi492PaROncAZtAbCAlgDsMDFQF4ADAG4tAHnK6DWgNQXRHAL6yG96raA==
am11 commentedon Sep 4, 2021
btw, we do remove empty loop if we return from it. e.g. with main (2a1eaa2):
comment out line 7, so it goes from:
to
; Total bytes of code 21, prolog size 4, PerfScore 12.35, instruction count 11, allocated bytes for code 21 (MethodHash=40e0669f) for method Program:GetY(int):intkunalspathak commentedon May 11, 2022
This is done now.