On 12/23/2011 06:46 AM, Richard Sandiford wrote:
So it looks like two pieces of work related to scheduling and register
pressure are being posted close together.  This one is an RFC for a less
aggressive form of -fsched-pressure.  I think it should complement
rather than compete with Bernd's IRA patch.  It seems like a good idea
to take register pressure into account during the first scheduling pass,
where we can still easily look at things like instruction latencies
and pipeline utilisation.  Better rematerialisation in the register
allocator would obviously be a good thing too though.

This patch started when we (Linaro) saw a big drop in performance
from vectorising an RGB to YIQ filter on ARM.  The first scheduling
pass was overly aggressive in creating a "wide" schedule, and caused
the newly-vectorised loop to contain lots of spills.  The loop grew so
big that it even had a constant pool in the middle of it.

-fsched-pressure did a very good job on this loop, creating far fewer
spills and consequently avoiding the constant pool.  However, it seemed
to make several other cases significantly worse.  The idea was therefore
to try to come up with a form of -fsched-pressure that could be turned
on for ARM by default.

Current -fsched-pressure works by assigning an "excess (pressure) cost change"
to each instruction; here I'll write that as ECC(X).  -fsched-pressure also
changes the way that the main list scheduler handles stalls due to data
dependencies.  If an instruction would stall for N cycles, the scheduler
would normally add it to the "now+N" queue, then add it to the ready queue
after N cycles.  With -fsched-pressure, it instead adds the instruction
to the ready queue immediately, while still recording that the instruction
would require N stalls.  I'll write the number of stalls on X as delay(X).

This arrangement allows the scheduler to choose between increasing register
pressure and introducing a deliberate stall.  Instructions are ranked by:

   (a) lowest ECC(X) + delay(X)
   (b) lowest delay(X)
   (c) normal list-scheduler ranking (based on things like INSN_PRIORITY)

Note that since delay(X) is measured in cycles, ECC(X) is effectively
measured in cycles too.

Several things seemed to be causing the degradations we were seeing
with -fsched-pressure:

   (1) The -fsched-pressure schedule is based purely on instruction latencies
       and issue rate; it doesn't take the DFA into account.  This means that
       we attempt to "dual issue" things like vector operations, loads and
       stores on Cortex A8 and A9.  In the examples I looked at, these sorts
       of inaccuracy seemed to accumulate, so that the value of delay(X)
       became based on slightly unrealistic cycle times.

       Note that this also affects code that doesn't have any pressure
       problems; it isn't limited to code that does.

       This may simply be historical.  It became much easier to use the
       DFA here after Bernd's introduction of prune_ready_list, but the
       original -fsched-pressure predates that.

   (2) We calculate ECC(X) by walking the unscheduled part of the block
       in its original order, then recording the pressure at each instruction.
       This seemed to make ECC(X) quite sensitive to that original order.
       I saw blocks that started out naturally "narrow" (not much ILP,
       e.g. from unrolled loops) and others that started naturally "wide"
       (a lot of ILP, such as in the libav h264 code), and walking the
       block in order meant that the two styles would be handled differently.

   (3) When calculating the pressure of the original block (as described
       in (2)), we ignore the deaths of registers that are used by more
       than one unscheduled instruction.  This tended to hurt long(ish)
       loops in things like filters, where the same value is often used
       as an input to two calculations.  The effect was that instructions
       towards the end of the block would appear to have very high pressure.
       This in turn made the algorithm very conservative; it wouldn't
       promote instructions from later in the block because those
       instructions seemed to have a prohibitively large cost.

       I asked Vlad about this, and he confirmed that it was a deliberate
       decision.  He'd tried honouring REG_DEAD notes instead, but it
       produced worse results on x86.  I'll return to this at the end.

   (4) ECC(X) is based on the pressure over and above ira_available_class_regs
       (the number of allocatable registers in a given class).  ARM has 14
       allocatable GENERAL_REGS: 16 minus the stack pointer and program
       counter.  So if 14 integer variables are live across a loop but
       not referenced within it, we end up scheduling that loop in a context
       of permanent pressure.  Pressure becomes the overriding concern,
       and we don't get much ILP.

       I suppose there are at least two ways of viewing this:

       (4a) We're giving an equal weight to:

           (Ra) registers that are live across a loop but not used within it
                (and which should therefore require no spill code in the
                loop itself)

           (Rb) registers that hold loop invariants (and so should only
                require loads in the loop itself)

           (Rc) registers that are used and set within the loop (and so would
                require both loads and stores in the loop itself)

           We effectively treat everything as (Rc).

       (4b) We always work to ira_available_class_regs, rather than to
           an estimate of what maximum pressure is realistically achievable.
           If a block has something like:

               A: R2 = *R1
                  ...
               B: R3 = *(R1 + 4)
                  ...
               C: R1 = R1 + 8
                  ...
               D: R4 = R2 + R3
                  ...

           then it can't help but have three registers live at once.

The first thing I tried was to change just (4a) in isolation.
Taking (Ra) out of the pressure estimation produced some benefits,
but not enough.  I then tried a "staging" of costs, so that:

   (Ra) had the cost of a load and store in the outer loop (if any)
   (Rb) had the cost of a load in the inner loop (or the cost of (Ra) if larger)
   (Rc) had the cost of a load and store in the inner loop ( " " )

These costs were still based on MEMORY_MOVE_COST, just like the current
ones are.  However, MEMORY_MOVE_COST is defined to be relative to
REGISTER_MOVE_COST, and REGISTER_MOVE_COST is defined to be 2 for a
"simple" move.  Since ECC(X) is effectively a cycle count, it seemed
better to divide the cost by 2.

This again produced some benefit, but not enough.  I think part of the
problem is that ARM's MEMORY_MOVE_COST is very high: it says that both
loads and stores cost the equivalent of 5 register moves, making the cost
of a full spill equivalent to 10 register moves.  ARM also makes these
costs independent of the mode, so that a single SImode load and store
has the same cost as a 4-quad-vector load and store.  This might be
something that's worth looking at, especially since the same costs
influence spilling decisions in the register allocator.

Yes, the accuracy of the costs was always a problem. I saw it for some targets.
However, even reducing the cost of a load to 4 moves and a store to 2
moves didn't bring enough benefit (although it was better than 5 and 5).
The division of costs above is also a little artificial.  Because we don't
have an integrated register allocator and scheduler, there's not really
any telling how many times the block will need to load or store a value.
An invariant might be used many times, and in such a way that several
loads are needed, while another variable might be involved in a single
read-modify-write sequence.  The true cost also depends on how easily
the spill instructions fit into the surrounding code.

Yes, there is also inheritance in reload. It is hard to say will reload to reuse already loaded value.
On a different tack, I tried to tackle (1) by taking the DFA into account.
If delay(X) is zero, but X cannot issue due to resource hazards, I set
delay(X) to 1.  Again this wasn't enough in itself, although it does
still form an important part of the "final" proposed version.

I tried the algorithms from a few papers, but they too tended to be
overly conservative or to suffer from degenerate behaviour in filters.

This area had no real serious attention of the researches, although it is quite important for some targets. I got this impression long ago. Some algorithms (e.g. one mentioned in Muchnik's book) would degrade the code instead, I believe.
In the end I tried an ad-hoc approach in an attempt to do something
about (2), (3) and (4b).  The idea was to construct a preliminary
"model" schedule in which the only objective is to keep register
pressure to a minimum.  This schedule ignores pipeline characteristics,
latencies, and the number of available registers.  The maximum pressure
seen in this initial model schedule (MP) is then the benchmark for ECC(X).

I always had an impression that the code before scheduler is close to minimal register pressure because of specific expression generation. May be I was wrong and some optimizations (global ones like pre) changes this a lot.
During the main scheduling, an instruction X that occurs at or before
the next point of maximum pressure in the model schedule is measured
based on the current register pressure.  If X doesn't increase the
current pressure beyond the current maximum, its ECC(X) is zero,
otherwise ECC(X) is the cost of going from MP to the new maximum.
The idea is that the final net pressure of scheduling a given set of
instructions is going to be the same regardless of the order; we simply
want to keep the intermediate pressure under control.  An ECC(X) of zero
usually[*] means that scheduling X next won't send the rest of the
sequence beyond the current maximum pressure.

   [*] but not always.  There's more about this in the patch comments.

If an instruction X occurs _after_ the next point of maximum pressure,
X is measured based on that maximum pressure.  If the current maximum
pressure is MP', and X increases pressure by dP, ECC(X) is the cost of
going from MP to MP' + dP.

Of course, this all depends on how good a value MP is, and therefore
on how good the model schedule is.  I tried a few variations before
settling on the one in the patch (which I hope makes conceptual sense).

I initially stayed with the idea above about assigning different costs to
(Ra), (Rb) and (Rc).  This produces some good results, but was still a
little too conservative in general, in that other tests were still worse
with -fsched-pressure than without.  I described some of the problems
with these costs above.  Another is that if an instruction X has a spill
cost of 6, say, then:

   ECC(X) + delay(X)

will only allow X to be scheduled if the next instruction without
a spill cost has a delay of 6 cycles or more.  This is overly harsh,
especially seeing as few ARM instructions have such a high latency.
The benefit of spilling is often to avoid a series of short
(e.g. single-cycle) stalls, rather than to avoid a single long one.

I then adjusted positive ECC(X) values based on the priority of X
relative to the highest-priority zero-cost instruction.  This was
better, but a DES filter in particular still suffered from the
"lots of short stalls" problem.

Then, as an experiment, I tried ignoring MEMORY_MOVE_COST altogether
and simply treating each extra spill as having a cost of 1.  Somewhat
to my surprise, there was only one test in which some improvement was
lost; that test was now only slightly better than without -fsched-pressure.
But the fixed cost of 1 per spill achieved the goal of being as good as
or better than -fno-sched-pressure in almost all cases, while still being
significantly better in some of the cases we care about.

Assigning a cost of just 1 to each excess spill is clearly going to
interfere with the normal list scheduler less often than assigning each
spill a higher cost.  Given that this appraoch also gives the most important
benefits, it felt like a good default.

We should use whatever works better but I always try to find an explanation and reason for some unexpected results. If it is OOO processor I have no problem with understanding this.
All in all, the patch is a complicated way of being quite timid,
Yes, it is a bit scaring me that pace of changes in scheduler would make it complicated as reload. On the other hand, the pass is one of the most target dependent and solves NP-problem. I think it is hard to generate a good code without some complications, except of course an optimal solution approach but it hardly sometime in future will be acceptable with compilation time point of view.
but I hope it could also be used as a basis for more aggressive
versions in future.  Things I haven't looked at include whether
it would make sense to disable the priority-based adjustment of
ECC for -Os.  (That's a question of whether this patch can improve
size still further over -fno-sched-pressure.)

Being timid ought to make it fit quite well with Bernd's patch,
although I haven't had chance to try the two together.

I talked with Vlad about this a couple of months ago (thanks again for
the help, Vlad).  He explained that some of the things I was seeing --
especially (3) -- were deliberate decisions that had improved the
results for x86.  And I think it's probably true that the patch below
is only going to work well on fairly regular RISCy machines.

For that reason, the patch adds an -fsched-pressure-algorithm=
option to choose between the two versions.  For historical reasons
I called the current algorithm "weighted" and the proposed one "model",
but I'm sure there are better names.  I've kept the option undocumented
because it's really an internal thing.  "weighted" remains the default.

Of course, adding an alternative like this would only be acceptable if
-fsched-pressure and -fsched-pressure-algorithm=model became the default
for ARM (or another target).  That's a decision for the ARM maintainers;
I've sent Ramana the results I got, which unfortunately I can't share
more widely.

I guess the result is positive, otherwise you would not submit the patch. Although I place the performance improvement always first, it would be interesting to know how the compilation time changes.
If anyone does have time to test this on their favourite port,
I'd really appreciate it.  I already know that it doesn't perform
well on SPECFP for rs6000 because of the choice of pressure classes;
I'll send a separate note about that so that it doesn't get lost in
this long essay.

Yes, before I submitted coloring without cover classes, we had 1-1 relation of cover classes and register pressure classes. Cover-classes were an approximation of classes should be used for pseudos. Coloring without cover classes makes more accurate coloring but there is no 1-1 relation with pressure classes anymore. It is not strange to me that biggest performance improvement I saw on SPECFP2000 on ppc when I submitted the patch.
Also, print_pseudo_costs segfaults when -fsched-pressure and dumping
are enabled.  I'm planning to send a patch for that at some point;
it's a regression from 4.6, so seems like stage 3 material.
In the meantime, a simple workaround is to stick "return;"
at the beginning of the function.


Richard, thanks for working on these problems. I thought about some of them but I never had time to address them. In overall, I have a very positive opinion about your systematic approach.

As for the patch itself, I look at this with more attention at the beginning of next year. As I understand there is no rush with that because we are still not at the stage 1.

Once again, thanks for your work.

Reply via email to