Files
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

797 lines
28 KiB
C++

//===- RematerializerTest.cpp ---------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "llvm/CodeGen/Rematerializer.h"
#include "CodeGenTestBase.h"
#include "llvm/CodeGen/LiveIntervals.h"
#include "llvm/CodeGen/RegisterPressure.h"
#include "llvm/Support/TargetSelect.h"
using namespace llvm;
using RegisterIdx = Rematerializer::RegisterIdx;
class RematerializerTest : public CodeGenTestBase {
public:
std::unique_ptr<SmallVector<Rematerializer::RegionBoundaries>> Regions;
std::unique_ptr<Rematerializer> Remater;
MachineFunction *MF;
static void SetUpTestCase() {
InitializeAllTargets();
InitializeAllTargetMCs();
}
void SetUp() override { setUpImpl("amdgcn--", "gfx950", ""); }
bool parseMIRAndInit(StringRef MIRCode, StringRef FunName) {
if (!parseMIR(MIRCode))
return false;
MF = &CodeGenTestBase::getMF(FunName);
LiveIntervals &LIS = MFAM.getResult<LiveIntervalsAnalysis>(*MF);
// Create regions for the rematerializer. Both MBBs and terminator MIs
// delimitate regions.
Regions = std::make_unique<SmallVector<Rematerializer::RegionBoundaries>>();
MachineInstr *FirstMI = nullptr;
for (MachineBasicBlock &MBB : *MF) {
for (MachineInstr &MI : MBB) {
if (!FirstMI)
FirstMI = &MI;
if (MI.isTerminator()) {
if (FirstMI != &MI)
Regions->push_back({FirstMI, MI});
FirstMI = nullptr;
}
}
// End the region at the end of the block.
if (FirstMI) {
Regions->push_back({FirstMI, MBB.end()});
FirstMI = nullptr;
}
}
Remater = std::make_unique<Rematerializer>(*MF, *Regions, LIS);
Remater->analyze();
return true;
}
MachineFunction &getMF() { return *MF; }
Rematerializer &getRematerializer() { return *Remater; }
/// Returns the number of users of register \p RegIdx.
unsigned getNumUsers(RegisterIdx RegIdx) {
unsigned NumUsers = 0;
for (const auto &[_, RegionUses] : Remater->getReg(RegIdx).Uses)
NumUsers += RegionUses.size();
return NumUsers;
}
/// Returns the size of region \p RegionIdx.
unsigned getRegionSize(unsigned RegionIdx) {
const Rematerializer::RegionBoundaries &Region = (*Regions)[RegionIdx];
return std::distance(Region.first, Region.second);
}
};
/// Asserts that region RegionIdx contains RegionSize instructions.
#define ASSERT_REGION_SIZE(RegionIdx, RegionSize) \
ASSERT_EQ(getRegionSize(RegionIdx), RegionSize)
/// Asserts that regions have sizes RegionSizes, which must be an iterable
/// object with the same number of elements as the number of regions.
#define ASSERT_REGION_SIZES(RegionSizes) \
{ \
ASSERT_EQ(RegionSizes.size(), Regions->size()); \
for (const auto [RegionIdx, ExpectedSize] : enumerate(RegionSizes)) \
ASSERT_REGION_SIZE(RegionIdx, ExpectedSize); \
}
/// Expects that register RegIdx in the rematerializer has a total of N users.
#define EXPECT_NUM_USERS(RegIdx, N) \
EXPECT_EQ(getNumUsers(RegIdx), static_cast<unsigned>(N))
/// Expects that register RegIdx in the remterializer hsa no users.
#define EXPECT_NO_USERS(RegIdx) EXPECT_NUM_USERS(RegIdx, 0)
/// Expects that rematerialized register RegIdx has origin OriginIdx, is defined
/// in region DefRegionIdx, and has a total of NumUsers users.
#define EXPECT_REMAT(RegIdx, OriginIdx, DefRegionIdx, NumUsers) \
{ \
const Rematerializer::Reg &RematReg = Remater.getReg(RegIdx); \
EXPECT_EQ(Remater.getOriginOf(RegIdx), OriginIdx); \
EXPECT_EQ(RematReg.DefRegion, DefRegionIdx); \
EXPECT_NUM_USERS(RegIdx, NumUsers); \
}
/// Rematerializes a tree of registers to a single user in different ways using
/// the dependency reuse mechanics and the coarse-grained or more fine-grained
/// API. Rollback rematerializations in-between each different wave of
/// rematerializations.
TEST_F(RematerializerTest, TreeRematRollback) {
StringRef MIR = R"(
name: TreeRematRollback
tracksRegLiveness: true
machineFunctionInfo:
isEntryFunction: true
body: |
bb.0:
%0:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 0, implicit $exec, implicit $mode
%1:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 1, implicit $exec, implicit $mode
%2:vgpr_32 = V_ADD_U32_e32 %0, %1, implicit $exec
%3:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 3, implicit $exec, implicit $mode
%4:vgpr_32 = V_ADD_U32_e32 %2, %3, implicit $exec
bb.1:
S_NOP 0, implicit %4
S_ENDPGM 0
...
)";
ASSERT_TRUE(parseMIRAndInit(MIR, "TreeRematRollback"));
Rematerializer &Remater = getRematerializer();
Rematerializer::DependencyReuseInfo DRI;
Rollbacker Rollbacker;
Remater.addListener(&Rollbacker);
// MBB/Region indices.
const unsigned MBB0 = 0, MBB1 = 1;
SmallVector<unsigned, 2> RegionSizes{5, 1};
ASSERT_REGION_SIZES(RegionSizes);
// Indices of rematerializable registers.
unsigned NumRegs = 0;
const RegisterIdx Cst0 = NumRegs++, Cst1 = NumRegs++, Add01 = NumRegs++,
Cst3 = NumRegs++, Add23 = NumRegs++;
ASSERT_EQ(Remater.getNumRegs(), NumRegs);
// Rematerialize Add23 with all transitive dependencies.
{
Remater.rematerializeToRegion(/*RootIdx=*/Add23, /*UseRegion=*/MBB1, DRI);
Remater.updateLiveIntervals();
// None of the original registers have any users left.
EXPECT_NO_USERS(Cst0);
EXPECT_NO_USERS(Cst1);
EXPECT_NO_USERS(Add01);
EXPECT_NO_USERS(Cst3);
EXPECT_NO_USERS(Add23);
// Copies of all MIs were inserted into the second MBB. Original registers
// were deleted.
RegionSizes[MBB0] -= 5;
RegionSizes[MBB1] += 5;
ASSERT_REGION_SIZES(RegionSizes);
NumRegs += 5;
ASSERT_EQ(Remater.getNumRegs(), NumRegs);
}
// After rollback all rematerializations are removed from the MIR.
Rollbacker.rollback(Remater);
RegionSizes[MBB0] += 5;
RegionSizes[MBB1] -= 5;
ASSERT_REGION_SIZES(RegionSizes);
// Rematerialize Add23 only with its direct dependencies, reuse the rest.
{
DRI.clear().reuse(Cst0).reuse(Cst1);
Remater.rematerializeToRegion(/*RootIdx=*/Add23, /*UseRegion=*/MBB1, DRI);
Remater.updateLiveIntervals();
// Re-used registers have rematerializations as their single user (original
// users are dead). Rematerialized registers have no users.
EXPECT_NUM_USERS(Cst0, 1);
EXPECT_NUM_USERS(Cst1, 1);
EXPECT_NO_USERS(Add01);
EXPECT_NO_USERS(Cst3);
EXPECT_NO_USERS(Add23);
// Only immediate dependencies are copied to the second MBB.
RegionSizes[MBB0] -= 3;
RegionSizes[MBB1] += 3;
ASSERT_REGION_SIZES(RegionSizes);
NumRegs += 3;
ASSERT_EQ(Remater.getNumRegs(), NumRegs);
}
// After rollback all rematerializations are removed from the MIR.
Rollbacker.rollback(Remater);
RegionSizes[MBB0] += 3;
RegionSizes[MBB1] -= 3;
ASSERT_REGION_SIZES(RegionSizes);
// Rematerialize Add23 only with its direct dependencies as before, but
// with as fine-grained operations as possible.
{
MachineInstr *NopMI = &*(*Regions)[MBB1].first;
DRI.clear().reuse(Cst0).reuse(Cst1);
const RegisterIdx RematAdd01 =
Remater.rematerializeToPos(Add01, MBB1, NopMI, DRI);
// This adds an additional user to the used constants, and does not change
// existing users for the original register.
EXPECT_NO_USERS(RematAdd01);
EXPECT_NUM_USERS(Add01, 1);
EXPECT_NUM_USERS(Cst0, 2);
EXPECT_NUM_USERS(Cst1, 2);
DRI.clear();
const RegisterIdx RematCst3 =
Remater.rematerializeToPos(Cst3, MBB1, NopMI, DRI);
// This does not change existing users for the original register.
EXPECT_NO_USERS(RematCst3);
EXPECT_NUM_USERS(Cst3, 1);
DRI.clear().useRemat(Add01, RematAdd01).useRemat(Cst3, RematCst3);
const RegisterIdx RematAdd23 =
Remater.rematerializeToPos(Add23, MBB1, NopMI, DRI);
// This adds a user to used rematerializations, and does not change existing
// users for the original register.
EXPECT_NO_USERS(RematAdd23);
EXPECT_NUM_USERS(Add23, 1);
EXPECT_NUM_USERS(RematAdd01, 1);
EXPECT_NUM_USERS(RematCst3, 1);
// Finally transfer the NOP user from the original to the rematerialized
// register.
Remater.transferUser(Add23, RematAdd23, MBB1, *NopMI);
EXPECT_NO_USERS(Add23);
EXPECT_NUM_USERS(RematAdd23, 1);
RegionSizes[MBB0] -= 3;
RegionSizes[MBB1] += 3;
ASSERT_REGION_SIZES(RegionSizes);
NumRegs += 3;
ASSERT_EQ(Remater.getNumRegs(), NumRegs);
}
// This time don't rollback.
Remater.updateLiveIntervals();
EXPECT_TRUE(getMF().verify());
}
/// To rematerialize %3 along with all its dependencies before its only use in
/// bb.1, we must first rematerialize %0 and %1 (in any order), then %2, and
/// finally %3. The rematerializer had a rematerialization order bug wherein,
/// because %0 is also used directly in the MI defining %3, it was
/// rematerialized after %2, breaking the invariant that dependencies of a
/// register must always be rematerialized before the register itself.
TEST_F(RematerializerTest, MultiplePathsRematOrder) {
StringRef MIR = R"(
name: MultiplePathsRematOrder
tracksRegLiveness: true
machineFunctionInfo:
isEntryFunction: true
body: |
bb.0:
%0:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 0, implicit $exec, implicit $mode
%1:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 1, implicit $exec, implicit $mode
%2:vgpr_32 = V_ADD_U32_e32 %0, %1, implicit $exec
%3:vgpr_32 = V_ADD_U32_e32 %0, %2, implicit $exec
bb.1:
S_NOP 0, implicit %3
S_ENDPGM 0
...
)";
ASSERT_TRUE(parseMIRAndInit(MIR, "MultiplePathsRematOrder"));
Rematerializer &Remater = getRematerializer();
Rematerializer::DependencyReuseInfo DRI;
const unsigned MBB1 = 1;
const RegisterIdx Add02 = 3;
// This call would previously fail.
Remater.rematerializeToRegion(Add02, MBB1, DRI);
Remater.updateLiveIntervals();
EXPECT_TRUE(getMF().verify());
}
/// Rematerializes a single register to multiple regions, tracking that
/// rematerializations are linked correctly and making sure that the original
/// register is deleted automatically when it no longer has any uses.
TEST_F(RematerializerTest, MultiRegionsRemat) {
StringRef MIR = R"(
name: MultiRegionsRemat
tracksRegLiveness: true
machineFunctionInfo:
isEntryFunction: true
body: |
bb.0:
%0:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 0, implicit $exec, implicit $mode
bb.1:
S_NOP 0, implicit %0, implicit %0
bb.2:
S_NOP 0, implicit %0
S_NOP 0, implicit %0
bb.3:
S_NOP 0, implicit %0
S_ENDPGM 0
...
)";
ASSERT_TRUE(parseMIRAndInit(MIR, "MultiRegionsRemat"));
Rematerializer &Remater = getRematerializer();
Rematerializer::DependencyReuseInfo DRI;
// MBB/Region indices.
const unsigned MBB0 = 0, MBB1 = 1, MBB2 = 2, MBB3 = 3;
SmallVector<unsigned, 2> RegionSizes{1, 1, 2, 1};
ASSERT_REGION_SIZES(RegionSizes);
// Indices of rematerializable registers.
const RegisterIdx Cst0 = 0;
ASSERT_EQ(Remater.getNumRegs(), 1U);
// Rematerialization to MBB1.
const RegisterIdx RematBB1 =
Remater.rematerializeToRegion(/*RootIdx=*/Cst0, /*UseRegion=*/MBB1, DRI);
++RegionSizes[MBB1];
ASSERT_REGION_SIZES(RegionSizes);
EXPECT_REMAT(/*RegIdx=*/RematBB1, /*OriginIdx=*/Cst0, /*DefRegionIdx=*/MBB1,
/*NumUsers=*/1);
// Rematerialization to MBB2.
DRI.clear();
const RegisterIdx RematBB2 =
Remater.rematerializeToRegion(/*RootIdx=*/Cst0, /*UseRegion=*/MBB2, DRI);
++RegionSizes[MBB2];
ASSERT_REGION_SIZES(RegionSizes);
EXPECT_REMAT(/*RegIdx=*/RematBB2, /*OriginIdx=*/Cst0, /*DefRegionIdx=*/MBB2,
/*NumUsers=*/2);
// Rematerialization to MBB3. Rematerializing to the last original user
// deletes the original register.
DRI.clear();
const RegisterIdx RematBB3 =
Remater.rematerializeToRegion(/*RootIdx=*/Cst0, /*UseRegion=*/MBB3, DRI);
--RegionSizes[MBB0];
++RegionSizes[MBB3];
ASSERT_REGION_SIZES(RegionSizes);
EXPECT_REMAT(/*RegIdx=*/RematBB3, /*OriginIdx=*/Cst0, /*DefRegionIdx=*/MBB3,
/*NumUsers=*/1);
Remater.updateLiveIntervals();
EXPECT_TRUE(getMF().verify());
}
/// Rematerializes a tree of register with some unrematerializable operands to a
/// final destination in two steps, creating rematerializations of
/// rematerializations in the process. Make sure that origins of
/// rematerializations are always original registers.
TEST_F(RematerializerTest, MultiStep) {
StringRef MIR = R"(
name: MultiStep
tracksRegLiveness: true
machineFunctionInfo:
isEntryFunction: true
body: |
bb.0:
%0:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 0, implicit $exec, implicit $mode
%1:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 1, implicit $exec, implicit $mode, implicit-def $m0
%2:vgpr_32 = V_ADD_U32_e32 %0, %1, implicit $exec
S_NOP 0, implicit %0
bb.1:
%3:vgpr_32 = V_ADD_U32_e32 %2, %2, implicit $exec
bb.2:
S_NOP 0, implicit %3
S_ENDPGM 0
...
)";
ASSERT_TRUE(parseMIRAndInit(MIR, "MultiStep"));
Rematerializer &Remater = getRematerializer();
Rematerializer::DependencyReuseInfo DRI;
// MBB/Region indices.
const unsigned MBB0 = 0, MBB1 = 1, MBB2 = 2;
SmallVector<unsigned, 2> RegionSizes{4, 1, 1};
ASSERT_REGION_SIZES(RegionSizes);
// Indices of rematerializable registers.
unsigned NumRegs = 0;
const RegisterIdx Cst0 = NumRegs++, Add01 = NumRegs++, Add22 = NumRegs++;
ASSERT_EQ(Remater.getNumRegs(), NumRegs);
// Rematerialize Add01 from the first to the second block along with its
// single rematerializable dependency (constant 0). The constant 1 has an
// implicit def that is non-ignorable so it cannot be rematerialized. The
// constant 0 remains in the first block because it has a user there, but the
// add is deleted.
Remater.rematerializeToRegion(/*RootIdx=*/Add01, /*UseRegion=*/MBB1, DRI);
const RegisterIdx RematCst0 = NumRegs++, RematAdd01 = NumRegs++;
RegionSizes[MBB0] -= 1;
RegionSizes[MBB1] += 2;
ASSERT_REGION_SIZES(RegionSizes);
EXPECT_REMAT(/*RegIdx=*/RematCst0, /*OriginIdx=*/Cst0, /*DefRegionIdx=*/MBB1,
/*NumUsers=*/1);
EXPECT_REMAT(/*RegIdx=*/RematAdd01, /*OriginIdx=*/Add01,
/*DefRegionIdx=*/MBB1,
/*NumUsers=*/1);
// We are going to re-rematerialize a register so the LIS need to be
// up-to-date.
Remater.updateLiveIntervals();
// Rematerialize Add22 from the second to the third block, which will
// also indirectly rematerialize RematAdd01; make sure the latter's
// rematerializations's origin is the original register, not RematAdd01.
DRI.clear().reuse(RematCst0);
Remater.rematerializeToRegion(/*RootIdx=*/Add22, /*UseRegion=*/MBB2, DRI);
const RegisterIdx RematRematAdd01 = NumRegs++, RematAdd22 = NumRegs++;
RegionSizes[MBB1] -= 2;
RegionSizes[MBB2] += 2;
ASSERT_REGION_SIZES(RegionSizes);
EXPECT_REMAT(/*RegIdx=*/RematRematAdd01, /*OriginIdx=*/Add01,
/*DefRegionIdx=*/MBB2,
/*NumUsers=*/1);
EXPECT_REMAT(/*RegIdx=*/RematAdd22, /*OriginIdx=*/Add22,
/*DefRegionIdx=*/MBB2,
/*NumUsers=*/1);
Remater.updateLiveIntervals();
EXPECT_TRUE(getMF().verify());
}
/// Checks that it is possible to rematerialize inside a region that was
/// rendered empty by previous rematerializations (as long as the region ends
/// with a terminator).
TEST_F(RematerializerTest, EmptyRegion) {
StringRef MIR = R"(
name: EmptyRegion
tracksRegLiveness: true
machineFunctionInfo:
isEntryFunction: true
body: |
bb.0:
%0:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 0, implicit $exec, implicit $mode
%1:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 1, implicit $exec, implicit $mode
bb.1:
%2:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 2, implicit $exec, implicit $mode
bb.2:
%3:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 3, implicit $exec, implicit $mode
S_BRANCH %bb.3
bb.3:
S_NOP 0, implicit %0, implicit %1
S_NOP 0, implicit %2, implicit %3
S_ENDPGM 0
...
)";
ASSERT_TRUE(parseMIRAndInit(MIR, "EmptyRegion"));
Rematerializer &Remater = getRematerializer();
Rematerializer::DependencyReuseInfo DRI;
// MBB/Region indices.
const unsigned MBB0 = 0, MBB1 = 1, MBB2 = 2, MBB3 = 3;
SmallVector<unsigned, 2> RegionSizes{2, 1, 1, 2};
ASSERT_REGION_SIZES(RegionSizes);
// Indices of rematerializable registers.
unsigned NumRegs = 0;
const RegisterIdx Cst0 = NumRegs++, Cst1 = NumRegs++, Cst2 = NumRegs++,
Cst3 = NumRegs++;
ASSERT_EQ(Remater.getNumRegs(), NumRegs);
// After rematerializing %2 and %3 to bb.3, their respective original defining
// regions are empty. %2's region ends at the end of its parent block, whereas
// %3's region ends at a terminator MI (S_BRANCH).
Remater.rematerializeToRegion(/*RootIdx=*/Cst2, /*UseRegion=*/MBB3, DRI);
Remater.rematerializeToRegion(/*RootIdx=*/Cst3, /*UseRegion=*/MBB3,
DRI.clear());
RegionSizes[MBB1] -= 1;
RegionSizes[MBB2] -= 1;
RegionSizes[MBB3] += 2;
ASSERT_REGION_SIZES(RegionSizes);
// Move %0 to the empty MBB1 block/region.
const RegisterIdx RematCst0 =
Remater.rematerializeToRegion(Cst0, MBB1, DRI.clear());
Remater.transferRegionUsers(Cst0, RematCst0, MBB3);
// Move %1 to the empty MBB2 region, right before the S_BRANCH terminator.
const RegisterIdx RematCst1 = Remater.rematerializeToPos(
Cst1, MBB2, (*Regions)[MBB2].first, DRI.clear());
Remater.transferRegionUsers(Cst1, RematCst1, MBB3);
RegionSizes[MBB0] -= 2;
RegionSizes[MBB1] += 1;
RegionSizes[MBB2] += 1;
ASSERT_REGION_SIZES(RegionSizes);
Remater.updateLiveIntervals();
EXPECT_TRUE(getMF().verify());
}
/// Checks that only registers with a single definition are rematerializable,
/// even when registers are made up of multiple sub-registers each with their
/// own definition.
TEST_F(RematerializerTest, SubReg) {
StringRef MIR = R"(
name: SubReg
tracksRegLiveness: true
machineFunctionInfo:
isEntryFunction: true
body: |
bb.0:
undef %01.sub0:vreg_64_align2 = nofpexcept V_CVT_I32_F64_e32 0, implicit $exec, implicit $mode
%01.sub1:vreg_64_align2 = nofpexcept V_CVT_I32_F64_e32 1, implicit $exec, implicit $mode
undef %2.sub0:vreg_64_align2 = nofpexcept V_CVT_I32_F64_e32 2, implicit $exec, implicit $mode
undef %34.sub0:vreg_64_align2 = nofpexcept V_CVT_I32_F64_e32 3, implicit $exec, implicit $mode
bb.1:
%34.sub1:vreg_64_align2 = nofpexcept V_CVT_I32_F64_e32 4, implicit $exec, implicit $mode
S_NOP 0, implicit %01, implicit %2, implicit %34
S_ENDPGM 0
...
)";
ASSERT_TRUE(parseMIRAndInit(MIR, "SubReg"));
Rematerializer &Remater = getRematerializer();
Rematerializer::DependencyReuseInfo DRI;
// MBB/Region indices.
const unsigned MBB0 = 0, MBB1 = 1;
SmallVector<unsigned, 2> RegionSizes{4, 2};
ASSERT_REGION_SIZES(RegionSizes);
// Indices of rematerializable registers.
unsigned NumRegs = 0;
const RegisterIdx Cst2 = NumRegs++;
ASSERT_EQ(Remater.getNumRegs(), NumRegs);
RegisterIdx RematCst2 =
Remater.rematerializeToRegion(/*RootIdx=*/Cst2, /*UseRegion=*/MBB1, DRI);
RegionSizes[MBB0] -= 1;
RegionSizes[MBB1] += 1;
ASSERT_REGION_SIZES(RegionSizes);
EXPECT_REMAT(/*RegIdx=*/RematCst2, /*OriginIdx=*/Cst2,
/*DefRegionIdx=*/MBB1,
/*NumUsers=*/1);
Remater.updateLiveIntervals();
EXPECT_TRUE(getMF().verify());
}
/// The rematerializer had a bug where re-creating the interval of a
/// non-rematerializable super-register defined over multiple MIs, some of which
/// defining entirely dead subregisters, could cause a crash when changing the
/// order of sub-definitions (for example during scheduling) because the
/// re-created interval could end up with multiple connected components, which
/// is illegal. The solution is to split separate components of the interval in
/// such cases.
TEST_F(RematerializerTest, SplitSubRegDeadDef) {
StringRef MIR = R"(
name: SplitSubRegDeadDef
tracksRegLiveness: true
machineFunctionInfo:
isEntryFunction: true
body: |
bb.0:
undef %0.sub0:vreg_64 = IMPLICIT_DEF
%0.sub1:vreg_64 = IMPLICIT_DEF
%1:vgpr_32 = V_ADD_U32_e32 %0.sub0, %0.sub0, implicit $exec
bb.1:
S_NOP 0, implicit %1
S_ENDPGM 0
...
)";
ASSERT_TRUE(parseMIRAndInit(MIR, "SplitSubRegDeadDef"));
LiveIntervals &LIS = MFAM.getResult<LiveIntervalsAnalysis>(*MF);
// Replicates the scheduler's effect on LIS on an intra-block move of MI.
auto MoveMIAndAdjustLiveness = [&](MachineInstr &MI) {
LIS.handleMove(MI);
const MachineRegisterInfo &MRI = MF->getRegInfo();
const TargetRegisterInfo &TRI = *MF->getSubtarget().getRegisterInfo();
RegisterOperands RegOpers;
RegOpers.collect(MI, TRI, MRI, true, /*IgnoreDead=*/false);
SlotIndex Sub1Slot = LIS.getInstructionIndex(MI).getRegSlot();
RegOpers.adjustLaneLiveness(LIS, MRI, Sub1Slot, &MI);
};
MachineBasicBlock &MBB0 = *MF->getBlockNumbered(0);
MachineInstr &Sub0Def = *MBB0.begin();
MachineInstr &Sub1Def = *MBB0.begin()->getNextNode();
// Flip %0's subdefinition order. After the move, the definitions look like:
// undef %0.sub1:vreg_64 = IMPLICIT_DEF
// undef %0.sub0:vreg_64 = IMPLICIT_DEF
MBB0.splice(Sub0Def.getIterator(), &MBB0, Sub1Def.getIterator());
MoveMIAndAdjustLiveness(Sub1Def);
// Rematerialize %1 to bb.1. This triggers a live-interval update of %0 when
// calling Remater.updateLiveIntervals(), during which its interval is split.
Rematerializer &Remater = getRematerializer();
Rematerializer::DependencyReuseInfo DRI;
const unsigned MBB1 = 1;
const RegisterIdx Add = 0;
Remater.rematerializeToRegion(Add, MBB1, DRI);
Remater.updateLiveIntervals();
// If we didn't split %0 before, its definitions would now look like:
// dead undef %0.sub1:vreg_64 = IMPLICIT_DEF
// undef %0.sub0:vreg_64 = IMPLICIT_DEF
//
// Trying to flip back %0's definition order then triggers an
// error in LIS.handleMove because its live interval is made up of multiple
// connected components.
ASSERT_NE(Sub0Def.getOperand(0).getReg(), Sub1Def.getOperand(0).getReg());
MBB0.splice(MBB0.end(), &MBB0, Sub1Def.getIterator());
MoveMIAndAdjustLiveness(Sub1Def);
EXPECT_TRUE(getMF().verify());
}
/// Checks that rollback works as expected when the rollback listener is added
/// mid-rematerializations.
TEST_F(RematerializerTest, Rollback) {
StringRef MIR = R"(
name: Rollback
tracksRegLiveness: true
machineFunctionInfo:
isEntryFunction: true
body: |
bb.0:
%0:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 0, implicit $exec, implicit $mode
%1:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 1, implicit $exec, implicit $mode
bb.1:
S_NOP 0, implicit %0, implicit %1
bb.2:
S_NOP 0, implicit %0, implicit %1
S_ENDPGM 0
)";
ASSERT_TRUE(parseMIRAndInit(MIR, "Rollback"));
Rematerializer &Remater = getRematerializer();
Rematerializer::DependencyReuseInfo DRI;
// MBB/Region indices.
const unsigned MBB0 = 0, MBB1 = 1, MBB2 = 2;
SmallVector<unsigned, 4> RegionSizes{2, 1, 1};
ASSERT_REGION_SIZES(RegionSizes);
// Indices of rematerializable registers.
unsigned NumRegs = 0;
const RegisterIdx Cst0 = NumRegs++, Cst1 = NumRegs++;
ASSERT_EQ(Remater.getNumRegs(), NumRegs);
// Rematerialize %0 to MBB1, taking one user from the original register.
RegisterIdx RematCst0MBB1 = Remater.rematerializeToRegion(Cst0, MBB1, DRI);
RegionSizes[MBB1] += 1;
ASSERT_REGION_SIZES(RegionSizes);
NumRegs += 1;
ASSERT_EQ(Remater.getNumRegs(), NumRegs);
Rollbacker Rollback;
Remater.addListener(&Rollback);
// Rematerialize %0 to MBB2 amd %1 to MBB1/MBB2; each rematerialization ends
// up with a single user and both original registers are deleted.
RegisterIdx RematCst0MBB2 =
Remater.rematerializeToRegion(Cst0, MBB2, DRI.clear());
RegisterIdx RematCst1MBB1 =
Remater.rematerializeToRegion(Cst1, MBB1, DRI.clear());
RegisterIdx RematCst1MBB2 =
Remater.rematerializeToRegion(Cst1, MBB2, DRI.clear());
RegionSizes[MBB0] -= 2;
RegionSizes[MBB1] += 1;
RegionSizes[MBB2] += 2;
ASSERT_REGION_SIZES(RegionSizes);
NumRegs += 3;
ASSERT_EQ(Remater.getNumRegs(), NumRegs);
EXPECT_NO_USERS(Cst0);
EXPECT_NO_USERS(Cst1);
EXPECT_NUM_USERS(RematCst0MBB1, 1);
EXPECT_NUM_USERS(RematCst0MBB2, 1);
EXPECT_NUM_USERS(RematCst1MBB1, 1);
EXPECT_NUM_USERS(RematCst1MBB2, 1);
// Rollback all changes since the rollbacker was added. The first
// rematerialization of %0 to MBB1 happened before so it is not rolled back.
// However %0 is re-created because it was deleted after.
Rollback.rollback(Remater);
RegionSizes[MBB0] += 2;
RegionSizes[MBB1] -= 1;
RegionSizes[MBB2] -= 2;
ASSERT_REGION_SIZES(RegionSizes);
ASSERT_EQ(Remater.getNumRegs(), NumRegs);
EXPECT_NUM_USERS(Cst0, 1);
EXPECT_NUM_USERS(Cst1, 2);
EXPECT_NUM_USERS(RematCst0MBB1, 1);
EXPECT_NO_USERS(RematCst0MBB2);
EXPECT_NO_USERS(RematCst1MBB1);
EXPECT_NO_USERS(RematCst1MBB2);
EXPECT_TRUE(getMF().verify());
}
/// Checks that rollback re-creates MIs at correct positions when the order of
/// register deletions forces the re-creation logic to iterate through multiple
/// deleted registers' respective insert position to find a valid one.
TEST_F(RematerializerTest, RollbackInvalidInsertPos) {
StringRef MIR = R"(
name: RollbackInvalidInsertPos
tracksRegLiveness: true
machineFunctionInfo:
isEntryFunction: true
body: |
bb.0:
%0:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 0, implicit $exec, implicit $mode
%1:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 1, implicit $exec, implicit $mode
%2:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 2, implicit $exec, implicit $mode
%3:vgpr_32 = nofpexcept V_CVT_I32_F64_e32 3, implicit $exec, implicit $mode
bb.1:
S_NOP 0, implicit %0, implicit %1, implicit %2, implicit %3
S_ENDPGM 0
)";
ASSERT_TRUE(parseMIRAndInit(MIR, "RollbackInvalidInsertPos"));
Rematerializer &Remater = getRematerializer();
Rematerializer::DependencyReuseInfo DRI;
Rollbacker Rollback;
Remater.addListener(&Rollback);
// MBB/Region indices.
const unsigned MBB0 = 0, MBB1 = 1;
SmallVector<unsigned, 4> RegionSizes{4, 1};
ASSERT_REGION_SIZES(RegionSizes);
// Indices of rematerializable registers.
const RegisterIdx Cst0 = 0, Cst1 = 1, Cst2 = 2, Cst3 = 3;
// Rematerialize %0 to MBB1, deleting the original register
Remater.rematerializeToRegion(Cst0, MBB1, DRI);
RegionSizes[MBB0] -= 1;
RegionSizes[MBB1] += 1;
ASSERT_REGION_SIZES(RegionSizes);
// Rematerialize %1 to MBB1, deleting the original register
Remater.rematerializeToRegion(Cst1, MBB1, DRI.clear());
RegionSizes[MBB0] -= 1;
RegionSizes[MBB1] += 1;
ASSERT_REGION_SIZES(RegionSizes);
// Rematerialize %2 to MBB1, deleting the original register
Remater.rematerializeToRegion(Cst2, MBB1, DRI.clear());
RegionSizes[MBB0] -= 1;
RegionSizes[MBB1] += 1;
ASSERT_REGION_SIZES(RegionSizes);
// Now rollback and check for correct instruction order in the original
// defining region. The asserts on region sizes ensure that all original
// registers were indeed deleted and will be re-created in the original
// region.
Rollback.rollback(Remater);
RegionSizes[MBB0] += 3;
RegionSizes[MBB1] -= 3;
ASSERT_REGION_SIZES(RegionSizes);
MachineInstr &DefCst0 = *Remater.getReg(Cst0).DefMI;
MachineInstr &DefCst1 = *Remater.getReg(Cst1).DefMI;
MachineInstr &DefCst2 = *Remater.getReg(Cst2).DefMI;
MachineInstr &DefCst3 = *Remater.getReg(Cst3).DefMI;
EXPECT_EQ(std::next(DefCst0.getIterator()), DefCst1.getIterator());
EXPECT_EQ(std::next(DefCst1.getIterator()), DefCst2.getIterator());
EXPECT_EQ(std::next(DefCst2.getIterator()), DefCst3.getIterator());
EXPECT_TRUE(getMF().verify());
}