mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-10 17:16:47 -04:00
902 lines
34 KiB
C#
902 lines
34 KiB
C#
using System;
|
|
using System.Buffers.Binary;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using Translator.Core.Ir;
|
|
using Translator.Core.Loading;
|
|
using Translator.Core.Translation;
|
|
using Xunit;
|
|
|
|
namespace Translator.Tests;
|
|
|
|
/// <summary>
|
|
/// IR-level leaf inlining (performance audit T-INLINE). The splice happens before SSA, so every
|
|
/// negative case is a silent-corruption bug if it starts firing; hence more rejection tests than positive ones.
|
|
/// </summary>
|
|
public class LeafInliningTests
|
|
{
|
|
private const uint Caller = MemoryLayout.RamBase;
|
|
private const uint Callee = MemoryLayout.RamBase + 0x100;
|
|
private const uint Callee2 = MemoryLayout.RamBase + 0x180;
|
|
private const int ImageSize = 0x400;
|
|
|
|
// --- minimal PowerPC encoder -------------------------------------------
|
|
|
|
private static uint Blr() => 0x4E800020u;
|
|
private static uint Bl(uint from, uint to) => 0x48000001u | ((to - from) & 0x03FFFFFCu);
|
|
private static uint B(uint from, uint to) => 0x48000000u | ((to - from) & 0x03FFFFFCu);
|
|
private static uint Bne(uint from, uint to) => 0x40820000u | ((to - from) & 0xFFFCu);
|
|
private static uint Addi(int d, int a, int simm) =>
|
|
0x38000000u | ((uint)d << 21) | ((uint)a << 16) | (uint)(simm & 0xFFFF);
|
|
private static uint Lwz(int d, int a, int offset) =>
|
|
0x80000000u | ((uint)d << 21) | ((uint)a << 16) | (uint)(offset & 0xFFFF);
|
|
private static uint Stwu(int s, int a, int offset) =>
|
|
0x94000000u | ((uint)s << 21) | ((uint)a << 16) | (uint)(offset & 0xFFFF);
|
|
private static uint Add(int d, int a, int b) =>
|
|
0x7C000214u | ((uint)d << 21) | ((uint)a << 16) | ((uint)b << 11);
|
|
private static uint Cmpwi(int crf, int a, int simm) =>
|
|
0x2C000000u | ((uint)crf << 23) | ((uint)a << 16) | (uint)(simm & 0xFFFF);
|
|
private static uint Mflr(int d) => 0x7C0802A6u | ((uint)d << 21);
|
|
private static uint Bctrl() => 0x4E800421u;
|
|
private static uint Bctr() => 0x4E800420u;
|
|
|
|
private static ProgramImage BuildImage(params (uint Address, uint Word)[] words)
|
|
{
|
|
var memory = new byte[ImageSize];
|
|
foreach (var (address, word) in words)
|
|
{
|
|
BinaryPrimitives.WriteUInt32BigEndian(
|
|
memory.AsSpan((int)(address - MemoryLayout.RamBase), 4), word);
|
|
}
|
|
|
|
return new ProgramImage(
|
|
memory,
|
|
AddressRange.FromStartAndSize(MemoryLayout.RamBase, ImageSize),
|
|
AddressRange.FromStartAndSize(MemoryLayout.RamBase, ImageSize),
|
|
default,
|
|
"leaf-inlining");
|
|
}
|
|
|
|
private static TranslationOptions Options(
|
|
bool inlining = true,
|
|
IReadOnlySet<uint>? blocked = null,
|
|
int maxCalleeInstructions = 32,
|
|
int maxGrowthPercent = 50,
|
|
bool multiBlock = true) =>
|
|
TranslationOptions.Default with
|
|
{
|
|
AllowUnsupportedInstructions = true,
|
|
KnownFunctionEntryPoints = new HashSet<uint> { Caller, Callee, Callee2 },
|
|
EnableLeafInlining = inlining,
|
|
LeafInliningBlockedTargets = blocked,
|
|
LeafInliningMaxCalleeInstructions = maxCalleeInstructions,
|
|
LeafInliningMaxCallerGrowthPercent = maxGrowthPercent,
|
|
LeafInliningAllowMultiBlockCallees = multiBlock
|
|
};
|
|
|
|
private static FunctionTranslationResult Translate(
|
|
ProgramImage image, uint entryPoint, TranslationOptions options) =>
|
|
new FunctionTranslator(image).Translate(entryPoint, options);
|
|
|
|
/// <summary>Caller that calls the leaf once and returns.</summary>
|
|
private static (uint Address, uint Word)[] SimpleCaller(uint callee = Callee) =>
|
|
[
|
|
(Caller + 0, Addi(3, 3, 4)),
|
|
(Caller + 4, Bl(Caller + 4, callee)),
|
|
(Caller + 8, Addi(3, 3, 1)),
|
|
(Caller + 12, Blr())
|
|
];
|
|
|
|
// --- positive cases -----------------------------------------------------
|
|
|
|
[Fact]
|
|
public void CalleeWritesLandInTheCallerAndTheCallDisappears()
|
|
{
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Lwz(3, 3, 8)),
|
|
(Callee + 4, Blr())
|
|
]);
|
|
|
|
var inlined = Translate(image, Caller, Options());
|
|
var reference = Translate(image, Caller, Options(inlining: false));
|
|
|
|
Assert.Equal(1, inlined.Metrics.InlinedCallSites);
|
|
Assert.Equal(2, inlined.Metrics.InlinedGuestInstructions);
|
|
Assert.Equal(0, reference.Metrics.InlinedCallSites);
|
|
Assert.Contains("InvokeDirectCpu<0x80000100u>(ctx);", reference.CxxCode, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("InvokeDirectCpu<0x80000100u>(ctx);", inlined.CxxCode);
|
|
// The callee's load is now a load of the caller's own r3.
|
|
Assert.Contains("MemoryInline::FlatRead32", inlined.CxxCode, StringComparison.Ordinal);
|
|
Assert.Contains("inline leaf 0x80000100", inlined.CxxCode, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void ArgumentRegistersAreReadFromTheCallerStateAtTheCallPoint()
|
|
{
|
|
// The caller computes r3 = r3 + 4 immediately before the call, and the
|
|
// callee's first act is to consume r3. After the splice, the callee's
|
|
// read must see the +4 value without any context round trip.
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Add(3, 3, 4)),
|
|
(Callee + 4, Blr())
|
|
]);
|
|
|
|
var result = Translate(image, Caller, Options());
|
|
|
|
Assert.Equal(1, result.Metrics.InlinedCallSites);
|
|
// Residency keeps r3 in a local across the splice, so there is no flush
|
|
// or reload of the guest register file where the call used to be.
|
|
Assert.DoesNotContain("InvokeDirectCpu<0x80000100u>(ctx);", result.CxxCode);
|
|
Assert.Contains("r3 = ", result.CxxCode, StringComparison.Ordinal);
|
|
var ir = result.LinearIr.Blocks.SelectMany(block => block.Instructions).ToArray();
|
|
Assert.DoesNotContain(ir, instruction => instruction is IrCall call && call.Target == "0x80000100");
|
|
}
|
|
|
|
[Fact]
|
|
public void ConditionRegisterWrittenInsideTheSpliceReachesTheCallersBranch()
|
|
{
|
|
// The callee sets CR0; the caller branches on it. Fusing the two across
|
|
// the splice is only possible because the compare became an ordinary
|
|
// instruction of the caller.
|
|
var image = BuildImage(
|
|
[
|
|
(Caller + 0, Bl(Caller + 0, Callee)),
|
|
(Caller + 4, Bne(Caller + 4, Caller + 12)),
|
|
(Caller + 8, Addi(3, 3, 1)),
|
|
(Caller + 12, Blr()),
|
|
(Callee + 0, Cmpwi(0, 4, 0)),
|
|
(Callee + 4, Blr())
|
|
]);
|
|
|
|
var inlined = Translate(image, Caller, Options());
|
|
var reference = Translate(image, Caller, Options(inlining: false));
|
|
|
|
Assert.Equal(1, inlined.Metrics.InlinedCallSites);
|
|
// Without inlining the branch has to reload the architectural CR after
|
|
// the call because the producing compare lives in another function.
|
|
Assert.Contains("InvokeDirectCpu<0x80000100u>(ctx);", reference.CxxCode, StringComparison.Ordinal);
|
|
Assert.Equal(2, CountOccurrences(reference.CxxCode, "cr = ctx->cr;"));
|
|
// With inlining the compare writes the caller's resident CR directly and
|
|
// the branch reads it with no context round trip at all.
|
|
Assert.Contains(
|
|
"SetCRResident(cr, xer, 0, static_cast<int32_t>(r4), static_cast<int32_t>(0));",
|
|
inlined.CxxCode, StringComparison.Ordinal);
|
|
Assert.Equal(1, CountOccurrences(inlined.CxxCode, "cr = ctx->cr;"));
|
|
}
|
|
|
|
[Fact]
|
|
public void CompareInsideTheSpliceIsFusedIntoTheFollowingBranch()
|
|
{
|
|
// Both successors immediately overwrite CR0, which is what makes the
|
|
// spliced compare's only consumer the caller's branch.
|
|
var image = BuildImage(
|
|
[
|
|
(Caller + 0, Bl(Caller + 0, Callee)),
|
|
(Caller + 4, Bne(Caller + 4, Caller + 16)),
|
|
(Caller + 8, Cmpwi(0, 3, 0)),
|
|
(Caller + 12, Blr()),
|
|
(Caller + 16, Cmpwi(0, 5, 0)),
|
|
(Caller + 20, Blr()),
|
|
(Callee + 0, Cmpwi(0, 4, 7)),
|
|
(Callee + 4, Blr())
|
|
]);
|
|
|
|
var code = Translate(image, Caller, Options()).CxxCode;
|
|
|
|
Assert.Contains(
|
|
"if ((static_cast<int32_t>(r4) != static_cast<int32_t>(7)))",
|
|
code, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheLinkRegisterWriteOfAnInlinedCallIsNotEmitted()
|
|
{
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Addi(4, 4, 1)),
|
|
(Callee + 4, Blr())
|
|
]);
|
|
|
|
var code = Translate(image, Caller, Options()).CxxCode;
|
|
|
|
Assert.DoesNotContain("ctx->lr = 0x80000008", code);
|
|
Assert.DoesNotContain("lr = 0x80000008u", code);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheStandaloneCalleeBodyIsStillTranslatedOnItsOwn()
|
|
{
|
|
// Indirect dispatch, mod overrides and the registry all resolve the
|
|
// callee by address, so the splice must never be a replacement for it.
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Lwz(3, 3, 8)),
|
|
(Callee + 4, Blr())
|
|
]);
|
|
|
|
var standalone = Translate(image, Callee, Options());
|
|
|
|
Assert.Contains("func_80000100", standalone.CxxCode, StringComparison.Ordinal);
|
|
Assert.Equal(0, standalone.Metrics.InlinedCallSites);
|
|
}
|
|
|
|
[Fact]
|
|
public void EveryCallSiteOfTheSameCalleeIsSplicedIndependently()
|
|
{
|
|
var image = BuildImage(
|
|
[
|
|
(Caller + 0, Bl(Caller + 0, Callee)),
|
|
(Caller + 4, Bl(Caller + 4, Callee)),
|
|
(Caller + 8, Blr()),
|
|
(Callee + 0, Lwz(3, 3, 8)),
|
|
(Callee + 4, Blr())
|
|
]);
|
|
|
|
var result = Translate(image, Caller, Options());
|
|
|
|
Assert.Equal(2, result.Metrics.InlinedCallSites);
|
|
Assert.Equal(2, CountOccurrences(result.CxxCode, "inline leaf 0x80000100"));
|
|
Assert.DoesNotContain("InvokeDirectCpu<0x80000100u>(ctx);", result.CxxCode);
|
|
}
|
|
|
|
[Fact]
|
|
public void TailCallToALeafIsSplicedAheadOfTheCallersReturn()
|
|
{
|
|
var image = BuildImage(
|
|
[
|
|
(Caller + 0, Addi(3, 3, 4)),
|
|
(Caller + 4, B(Caller + 4, Callee)),
|
|
(Callee + 0, Lwz(3, 3, 8)),
|
|
(Callee + 4, Blr())
|
|
]);
|
|
|
|
var result = Translate(image, Caller, Options());
|
|
|
|
Assert.Equal(1, result.Metrics.InlinedCallSites);
|
|
Assert.DoesNotContain("InvokeDirectCpu<0x80000100u>(ctx);", result.CxxCode);
|
|
}
|
|
|
|
// --- acyclic multi-block callees ----------------------------------------
|
|
|
|
/// <summary>Compare, branch, one arm, join - the shape of nw4r list walkers.</summary>
|
|
private static (uint Address, uint Word)[] DiamondCallee() =>
|
|
[
|
|
(Callee + 0, Cmpwi(0, 3, 0)),
|
|
(Callee + 4, Bne(Callee + 4, Callee + 12)),
|
|
(Callee + 8, Addi(3, 3, 1)),
|
|
(Callee + 12, Blr())
|
|
];
|
|
|
|
[Fact]
|
|
public void AnAcyclicMultiBlockCalleeIsSplicedWithItsOwnControlFlow()
|
|
{
|
|
var image = BuildImage([.. SimpleCaller(), .. DiamondCallee()]);
|
|
|
|
var result = Translate(image, Caller, Options());
|
|
|
|
Assert.Equal(1, result.Metrics.InlinedCallSites);
|
|
Assert.Equal(4, result.Metrics.InlinedGuestInstructions);
|
|
Assert.DoesNotContain("InvokeDirectCpu<0x80000100u>(ctx);", result.CxxCode);
|
|
// The callee's blocks are now blocks of the caller, under call-site
|
|
// local labels, and the branch skips the arm exactly as it did inside
|
|
// the callee.
|
|
Assert.Contains("loc_inl0_0x80000100:", result.CxxCode, StringComparison.Ordinal);
|
|
Assert.Contains("loc_inl0_0x80000108:", result.CxxCode, StringComparison.Ordinal);
|
|
Assert.Contains("loc_inl0_0x8000010C:", result.CxxCode, StringComparison.Ordinal);
|
|
Assert.Contains("goto loc_inl0_0x8000010C;", result.CxxCode, StringComparison.Ordinal);
|
|
// The callee's return became the caller's continuation, which is where
|
|
// the code after the call site now lives.
|
|
Assert.Contains("loc_inl0_cont_80000100:", result.CxxCode, StringComparison.Ordinal);
|
|
Assert.Equal(
|
|
new IrJump("inl0_cont_80000100"),
|
|
Block(result, "inl0_0x8000010C").Instructions[^1]);
|
|
result.Ssa.ValidateUseDef();
|
|
}
|
|
|
|
[Fact]
|
|
public void TheCallersOwnCodeAroundAMultiBlockSpliceStillRuns()
|
|
{
|
|
// The call site sits in the middle of the caller's only block, so the
|
|
// splice has to cut that block in two and keep both halves wired up.
|
|
var image = BuildImage([.. SimpleCaller(), .. DiamondCallee()]);
|
|
|
|
var result = Translate(image, Caller, Options());
|
|
var labels = result.LinearIr.Blocks.Select(block => block.Label).ToArray();
|
|
|
|
// Entry block keeps its label, so the function still starts where the
|
|
// emitter's prologue jumps.
|
|
Assert.Equal("0x80000000", labels[0]);
|
|
Assert.Equal(result.LinearIr.EntryLabel, labels[0]);
|
|
Assert.Equal(labels.Length, labels.Distinct(StringComparer.OrdinalIgnoreCase).Count());
|
|
// Caller prefix, the callee's three blocks, then the continuation that
|
|
// carries everything the caller did after the call.
|
|
Assert.Equal(
|
|
[
|
|
"0x80000000",
|
|
"inl0_0x80000100",
|
|
"inl0_0x80000104",
|
|
"inl0_0x80000108",
|
|
"inl0_0x8000010C",
|
|
"inl0_cont_80000100"
|
|
],
|
|
labels);
|
|
Assert.IsType<IrReturn>(result.LinearIr.Blocks[^1].Instructions[^1]);
|
|
}
|
|
|
|
[Fact]
|
|
public void EachMultiBlockCallSiteGetsItsOwnCopyOfTheCalleesLabels()
|
|
{
|
|
var image = BuildImage(
|
|
[
|
|
(Caller + 0, Bl(Caller + 0, Callee)),
|
|
(Caller + 4, Bl(Caller + 4, Callee)),
|
|
(Caller + 8, Blr()),
|
|
.. DiamondCallee()
|
|
]);
|
|
|
|
var result = Translate(image, Caller, Options(maxGrowthPercent: 100_000));
|
|
|
|
Assert.Equal(2, result.Metrics.InlinedCallSites);
|
|
Assert.Contains("loc_inl0_cont_80000100:", result.CxxCode, StringComparison.Ordinal);
|
|
Assert.Contains("loc_inl1_cont_80000100:", result.CxxCode, StringComparison.Ordinal);
|
|
result.Ssa.ValidateUseDef();
|
|
}
|
|
|
|
[Fact]
|
|
public void SplicedLabelsCanNeverBeReadBackAsAGuestAddress()
|
|
{
|
|
// A `loc_XXXXXXXX` label is how the emitter reports an interior resume
|
|
// point of the caller. A spliced copy of the callee is not one, so its
|
|
// labels must not parse as an address.
|
|
var image = BuildImage([.. SimpleCaller(), .. DiamondCallee()]);
|
|
|
|
var result = Translate(image, Caller, Options());
|
|
|
|
var spliced = result.LinearIr.Blocks
|
|
.Select(block => block.Label)
|
|
.Where(label => label.StartsWith("inl", StringComparison.Ordinal))
|
|
.ToArray();
|
|
Assert.NotEmpty(spliced);
|
|
foreach (var label in spliced)
|
|
{
|
|
Assert.False(
|
|
uint.TryParse(label, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _),
|
|
$"Spliced label '{label}' is readable as a guest address.");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ARaisedInstructionCapAdmitsALeafTheOldCapRefused()
|
|
{
|
|
// 45 arithmetic instructions plus the terminating blr: past the old cap
|
|
// of 32, inside the current one.
|
|
var words = new List<(uint, uint)>(SimpleCaller());
|
|
for (var index = 0; index < 45; index++)
|
|
{
|
|
words.Add((Callee + (uint)(index * 4), Addi(3, 3, 1)));
|
|
}
|
|
|
|
words.Add((Callee + 180, Blr()));
|
|
var image = BuildImage([.. words]);
|
|
|
|
Assert.Equal(56, TranslationOptions.Default.LeafInliningMaxCalleeInstructions);
|
|
Assert.Equal(
|
|
1,
|
|
Translate(image, Caller, Options(maxCalleeInstructions: 56, maxGrowthPercent: 100_000))
|
|
.Metrics.InlinedCallSites);
|
|
Assert.Equal(
|
|
0,
|
|
Translate(image, Caller, Options(maxCalleeInstructions: 32, maxGrowthPercent: 100_000))
|
|
.Metrics.InlinedCallSites);
|
|
}
|
|
|
|
// --- rejection cases ----------------------------------------------------
|
|
|
|
[Fact]
|
|
public void AMultiBlockCalleeIsRejectedWhenMultiBlockSplicingIsOff()
|
|
{
|
|
var image = BuildImage([.. SimpleCaller(), .. DiamondCallee()]);
|
|
|
|
Assert.Equal(0, Translate(image, Caller, Options(multiBlock: false)).Metrics.InlinedCallSites);
|
|
Assert.Equal(1, Translate(image, Caller, Options()).Metrics.InlinedCallSites);
|
|
}
|
|
|
|
[Fact]
|
|
public void ACalleeWithALoopIsRejected()
|
|
{
|
|
// The back edge to the entry makes this a loop, not a one-way region
|
|
// between the call point and its continuation.
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Addi(4, 4, -1)),
|
|
(Callee + 4, Cmpwi(0, 4, 0)),
|
|
(Callee + 8, Bne(Callee + 8, Callee + 0)),
|
|
(Callee + 12, Blr())
|
|
]);
|
|
|
|
AssertNotInlined(image);
|
|
}
|
|
|
|
[Fact]
|
|
public void AMultiBlockCalleeThatTouchesTheLinkRegisterIsRejected()
|
|
{
|
|
// Reachable only on one arm, and still disqualifying: the call's own LR
|
|
// write is dropped, so the mflr would read the caller's incoming value.
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Cmpwi(0, 3, 0)),
|
|
(Callee + 4, Bne(Callee + 4, Callee + 12)),
|
|
(Callee + 8, Mflr(0)),
|
|
(Callee + 12, Blr())
|
|
]);
|
|
|
|
AssertNotInlined(image);
|
|
}
|
|
|
|
[Fact]
|
|
public void AMultiBlockCalleeThatCallsAnotherFunctionIsRejected()
|
|
{
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Cmpwi(0, 3, 0)),
|
|
(Callee + 4, Bne(Callee + 4, Callee + 12)),
|
|
(Callee + 8, Bl(Callee + 8, Callee2)),
|
|
(Callee + 12, Blr()),
|
|
(Callee2 + 0, Addi(3, 3, 1)),
|
|
(Callee2 + 4, Blr())
|
|
]);
|
|
|
|
AssertNotInlined(image);
|
|
}
|
|
|
|
[Fact]
|
|
public void AMultiBlockCalleeIsStillSubjectToTheBlockList()
|
|
{
|
|
var image = BuildImage([.. SimpleCaller(), .. DiamondCallee()]);
|
|
|
|
// A patch site on the callee's second block is still a patch site.
|
|
Assert.Equal(
|
|
0,
|
|
Translate(image, Caller, Options(blocked: new HashSet<uint> { Callee + 8 })).Metrics.InlinedCallSites);
|
|
}
|
|
|
|
[Fact]
|
|
public void AdmittingMultiBlockCalleesCannotChangeACallerThatHasNone()
|
|
{
|
|
// The invariant the whole transform rests on: a caller whose admitted
|
|
// set does not change must emit byte-identical text.
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Lwz(3, 3, 8)),
|
|
(Callee + 4, Blr())
|
|
]);
|
|
|
|
var withMultiBlock = Translate(image, Caller, Options());
|
|
var withoutMultiBlock = Translate(image, Caller, Options(multiBlock: false));
|
|
var atTheOldCap = Translate(image, Caller, Options(maxCalleeInstructions: 32));
|
|
var atTheNewCap = Translate(image, Caller, Options(maxCalleeInstructions: 56));
|
|
|
|
Assert.Equal(1, withMultiBlock.Metrics.InlinedCallSites);
|
|
Assert.Equal(withoutMultiBlock.CxxCode, withMultiBlock.CxxCode);
|
|
Assert.Equal(atTheOldCap.CxxCode, atTheNewCap.CxxCode);
|
|
}
|
|
|
|
[Fact]
|
|
public void InliningIsOffUnlessRequested()
|
|
{
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Lwz(3, 3, 8)),
|
|
(Callee + 4, Blr())
|
|
]);
|
|
|
|
var result = Translate(image, Caller, Options(inlining: false));
|
|
|
|
Assert.Equal(0, result.Metrics.InlinedCallSites);
|
|
Assert.Contains("InvokeDirectCpu<0x80000100u>(ctx);", result.CxxCode, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void ACalleeThatCallsAnotherFunctionIsRejected()
|
|
{
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Bl(Callee + 0, Callee2)),
|
|
(Callee + 4, Blr()),
|
|
(Callee2 + 0, Addi(3, 3, 1)),
|
|
(Callee2 + 4, Blr())
|
|
]);
|
|
|
|
AssertNotInlined(image);
|
|
}
|
|
|
|
[Fact]
|
|
public void ACalleeWithAnIndirectCallIsRejected()
|
|
{
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Bctrl()),
|
|
(Callee + 4, Blr())
|
|
]);
|
|
|
|
AssertNotInlined(image);
|
|
}
|
|
|
|
[Fact]
|
|
public void ACalleeWithAnIndirectBranchIsRejected()
|
|
{
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Bctr())
|
|
]);
|
|
|
|
AssertNotInlined(image);
|
|
}
|
|
|
|
[Fact]
|
|
public void ACalleeThatTouchesTheLinkRegisterIsRejected()
|
|
{
|
|
// The call's own LR write is dropped, so a callee that observes LR
|
|
// would read the caller's incoming value instead of a return address.
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Mflr(0)),
|
|
(Callee + 4, Blr())
|
|
]);
|
|
|
|
AssertNotInlined(image);
|
|
}
|
|
|
|
[Fact]
|
|
public void ACalleeThatCreatesAStackFrameIsRejected()
|
|
{
|
|
// Splicing a second r1 epoch into the caller invalidates the stack
|
|
// aliasing model used after inlining, even when the callee restores r1.
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Stwu(1, 1, -16)),
|
|
(Callee + 4, Addi(3, 3, 1)),
|
|
(Callee + 8, Addi(1, 1, 16)),
|
|
(Callee + 12, Blr())
|
|
]);
|
|
|
|
AssertNotInlined(image);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnOversizedCalleeIsRejected()
|
|
{
|
|
var body = new List<(uint, uint)>(SimpleCaller());
|
|
for (var index = 0; index < 8; index++)
|
|
{
|
|
body.Add((Callee + (uint)(index * 4), Addi(3, 3, 1)));
|
|
}
|
|
body.Add((Callee + 32, Blr()));
|
|
|
|
var image = BuildImage([.. body]);
|
|
|
|
Assert.Equal(1, Translate(image, Caller, Options(maxCalleeInstructions: 16)).Metrics.InlinedCallSites);
|
|
Assert.Equal(0, Translate(image, Caller, Options(maxCalleeInstructions: 4)).Metrics.InlinedCallSites);
|
|
}
|
|
|
|
[Fact]
|
|
public void ABlockedAddressAnywhereInsideTheCalleeIsRejected()
|
|
{
|
|
// A native registration or mod patch inside the decoded body means the
|
|
// runtime winner is not these bytes.
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Lwz(3, 3, 8)),
|
|
(Callee + 4, Addi(3, 3, 1)),
|
|
(Callee + 8, Blr())
|
|
]);
|
|
|
|
Assert.Equal(
|
|
0,
|
|
Translate(image, Caller, Options(blocked: new HashSet<uint> { Callee })).Metrics.InlinedCallSites);
|
|
Assert.Equal(
|
|
0,
|
|
Translate(image, Caller, Options(blocked: new HashSet<uint> { Callee + 4 })).Metrics.InlinedCallSites);
|
|
Assert.Equal(1, Translate(image, Caller, Options()).Metrics.InlinedCallSites);
|
|
}
|
|
|
|
[Fact]
|
|
public void GrowthBudgetStopsInliningOnceTheCallerWouldBalloon()
|
|
{
|
|
var words = new List<(uint, uint)>
|
|
{
|
|
(Caller + 0, Bl(Caller + 0, Callee)),
|
|
(Caller + 4, Bl(Caller + 4, Callee)),
|
|
(Caller + 8, Bl(Caller + 8, Callee)),
|
|
(Caller + 12, Blr())
|
|
};
|
|
for (var index = 0; index < 12; index++)
|
|
{
|
|
words.Add((Callee + (uint)(index * 4), Addi(3, 3, 1)));
|
|
}
|
|
words.Add((Callee + 48, Blr()));
|
|
|
|
var image = BuildImage([.. words]);
|
|
|
|
var generous = Translate(image, Caller, Options(maxGrowthPercent: 100_000)).Metrics.InlinedCallSites;
|
|
var stingy = Translate(image, Caller, Options(maxGrowthPercent: 0)).Metrics.InlinedCallSites;
|
|
|
|
Assert.Equal(3, generous);
|
|
Assert.InRange(stingy, 0, 2);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContinuationDispatchDisablesInliningForTheWholeCaller()
|
|
{
|
|
// Non-returning and continuation call sites place labels keyed by guest
|
|
// instruction address inside the emitted body; a duplicated guest
|
|
// instruction stream cannot coexist with that.
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Lwz(3, 3, 8)),
|
|
(Callee + 4, Blr())
|
|
]);
|
|
|
|
var options = Options() with
|
|
{
|
|
LrContinuationCallTargets = new HashSet<uint> { Callee2 }
|
|
};
|
|
|
|
Assert.Equal(0, Translate(image, Caller, options).Metrics.InlinedCallSites);
|
|
}
|
|
|
|
[Fact]
|
|
public void ModuleTranslationsAreNeverInlined()
|
|
{
|
|
var image = BuildImage(
|
|
[
|
|
.. SimpleCaller(),
|
|
(Callee + 0, Lwz(3, 3, 8)),
|
|
(Callee + 4, Blr())
|
|
]);
|
|
|
|
var options = Options() with
|
|
{
|
|
ModuleLinkBase = 0x80800000u,
|
|
ModuleGuestBase = 0x80900000u,
|
|
ModuleLinkedCodeSize = 0x1000
|
|
};
|
|
|
|
Assert.Equal(0, Translate(image, Caller, options).Metrics.InlinedCallSites);
|
|
}
|
|
|
|
[Fact]
|
|
public void ASelfRecursiveCallIsNeverInlined()
|
|
{
|
|
var image = BuildImage(
|
|
[
|
|
(Caller + 0, Bl(Caller + 0, Caller)),
|
|
(Caller + 4, Blr())
|
|
]);
|
|
|
|
Assert.Equal(0, Translate(image, Caller, Options()).Metrics.InlinedCallSites);
|
|
}
|
|
|
|
// --- classifier unit tests ---------------------------------------------
|
|
|
|
[Fact]
|
|
public void CandidateClassifierReportsWhyACalleeWasRefused()
|
|
{
|
|
var policy = new LeafInliningPolicy();
|
|
|
|
LeafFunctionInliner.TryCreateCandidate(
|
|
0x80000100u,
|
|
new IrFunction("two_blocks", "a",
|
|
[
|
|
new IrBasicBlock("a", [new IrJump("b")]),
|
|
new IrBasicBlock("b", [new IrReturn(null)])
|
|
]),
|
|
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
|
|
policy,
|
|
out var jumpBetweenBlocks);
|
|
Assert.Equal(LeafInlineRejection.ControlFlow, jumpBetweenBlocks);
|
|
|
|
LeafFunctionInliner.TryCreateCandidate(
|
|
0x80000100u,
|
|
new IrFunction("many_blocks", "a",
|
|
[
|
|
new IrBasicBlock("a", []),
|
|
new IrBasicBlock("b", []),
|
|
new IrBasicBlock("c", []),
|
|
new IrBasicBlock("d", []),
|
|
new IrBasicBlock("e", [new IrReturn(null)])
|
|
]),
|
|
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
|
|
policy,
|
|
out var multiBlock);
|
|
Assert.Equal(LeafInlineRejection.MultipleBlocks, multiBlock);
|
|
|
|
LeafFunctionInliner.TryCreateCandidate(
|
|
0x80000100u,
|
|
Single(new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()), new IrReturn(null)),
|
|
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
|
|
policy,
|
|
out var guestCall);
|
|
Assert.Equal(LeafInlineRejection.GuestCall, guestCall);
|
|
|
|
LeafFunctionInliner.TryCreateCandidate(
|
|
0x80000100u,
|
|
Single(new IrCall(string.Empty, "OSSystemCall", Array.Empty<IrValue>()), new IrReturn(null)),
|
|
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
|
|
policy,
|
|
out var fence);
|
|
Assert.Equal(LeafInlineRejection.OpaqueHelper, fence);
|
|
|
|
LeafFunctionInliner.TryCreateCandidate(
|
|
0x80000100u,
|
|
Single(new IrAssign("r0", IrValue.Register("lr")), new IrReturn(null)),
|
|
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
|
|
policy,
|
|
out var link);
|
|
Assert.Equal(LeafInlineRejection.TouchesLinkRegister, link);
|
|
|
|
LeafFunctionInliner.TryCreateCandidate(
|
|
0x80000100u,
|
|
Single(new IrAssign("r1", IrValue.Imm(0)), new IrReturn(null)),
|
|
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
|
|
policy,
|
|
out var stackPointer);
|
|
Assert.Equal(LeafInlineRejection.TouchesStackPointer, stackPointer);
|
|
|
|
LeafFunctionInliner.TryCreateCandidate(
|
|
0x80000100u,
|
|
Single(new IrAssign("r3", IrValue.Imm(1))),
|
|
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
|
|
policy,
|
|
out var noReturn);
|
|
Assert.Equal(LeafInlineRejection.NoTerminatingReturn, noReturn);
|
|
|
|
var accepted = LeafFunctionInliner.TryCreateCandidate(
|
|
0x80000100u,
|
|
Single(
|
|
new IrTracePpc(0x80000100u, "addi r3,r3,1", "0x38630001"),
|
|
new IrAssign("r3", IrValue.Imm(1)),
|
|
new IrReturn(null)),
|
|
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
|
|
policy,
|
|
out var none);
|
|
Assert.Equal(LeafInlineRejection.None, none);
|
|
Assert.NotNull(accepted);
|
|
Assert.Equal(1, accepted!.GuestInstructionCount);
|
|
// The terminating return became a fall-through.
|
|
Assert.DoesNotContain(accepted.Body, instruction => instruction is IrReturn);
|
|
}
|
|
|
|
[Fact]
|
|
public void CandidateClassifierAdmitsAnAcyclicRegionAndRefusesALoop()
|
|
{
|
|
var policy = new LeafInliningPolicy();
|
|
|
|
// if (cr0) goto join; arm; join: return
|
|
var diamond = LeafFunctionInliner.TryCreateCandidate(
|
|
0x80000100u,
|
|
new IrFunction("diamond", "0x80000100",
|
|
[
|
|
new IrBasicBlock("0x80000100",
|
|
[
|
|
new IrTracePpc(0x80000100u, "cmpwi r3,0", "0x2C030000"),
|
|
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
|
|
new IrTracePpc(0x80000104u, "bne 0x8000010C", "0x40820008"),
|
|
new IrBranch("ne", "0x8000010C", "0x80000108")
|
|
]),
|
|
new IrBasicBlock("0x80000108",
|
|
[
|
|
new IrTracePpc(0x80000108u, "addi r3,r3,1", "0x38630001"),
|
|
new IrAssign("r3", IrValue.Imm(1))
|
|
]),
|
|
new IrBasicBlock("0x8000010C",
|
|
[
|
|
new IrTracePpc(0x8000010Cu, "blr", "0x4E800020"),
|
|
new IrReturn(null)
|
|
])
|
|
]),
|
|
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
|
|
policy,
|
|
out var acyclic);
|
|
Assert.Equal(LeafInlineRejection.None, acyclic);
|
|
Assert.NotNull(diamond);
|
|
Assert.Equal(4, diamond!.GuestInstructionCount);
|
|
Assert.Empty(diamond.Body);
|
|
Assert.NotNull(diamond.Blocks);
|
|
Assert.Equal(3, diamond.Blocks!.Count);
|
|
// The arm's implicit fall-through onto the join is materialized, so the
|
|
// splice does not depend on where the caller places these blocks.
|
|
Assert.Equal(new IrJump("0x8000010C"), diamond.Blocks[1].Instructions[^1]);
|
|
|
|
// Same shape, but the branch goes back to the entry.
|
|
LeafFunctionInliner.TryCreateCandidate(
|
|
0x80000100u,
|
|
new IrFunction("loop", "0x80000100",
|
|
[
|
|
new IrBasicBlock("0x80000100",
|
|
[
|
|
new IrTracePpc(0x80000100u, "cmpwi r3,0", "0x2C030000"),
|
|
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
|
|
new IrTracePpc(0x80000104u, "bne 0x80000100", "0x4082FFFC"),
|
|
new IrBranch("ne", "0x80000100", "0x80000108")
|
|
]),
|
|
new IrBasicBlock("0x80000108",
|
|
[
|
|
new IrTracePpc(0x80000108u, "blr", "0x4E800020"),
|
|
new IrReturn(null)
|
|
])
|
|
]),
|
|
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
|
|
policy,
|
|
out var cyclic);
|
|
Assert.Equal(LeafInlineRejection.CyclicControlFlow, cyclic);
|
|
|
|
// Turning the shape off restores the straight-line-only verdict.
|
|
LeafFunctionInliner.TryCreateCandidate(
|
|
0x80000100u,
|
|
new IrFunction("diamond", "0x80000100",
|
|
[
|
|
new IrBasicBlock("0x80000100",
|
|
[
|
|
new IrTracePpc(0x80000100u, "b 0x80000104", "0x48000004"),
|
|
new IrJump("0x80000104")
|
|
]),
|
|
new IrBasicBlock("0x80000104",
|
|
[
|
|
new IrTracePpc(0x80000104u, "blr", "0x4E800020"),
|
|
new IrReturn(null)
|
|
])
|
|
]),
|
|
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
|
|
policy with { AllowAcyclicMultiBlockCallees = false },
|
|
out var disabled);
|
|
Assert.Equal(LeafInlineRejection.ControlFlow, disabled);
|
|
}
|
|
|
|
private static IrFunction Single(params IrInstruction[] instructions) =>
|
|
new("leaf", "entry", [new IrBasicBlock("entry", instructions)]);
|
|
|
|
private static IrBasicBlock Block(FunctionTranslationResult result, string label) =>
|
|
result.LinearIr.Blocks.Single(
|
|
block => string.Equals(block.Label, label, StringComparison.Ordinal));
|
|
|
|
private static void AssertNotInlined(ProgramImage image)
|
|
{
|
|
var result = Translate(image, Caller, Options());
|
|
Assert.Equal(0, result.Metrics.InlinedCallSites);
|
|
Assert.Contains("InvokeDirectCpu<0x80000100u>(ctx);", result.CxxCode, StringComparison.Ordinal);
|
|
}
|
|
|
|
private static int CountOccurrences(string haystack, string needle)
|
|
{
|
|
var count = 0;
|
|
var index = haystack.IndexOf(needle, StringComparison.Ordinal);
|
|
while (index >= 0)
|
|
{
|
|
count++;
|
|
index = haystack.IndexOf(needle, index + needle.Length, StringComparison.Ordinal);
|
|
}
|
|
|
|
return count;
|
|
}
|
|
}
|