Files
FEX-Emu--FEX/FEXCore/Source/Interface/IR/Passes/ConstProp.cpp
T
Alyssa Rosenzweig d966ae145e ConstProp: merge inline + pooling
now that the algebraic/folding opts are gone, we can do this in one pass for a
2.5% speedup:

    N           Min           Max        Median           Avg        Stddev
x  50    0.44474704    0.46750433    0.45455258    0.45446569  0.0044727894
+  50    0.43149892    0.45173984    0.44252267    0.44295575  0.0045621814
Difference at 95.0% confidence
	-0.0115099 +/- 0.00179263
	-2.53263% +/- 0.394447%
	(Student's t, pooled s = 0.00451771)

Signed-off-by: Alyssa Rosenzweig <alyssa@rosenzweig.io>
2025-06-02 14:27:02 -04:00

402 lines
13 KiB
C++

// SPDX-License-Identifier: MIT
/*
$info$
tags: ir|opts
desc: ConstProp, ZExt elim, const pooling, fcmp reduction, const inlining
$end_info$
*/
#include <CodeEmitter/Emitter.h>
#include "Interface/IR/IREmitter.h"
#include "Interface/IR/PassManager.h"
#include <FEXCore/IR/IR.h>
#include <FEXCore/HLE/SyscallHandler.h>
#include <FEXCore/Utils/LogManager.h>
#include <FEXCore/Utils/Profiler.h>
#include <FEXCore/fextl/map.h>
#include <FEXCore/fextl/unordered_map.h>
#include <cstdint>
#include <string.h>
namespace FEXCore::IR {
// aarch64 heuristics
static bool IsImmLogical(uint64_t imm, unsigned width) {
if (width < 32) {
width = 32;
}
return ARMEmitter::Emitter::IsImmLogical(imm, width);
}
class ConstProp final : public FEXCore::IR::Pass {
public:
explicit ConstProp(bool SupportsTSOImm9)
: SupportsTSOImm9 {SupportsTSOImm9} {}
void Run(IREmitter* IREmit) override;
private:
void ConstantPropagation(IREmitter* IREmit, const IRListView& CurrentIR, Ref CodeNode, IROp_Header* IROp);
bool SupportsTSOImm9 {};
template<class F>
bool InlineIf(IREmitter* IREmit, const IRListView& CurrentIR, Ref CodeNode, IROp_Header* IROp, unsigned Index, F Filter) {
uint64_t Constant;
if (!IREmit->IsValueConstant(IROp->Args[Index], &Constant) || !Filter(Constant)) {
return false;
}
IREmit->SetWriteCursor(CurrentIR.GetNode(IROp->Args[Index]));
IREmit->ReplaceNodeArgument(CodeNode, Index, IREmit->_InlineConstant(Constant));
return true;
}
bool Inline(IREmitter* IREmit, const IRListView& CurrentIR, Ref CodeNode, IROp_Header* IROp, unsigned Index) {
return InlineIf(IREmit, CurrentIR, CodeNode, IROp, Index, [](uint64_t _) { return true; });
}
bool InlineIfZero(IREmitter* IREmit, const IRListView& CurrentIR, Ref CodeNode, IROp_Header* IROp, unsigned Index) {
return InlineIf(IREmit, CurrentIR, CodeNode, IROp, Index, [](uint64_t X) { return X == 0; });
}
bool InlineIfLargeAddSub(IREmitter* IREmit, const IRListView& CurrentIR, Ref CodeNode, IROp_Header* IROp, unsigned Index) {
// We don't allow 8/16-bit operations to have constants, since no
// constant would be in bounds after the JIT's 24/16 shift.
auto Filter = [&IROp](uint64_t X) {
return ARMEmitter::IsImmAddSub(X) && IROp->Size >= OpSize::i32Bit;
};
return InlineIf(IREmit, CurrentIR, CodeNode, IROp, Index, Filter);
}
void InlineMemImmediate(IREmitter* IREmit, const IRListView& IR, Ref CodeNode, IR::RegisterClassType RegisterClass, IROp_Header* IROp,
OrderedNodeWrapper Offset, MemOffsetType OffsetType, const size_t Offset_Index, uint8_t& OffsetScale, bool TSO) {
uint64_t Imm {};
if (OffsetType != MEM_OFFSET_SXTX || !IREmit->IsValueConstant(Offset, &Imm)) {
return;
}
// The immediate may be scaled in the IR, we need to correct for that.
Imm *= OffsetScale;
// Signed immediate unscaled 9-bit range for both regular and LRCPC2 ops.
bool IsSIMM9 = ((int64_t)Imm >= -256) && ((int64_t)Imm <= 255);
IsSIMM9 &= (SupportsTSOImm9 || !TSO);
// Extended offsets for regular loadstore only.
LOGMAN_THROW_A_FMT(IROp->Size >= IR::OpSize::i8Bit && IROp->Size <= (RegisterClass == GPRClass ? IR::OpSize::i64Bit : IR::OpSize::i256Bit),
"Invalid "
"size");
bool IsExtended = (Imm & (IR::OpSizeToSize(IROp->Size) - 1)) == 0 && Imm / IR::OpSizeToSize(IROp->Size) <= 4095;
IsExtended &= !TSO;
if (IsSIMM9 || IsExtended) {
IREmit->SetWriteCursor(IR.GetNode(Offset));
IREmit->ReplaceNodeArgument(CodeNode, Offset_Index, IREmit->_InlineConstant(Imm));
OffsetScale = 1;
}
}
};
// constprop + some more per instruction logic
void ConstProp::ConstantPropagation(IREmitter* IREmit, const IRListView& CurrentIR, Ref CodeNode, IROp_Header* IROp) {
switch (IROp->Op) {
case OP_ADD:
case OP_SUB:
case OP_ADDWITHFLAGS:
case OP_SUBWITHFLAGS: {
auto Op = IROp->C<IR::IROp_Add>();
uint64_t Constant1 {};
uint64_t Constant2 {};
bool IsConstant2 = IREmit->IsValueConstant(IROp->Args[1], &Constant2);
/* IsImmAddSub assumes the constants are sign-extended, take care of that
* here so we get the optimization for 32-bit adds too.
*/
if (Op->Header.Size == OpSize::i32Bit) {
Constant1 = (int64_t)(int32_t)Constant1;
Constant2 = (int64_t)(int32_t)Constant2;
}
if (IsConstant2 && !ARMEmitter::IsImmAddSub(Constant2) && ARMEmitter::IsImmAddSub(-Constant2)) {
// If the second argument is constant, the immediate is not ImmAddSub, but when negated is.
// So, negate the operation to negate (and inline) the constant.
if (IROp->Op == OP_ADD) {
IROp->Op = OP_SUB;
} else if (IROp->Op == OP_SUB) {
IROp->Op = OP_ADD;
} else if (IROp->Op == OP_ADDWITHFLAGS) {
IROp->Op = OP_SUBWITHFLAGS;
} else if (IROp->Op == OP_SUBWITHFLAGS) {
IROp->Op = OP_ADDWITHFLAGS;
}
IREmit->SetWriteCursorBefore(CodeNode);
// Negate the constant.
auto NegConstant = IREmit->_Constant(-Constant2);
// Replace the second source with the negated constant.
IREmit->ReplaceNodeArgument(CodeNode, Op->Src2_Index, NegConstant);
}
if (!InlineIfLargeAddSub(IREmit, CurrentIR, CodeNode, IROp, 1) && (IROp->Op == OP_SUB || IROp->Op == OP_SUBWITHFLAGS)) {
// TODO: Generalize this
InlineIfZero(IREmit, CurrentIR, CodeNode, IROp, 0);
}
break;
}
case OP_ADDNZCV: {
InlineIfLargeAddSub(IREmit, CurrentIR, CodeNode, IROp, 1);
break;
}
case OP_SUBNZCV: {
if (!InlineIfLargeAddSub(IREmit, CurrentIR, CodeNode, IROp, 1)) {
// TODO: Generalize this
InlineIfZero(IREmit, CurrentIR, CodeNode, IROp, 0);
}
break;
}
case OP_AND:
case OP_OR:
case OP_XOR: {
InlineIf(IREmit, CurrentIR, CodeNode, IROp, 1, [&IROp](uint64_t X) { return IsImmLogical(X, IR::OpSizeAsBits(IROp->Size)); });
break;
}
case OP_ANDWITHFLAGS:
case OP_ANDN:
case OP_TESTNZ: {
InlineIf(IREmit, CurrentIR, CodeNode, IROp, 1, [&IROp](uint64_t X) { return IsImmLogical(X, IR::OpSizeAsBits(IROp->Size)); });
break;
}
case OP_ASHR:
case OP_ROR: {
Inline(IREmit, CurrentIR, CodeNode, IROp, 1);
break;
}
case OP_LSHL: {
Inline(IREmit, CurrentIR, CodeNode, IROp, 1);
break;
}
case OP_LSHR: {
Inline(IREmit, CurrentIR, CodeNode, IROp, 1);
break;
}
case OP_ADC:
case OP_ADCWITHFLAGS:
case OP_RMIFNZCV: {
InlineIfZero(IREmit, CurrentIR, CodeNode, IROp, 0);
break;
}
case OP_STORECONTEXT: {
// For i128Bit, we won't see a normal Constant to inline, but as a special
// case we can replace with a 2x64-bit store which can use inline zeroes.
if (IROp->Size == OpSize::i128Bit) {
auto Op = IROp->C<IR::IROp_StoreContext>();
auto Header = IREmit->GetOpHeader(IROp->Args[0]);
const auto MAX_STP_OFFSET = (252 * 4);
if (Op->Offset <= MAX_STP_OFFSET && Header->Op == OP_LOADNAMEDVECTORCONSTANT) {
auto Const = Header->C<IR::IROp_LoadNamedVectorConstant>();
if (Const->Constant == IR::NamedVectorConstant::NAMED_VECTOR_ZERO) {
IREmit->SetWriteCursor(CodeNode);
Ref Zero = IREmit->_Constant(0);
Ref STP = IREmit->_StoreContextPair(IR::OpSize::i64Bit, GPRClass, Zero, Zero, Op->Offset);
IREmit->Remove(CodeNode);
// XXX: This works around InlineConstant not having an associated
// register class, else we'd just do InlineConstant above.
Ref InlineZero = IREmit->_InlineConstant(0);
IREmit->ReplaceNodeArgument(STP, 0, InlineZero);
IREmit->ReplaceNodeArgument(STP, 1, InlineZero);
}
}
} else {
InlineIfZero(IREmit, CurrentIR, CodeNode, IROp, 0);
}
break;
}
case OP_CONDADDNZCV:
case OP_CONDSUBNZCV: {
InlineIfZero(IREmit, CurrentIR, CodeNode, IROp, 0);
InlineIf(IREmit, CurrentIR, CodeNode, IROp, 1, ARMEmitter::IsImmAddSub);
break;
}
case OP_SELECT: {
InlineIf(IREmit, CurrentIR, CodeNode, IROp, 1, ARMEmitter::IsImmAddSub);
uint64_t AllOnes = IROp->Size == OpSize::i64Bit ? 0xffff'ffff'ffff'ffffull : 0xffff'ffffull;
uint64_t Constant2 {};
uint64_t Constant3 {};
if (IREmit->IsValueConstant(IROp->Args[2], &Constant2) && IREmit->IsValueConstant(IROp->Args[3], &Constant3) &&
(Constant2 == 1 || Constant2 == AllOnes) && Constant3 == 0) {
IREmit->SetWriteCursor(CurrentIR.GetNode(IROp->Args[2]));
IREmit->ReplaceNodeArgument(CodeNode, 2, IREmit->_InlineConstant(Constant2));
IREmit->ReplaceNodeArgument(CodeNode, 3, IREmit->_InlineConstant(Constant3));
}
break;
}
case OP_NZCVSELECT: {
// We always allow source 1 to be zero, but source 0 can only be a
// special 1/~0 constant if source 1 is 0.
if (InlineIfZero(IREmit, CurrentIR, CodeNode, IROp, 1)) {
uint64_t AllOnes = IROp->Size == OpSize::i64Bit ? 0xffff'ffff'ffff'ffffull : 0xffff'ffffull;
InlineIf(IREmit, CurrentIR, CodeNode, IROp, 0, [&AllOnes](uint64_t X) { return X == 1 || X == AllOnes; });
}
break;
}
case OP_CONDJUMP: {
InlineIf(IREmit, CurrentIR, CodeNode, IROp, 1, ARMEmitter::IsImmAddSub);
break;
}
case OP_EXITFUNCTION: {
auto Op = IROp->C<IR::IROp_ExitFunction>();
if (!Inline(IREmit, CurrentIR, CodeNode, IROp, Op->NewRIP_Index)) {
auto NewRIP = IREmit->GetOpHeader(Op->NewRIP);
if (NewRIP->Op == OP_ENTRYPOINTOFFSET) {
auto EO = NewRIP->C<IR::IROp_EntrypointOffset>();
IREmit->SetWriteCursor(CurrentIR.GetNode(Op->NewRIP));
IREmit->ReplaceNodeArgument(CodeNode, 0, IREmit->_InlineEntrypointOffset(EO->Header.Size, EO->Offset));
}
}
break;
}
case OP_LOADMEM: {
auto Op = IROp->CW<IR::IROp_LoadMem>();
InlineMemImmediate(IREmit, CurrentIR, CodeNode, Op->Class, IROp, Op->Offset, Op->OffsetType, Op->Offset_Index, Op->OffsetScale, false);
break;
}
case OP_STOREMEM: {
auto Op = IROp->CW<IR::IROp_StoreMem>();
InlineMemImmediate(IREmit, CurrentIR, CodeNode, Op->Class, IROp, Op->Offset, Op->OffsetType, Op->Offset_Index, Op->OffsetScale, false);
InlineIfZero(IREmit, CurrentIR, CodeNode, IROp, Op->Value_Index);
break;
}
case OP_PREFETCH: {
auto Op = IROp->CW<IR::IROp_Prefetch>();
InlineMemImmediate(IREmit, CurrentIR, CodeNode, GPRClass, IROp, Op->Offset, Op->OffsetType, Op->Offset_Index, Op->OffsetScale, false);
break;
}
case OP_LOADMEMTSO: {
auto Op = IROp->CW<IR::IROp_LoadMemTSO>();
InlineMemImmediate(IREmit, CurrentIR, CodeNode, Op->Class, IROp, Op->Offset, Op->OffsetType, Op->Offset_Index, Op->OffsetScale, true);
break;
}
case OP_STOREMEMTSO: {
auto Op = IROp->CW<IR::IROp_StoreMemTSO>();
InlineMemImmediate(IREmit, CurrentIR, CodeNode, Op->Class, IROp, Op->Offset, Op->OffsetType, Op->Offset_Index, Op->OffsetScale, true);
InlineIfZero(IREmit, CurrentIR, CodeNode, IROp, Op->Value_Index);
break;
}
case OP_STOREMEMPAIR: {
auto Op = IROp->CW<IR::IROp_StoreMemPair>();
InlineIfZero(IREmit, CurrentIR, CodeNode, IROp, Op->Value1_Index);
InlineIfZero(IREmit, CurrentIR, CodeNode, IROp, Op->Value2_Index);
break;
}
case OP_MEMCPY: {
auto Op = IROp->CW<IR::IROp_MemCpy>();
Inline(IREmit, CurrentIR, CodeNode, IROp, Op->Direction_Index);
break;
}
case OP_MEMSET: {
auto Op = IROp->CW<IR::IROp_MemSet>();
Inline(IREmit, CurrentIR, CodeNode, IROp, Op->Direction_Index);
InlineIfZero(IREmit, CurrentIR, CodeNode, IROp, Op->Value_Index);
break;
}
default: break;
}
}
void ConstProp::Run(IREmitter* IREmit) {
FEXCORE_PROFILE_SCOPED("PassManager::ConstProp");
auto CurrentIR = IREmit->ViewIR();
const uint32_t SSACount = CurrentIR.GetSSACount();
// Allocation/initialization deferred until first use, since many multiblocks
// don't have constants leftover after all inlining.
fextl::vector<Ref> Remap {};
struct Entry {
int64_t Value;
Ref R;
};
fextl::vector<Entry> Pool {};
for (auto [BlockNode, BlockIROp] : CurrentIR.GetBlocks()) {
Pool.clear();
for (auto [CodeNode, IROp] : CurrentIR.GetCode(BlockNode)) {
if (IROp->Op == OP_CONSTANT) {
auto Op = IROp->C<IR::IROp_Constant>();
bool Found = false;
// Search for the constant. This is O(n^2) but n is small since it's
// local and most constants are inlined. In practice, it ends up much
// faster than a hash table.
for (auto K : Pool) {
if (K.Value == Op->Constant) {
uint32_t Value = CurrentIR.GetID(CodeNode).Value;
if (Value < SSACount) {
if (Remap.empty()) {
Remap.resize(SSACount, nullptr);
}
Remap[Value] = K.R;
}
Found = true;
break;
}
}
if (!Found) {
Pool.push_back({.Value = Op->Constant, .R = CodeNode});
}
continue;
}
ConstantPropagation(IREmit, CurrentIR, CodeNode, IROp);
if (!Remap.empty()) {
const uint8_t NumArgs = IR::GetArgs(IROp->Op);
for (uint8_t i = 0; i < NumArgs; ++i) {
if (IROp->Args[i].IsInvalid()) {
continue;
}
uint32_t Value = IROp->Args[i].ID().Value;
if (Value < SSACount) {
Ref New = Remap[Value];
if (New) {
IREmit->ReplaceNodeArgument(CodeNode, i, New);
}
}
}
}
}
}
}
fextl::unique_ptr<FEXCore::IR::Pass> CreateConstProp(bool SupportsTSOImm9) {
return fextl::make_unique<ConstProp>(SupportsTSOImm9);
}
} // namespace FEXCore::IR