Ryujinx/ARMeilleure/CodeGen/RegisterAllocators/CopyResolver.cs
FICTURE7 69093cf2d6
Optimize LSRA (#2563)
* Optimize `TryAllocateRegWithtoutSpill` a bit

* Add a fast path for when all registers are live.
* Do not query `GetOverlapPosition` if the register is already in use
  (i.e: free position is 0).

* Do not allocate child split list if not parent

* Turn `LiveRange` into a reference struct

`LiveRange` is now a reference wrapping struct like `Operand` and
`Operation`.

It has also been changed into a singly linked-list. In micro-benchmarks
traversing the linked-list was faster than binary search on `List<T>`.
Even for quite large input sizes (e.g: 1,000,000), surprisingly.

Could be because the code gen for traversing the linked-list is much
much cleaner and there is no virtual dispatch happening when checking if
intervals overlaps.

* Turn `LiveInterval` into an iterator

The LSRA allocates in forward order and never inspect previous
`LiveInterval` once they are expired. Something similar can be done for
the `LiveRange`s within the `LiveInterval`s themselves.

The `LiveInterval` is turned into a iterator which expires `LiveRange`
within it. The iterator is moved forward along with interval walking
code, i.e: AllocateInterval(context, interval, cIndex).

* Remove `LinearScanAllocator.Sources`

Local methods are less susceptible to do allocations than lambdas.

* Optimize `GetOverlapPosition(interval)` a bit

Time complexity should be in O(n+m) instead of O(nm) now.

* Optimize `NumberLocals` a bit

Use the same idea as in `HybridAllocator` to store the visited state
in the MSB of the Operand's value instead of using a `HashSet<T>`.

* Optimize `InsertSplitCopies` a bit

Avoid allocating a redundant `CopyResolver`.

* Optimize `InsertSplitCopiesAtEdges` a bit

Avoid redundant allocations of `CopyResolver`.

* Use stack allocation for `freePositions`

Avoid redundant computations.

* Add `UseList`

Replace `SortedIntegerList` with an even more specialized data
structure. It allocates memory on the arena allocators and does not
require copying use positions when splitting it.

* Turn `LiveInterval` into a reference struct

`LiveInterval` is now a reference wrapping struct like `Operand` and
`Operation`.

The rationale behind turning this in a reference wrapping struct is
because a `LiveInterval` is associated with each local variable, and
these intervals may themselves be split further. I've seen translations
having up to 8000 local variables.

To make the `LiveInterval` unmanaged, a new data structure called
`LiveIntervalList` was added to store child splits. This differs from
`SortedList<,>` because it can contain intervals with the same start
position.

Really wished we got some more of C++ template in C#. :^(

* Optimize `GetChildSplit` a bit

No need to inspect the remaining ranges if we've reached a range which
starts after position, since the split list is ordered.

* Optimize `CopyResolver` a bit

Lazily allocate the fill, spill and parallel copy structures since most
of the time only one of them is needed.

* Optimize `BitMap.Enumerator` a bit

Marking `MoveNext` as `AggressiveInlining` allows RyuJIT to promote the
`Enumerator` struct into registers completely, reducing load/store code
a lot since it does not have to store the struct on the stack for ABI
purposes.

* Use stack allocation for `use/blockedPositions`

* Optimize `AllocateWithSpill` a bit

* Address feedback

* Make `LiveInterval.AddRange(,)` more conservative

Produces no diff against master, but just for good measure.
2021-10-08 18:15:44 -03:00

259 lines
8.9 KiB
C#

using ARMeilleure.IntermediateRepresentation;
using System;
using System.Collections.Generic;
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;
using static ARMeilleure.IntermediateRepresentation.Operation.Factory;
namespace ARMeilleure.CodeGen.RegisterAllocators
{
class CopyResolver
{
private class ParallelCopy
{
private struct Copy
{
public Register Dest { get; }
public Register Source { get; }
public OperandType Type { get; }
public Copy(Register dest, Register source, OperandType type)
{
Dest = dest;
Source = source;
Type = type;
}
}
private readonly List<Copy> _copies;
public int Count => _copies.Count;
public ParallelCopy()
{
_copies = new List<Copy>();
}
public void AddCopy(Register dest, Register source, OperandType type)
{
_copies.Add(new Copy(dest, source, type));
}
public void Sequence(List<Operation> sequence)
{
Dictionary<Register, Register> locations = new Dictionary<Register, Register>();
Dictionary<Register, Register> sources = new Dictionary<Register, Register>();
Dictionary<Register, OperandType> types = new Dictionary<Register, OperandType>();
Queue<Register> pendingQueue = new Queue<Register>();
Queue<Register> readyQueue = new Queue<Register>();
foreach (Copy copy in _copies)
{
locations[copy.Source] = copy.Source;
sources[copy.Dest] = copy.Source;
types[copy.Dest] = copy.Type;
pendingQueue.Enqueue(copy.Dest);
}
foreach (Copy copy in _copies)
{
// If the destination is not used anywhere, we can assign it immediately.
if (!locations.ContainsKey(copy.Dest))
{
readyQueue.Enqueue(copy.Dest);
}
}
while (pendingQueue.TryDequeue(out Register current))
{
Register copyDest;
Register origSource;
Register copySource;
while (readyQueue.TryDequeue(out copyDest))
{
origSource = sources[copyDest];
copySource = locations[origSource];
OperandType type = types[copyDest];
EmitCopy(sequence, GetRegister(copyDest, type), GetRegister(copySource, type));
locations[origSource] = copyDest;
if (origSource == copySource && sources.ContainsKey(origSource))
{
readyQueue.Enqueue(origSource);
}
}
copyDest = current;
origSource = sources[copyDest];
copySource = locations[origSource];
if (copyDest != copySource)
{
OperandType type = types[copyDest];
type = type.IsInteger() ? OperandType.I64 : OperandType.V128;
EmitXorSwap(sequence, GetRegister(copyDest, type), GetRegister(copySource, type));
locations[origSource] = copyDest;
Register swapOther = copySource;
if (copyDest != locations[sources[copySource]])
{
// Find the other swap destination register.
// To do that, we search all the pending registers, and pick
// the one where the copy source register is equal to the
// current destination register being processed (copyDest).
foreach (Register pending in pendingQueue)
{
// Is this a copy of pending <- copyDest?
if (copyDest == locations[sources[pending]])
{
swapOther = pending;
break;
}
}
}
// The value that was previously at "copyDest" now lives on
// "copySource" thanks to the swap, now we need to update the
// location for the next copy that is supposed to copy the value
// that used to live on "copyDest".
locations[sources[swapOther]] = copySource;
}
}
}
private static void EmitCopy(List<Operation> sequence, Operand x, Operand y)
{
sequence.Add(Operation(Instruction.Copy, x, y));
}
private static void EmitXorSwap(List<Operation> sequence, Operand x, Operand y)
{
sequence.Add(Operation(Instruction.BitwiseExclusiveOr, x, x, y));
sequence.Add(Operation(Instruction.BitwiseExclusiveOr, y, y, x));
sequence.Add(Operation(Instruction.BitwiseExclusiveOr, x, x, y));
}
}
private Queue<Operation> _fillQueue = null;
private Queue<Operation> _spillQueue = null;
private ParallelCopy _parallelCopy = null;
public bool HasCopy { get; private set; }
public void AddSplit(LiveInterval left, LiveInterval right)
{
if (left.Local != right.Local)
{
throw new ArgumentException("Intervals of different variables are not allowed.");
}
OperandType type = left.Local.Type;
if (left.IsSpilled && !right.IsSpilled)
{
// Move from the stack to a register.
AddSplitFill(left, right, type);
}
else if (!left.IsSpilled && right.IsSpilled)
{
// Move from a register to the stack.
AddSplitSpill(left, right, type);
}
else if (!left.IsSpilled && !right.IsSpilled && left.Register != right.Register)
{
// Move from one register to another.
AddSplitCopy(left, right, type);
}
else if (left.SpillOffset != right.SpillOffset)
{
// This would be the stack-to-stack move case, but this is not supported.
throw new ArgumentException("Both intervals were spilled.");
}
}
private void AddSplitFill(LiveInterval left, LiveInterval right, OperandType type)
{
if (_fillQueue == null)
{
_fillQueue = new Queue<Operation>();
}
Operand register = GetRegister(right.Register, type);
Operand offset = Const(left.SpillOffset);
_fillQueue.Enqueue(Operation(Instruction.Fill, register, offset));
HasCopy = true;
}
private void AddSplitSpill(LiveInterval left, LiveInterval right, OperandType type)
{
if (_spillQueue == null)
{
_spillQueue = new Queue<Operation>();
}
Operand offset = Const(right.SpillOffset);
Operand register = GetRegister(left.Register, type);
_spillQueue.Enqueue(Operation(Instruction.Spill, default, offset, register));
HasCopy = true;
}
private void AddSplitCopy(LiveInterval left, LiveInterval right, OperandType type)
{
if (_parallelCopy == null)
{
_parallelCopy = new ParallelCopy();
}
_parallelCopy.AddCopy(right.Register, left.Register, type);
HasCopy = true;
}
public Operation[] Sequence()
{
List<Operation> sequence = new List<Operation>();
if (_spillQueue != null)
{
while (_spillQueue.TryDequeue(out Operation spillOp))
{
sequence.Add(spillOp);
}
}
_parallelCopy?.Sequence(sequence);
if (_fillQueue != null)
{
while (_fillQueue.TryDequeue(out Operation fillOp))
{
sequence.Add(fillOp);
}
}
return sequence.ToArray();
}
private static Operand GetRegister(Register reg, OperandType type)
{
return Register(reg.Index, reg.Type, type);
}
}
}