This is an archive of the discontinued LLVM Phabricator instance.

[PATCH] Global reassociation for improved CSE
ClosedPublic

Authored by escha on Nov 14 2017, 1:54 PM.

Details

Summary

This has been improved from the original patch to take into account suggestions, fix bugs, and reduce complexity, so it's no longer an RFC and now a patch ;-)

When playing around with reassociate I noticed a seemingly obvious optimization that was not getting done anywhere in llvm… nor in gcc or ICC.

Consider the following trivial function:

void foo(int a, int b, int c, int d, int e, int *res) {
  res[0] = (e * a) * d;
  res[1] = (e * b) * d;
  res[2] = (e * c) * d;
}

This function can be optimized down to 4 multiplies instead of 6 by reassociating such that (e*d) is the common subexpresion. However, no compiler I’ve tested does this. I wrote a slightly hacky heuristic algorithm to augment reassociate to do this and tested it.

First, before the details, the results: on a large offline test suite of graphics shaders it cut down total instruction count by ~0.9% (!) and float math instruction count by 1.5% (!).

Here’s how it works:

  1. Do reassociate once as normal.
  1. Create a “pair map” consisting of a mapping from <Instr, Instr> to <unsigned>. Have one pair map for each type of BinaryOperation. This map represents how common a given operand pair occurs in the source code for a given BinaryOperation. But in addition to counting each actual instruction, we also count each possible O(N^2) pair of each linear operand chain. So for example, if the operand chain is this:
a*b*c

we do:

PairMap[Multiply][{a, b}]++;
PairMap[Multiply][{b, c}]++;
PairMap[Multiply][{a, c}]++;

The chain length is capped at an arbitrary but low value to avoid possible quadratic behavior.

Furthermore, we do *not* count duplicate pairs in a single chain, so for example, if our chain is

a*a*b*c

we only count a*b and a*c once, to avoid bias.

  1. Run reassociate again. All the information is saved from the first time around so hopefully this won’t be very expensive except for the changes we actually make. But this time, whenever emitting a linear operand chain, pick the operand pair that’s *most common* in the source code (using PairMap) and make that one the first operation. Thus, for example:
(((a*b)*c)*d)*e

if “b*e” is the most common, this becomes:

(((b*e)*a)*c)*d

Now b*e can be CSE’d later! Magic!

Also, as a tiebreaker, the current one I’m using is the “pair which has the lowest max rank of the two operands”, which makes sense because in this example, “a*b” is the first operation in the chain, so we want to pick the duplicates which are also higher up in the program vs closer to the leaf. No other tiebreaker I tried seemed to work as well.

Overall this patch was structured to be a minimally invasive change to avoid major structural changes in Reassociate. It does nothing except change the order in which operands are emitted during the final step, which should be much safer and less complex than actually changing the core algorithm. The core algorithm of reassociate is actually agnostic to this; it only serves to determine *which* operands are picked, not which order they're emitted in.

This patch unfortunately has some overlap with N-ary reassociate, but because they work in such different ways they are probably not unifiable (N-ary reassociate uses a very different algorithm that doesn't catch as many cases, but is less heuristic-based and designed around addressing expressions. And because it uses SCEV, it can't work on float, which is my primary use-case).

Most of the test changes are caused by dead code that was eliminated or other effectively no-op changes that come from the fact reassociate is now iterated twice.

Diff Detail

Repository
rL LLVM

Event Timeline

escha created this revision.Nov 14 2017, 1:54 PM
escha edited the summary of this revision. (Show Details)Nov 14 2017, 1:55 PM
escha edited the summary of this revision. (Show Details)

*gentle bump*

turkey week is over pls glance at patch, thank

spatel edited edge metadata.Nov 29 2017, 7:28 AM

My understanding of 'reassociate' isn't good enough to approve this, but I'm curious about running the main loop multiple times (ReassociateStep) because I noticed an improvement in some other code by running the pass twice.

  1. Can we split that part into a preliminary patch?
  2. Is it too expensive in compile time to run that to a fixed-point (like instcombine)?

My understanding of 'reassociate' isn't good enough to approve this, but I'm curious about running the main loop multiple times (ReassociateStep) because I noticed an improvement in some other code by running the pass twice.

  1. Can we split that part into a preliminary patch?
  2. Is it too expensive in compile time to run that to a fixed-point (like instcombine)?

I'm extremely nervous about running it to a fixed point; I'm not confident that even the *existing pass* will always converge to a fixed point.

Running it twice without any changes seems fairly pointless; the changes are mostly just removal of dead instructions and such that would get instcombined away later.

My understanding of 'reassociate' isn't good enough to approve this, but I'm curious about running the main loop multiple times (ReassociateStep) because I noticed an improvement in some other code by running the pass twice.

  1. Can we split that part into a preliminary patch?
  2. Is it too expensive in compile time to run that to a fixed-point (like instcombine)?

The compile time impact here is a secondary concern.
You first need to prove this reaches a fixpoint, pencil & paper. Not necessarily looking forward to @zhendongsu reports of this pass indefinitely looping on fuzzer generated testcases.

plotfi added a subscriber: plotfi.Nov 30 2017, 12:44 PM

Tests look good.

lib/Transforms/Scalar/Reassociate.cpp
2190

Please provide some high level comments here explaining what is happening like all the other steps in the same function.

2197

Style nitpick: Might be clear to read of both for loops have brackets.

2198

Could there be a way to do this without a nesting loop? If not ignore this.

2265

This next block of nested for loops looks similar to the above best pair code (that is also part of this patch). Could you possibly refactor it to reuse this same pattern in a helper?

2296

Add comment explaining the reason for having 2 reassociate steps. Also consider assining 2 to a value, call it const unsigned MaxReassociatreSteps = 2; And put the comment with the assignment.

2334

Could we populate the pair map prior to the first iteration? If so, Move this before the loop starts.

escha updated this revision to Diff 125201.Dec 1 2017, 1:04 PM

Updated patch and added comments.

My main change is that it no longer runs reassociate twice; it turns out despite my intuition this helps EXTREMELY little on my shader test suite! It looks like it's better to just build the pair map at the start even if it's not 100% accurate.

scanon added a subscriber: scanon.Dec 4 2017, 11:13 AM
arsenm added inline comments.Dec 4 2017, 11:15 AM
lib/Transforms/Scalar/Reassociate.cpp
2253

Capitalize

2271

I'm not sure what std::less on a Value * means. Is this sorting by pointer value?

escha added inline comments.Dec 4 2017, 11:17 AM
lib/Transforms/Scalar/Reassociate.cpp
2271

yes. it's canonically ordering them so we don't have to worry about checking both [a,b] and [b,a].

qcolombet added inline comments.
lib/Transforms/Scalar/Reassociate.cpp
2271

We would rather use getComplexity(Value *V) for that.

qcolombet added inline comments.Dec 4 2017, 11:43 AM
lib/Transforms/Scalar/Reassociate.cpp
2271

(Though you'll probably still want something to break ties)

escha added inline comments.Dec 4 2017, 12:52 PM
lib/Transforms/Scalar/Reassociate.cpp
2271

(talked about this one offline: conclusion was std:less is okay, I think, so long as there's no ordering dependency, which there shouldn't be)

escha added a comment.Dec 12 2017, 8:25 AM

got the approval from puyan so i'm gonna push this later unless someone else has some other comments!

This revision is now accepted and ready to land.Dec 12 2017, 10:26 AM

Thanks @escha ! lgtm too :)

A couple minor comments (feel free to address as you commit).

include/llvm/Transforms/Scalar/Reassociate.h
77

Can you make GlobalReassociateLimit a cl::opt (in case we feel like experimenting with tuning it at some point)?

lib/Transforms/Scalar/Reassociate.cpp
2242

This comment is now out of date (reassociate is not run first now, right?).

fhahn closed this revision.Dec 19 2017, 2:08 AM

Looks like this has been committed in rL320515