[CPU] Implement AND+NOT folding into AND_NOT

Detect dependent `AND` and `NOT` IR sequences and combine them into a
singular `AND_NOT` opcode. The later dead-code-elimination-pass will
get rid of the left-over `NOT` opcode if nothing else uses it.
This gets quite a good amount of hits in some of the titles I've tested.

Also updates unit tests with additional data-types and ensures that
`And(..., Not())` returns the same result as `AndNot(...)`
This commit is contained in:
Wunkolo
2026-03-30 21:58:35 -07:00
committed by Heel
parent 11ad6b0a87
commit c383d049ec
3 changed files with 208 additions and 0 deletions

View File

@@ -568,6 +568,10 @@ bool SimplificationPass::TryHandleANDROLORSHLSeq(hir::Instr* i,
bool SimplificationPass::CheckAnd(hir::Instr* i, hir::HIRBuilder* builder) {
retry_and_simplification:
if (SimplifyAndNot(i, builder)) {
return true;
}
auto [constant_value, variable_value] = i->BinaryValueArrangeAsConstAndVar();
if (!constant_value) {
// added this for srawi
@@ -1247,6 +1251,38 @@ bool SimplificationPass::SimplifyAddArith(hir::Instr* i,
return false;
}
bool SimplificationPass::SimplifyAndNot(hir::Instr* i,
hir::HIRBuilder* builder) {
// check if either of the 2 AND operands has just used NOT and fold into
// an AND_NOT opcode
Value* src1 = i->src1.value;
Value* src2 = i->src2.value;
Instr* def1 = src1->def;
Instr* def2 = src2->def;
if (!def1 || !def2) return false;
// Bypass the NOT from an incoming operand and combine it into AND_NOT.
// If the original NOT does not have any further uses, then the
// dead-code-elimination pass will delete it. Otherwise, if it still has uses,
// then there will still be a NOT operation.
if (def2->opcode == &OPCODE_NOT_info) {
// Fold src2's NOT into AND_NOT
i->Replace(&OPCODE_AND_NOT_info, 0);
i->set_src1(src1);
i->set_src2(def2->src1.value);
return true;
} else if (def1->opcode == &OPCODE_NOT_info) {
// Swap operands and fold src1's NOT into AND_NOT
i->Replace(&OPCODE_AND_NOT_info, 0);
i->set_src1(src2);
i->set_src2(def1->src1.value);
return true;
}
return false;
}
bool SimplificationPass::SimplifySubArith(hir::Instr* i,
hir::HIRBuilder* builder) {
/*

View File

@@ -42,6 +42,7 @@ class SimplificationPass : public ConditionalGroupSubpass {
bool SimplifyAddWithSHL(hir::Instr* i, hir::HIRBuilder* builder);
bool SimplifyAddToSelf(hir::Instr* i, hir::HIRBuilder* builder);
bool SimplifyAddArith(hir::Instr* i, hir::HIRBuilder* builder);
bool SimplifyAndNot(hir::Instr* i, hir::HIRBuilder* builder);
bool SimplifySubArith(hir::Instr* i, hir::HIRBuilder* builder);
bool SimplifySHLArith(hir::Instr* i, hir::HIRBuilder* builder);
// handle either or or xor with 0

View File

@@ -491,8 +491,100 @@ TEST_CASE("ATOMIC_COMPARE_EXCHANGE_I32", "[atomic]") {
// ============================================================================
// AND_NOT — bitwise AND with complement of second operand
// ============================================================================
TEST_CASE("AND_NOT_I8", "[bitwise]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 2,
b.ZeroExtend(b.And(b.Truncate(LoadGPR(b, 4), INT8_TYPE),
b.Not(b.Truncate(LoadGPR(b, 5), INT8_TYPE))),
INT64_TYPE));
StoreGPR(b, 3,
b.ZeroExtend(b.AndNot(b.Truncate(LoadGPR(b, 4), INT8_TYPE),
b.Truncate(LoadGPR(b, 5), INT8_TYPE)),
INT64_TYPE));
b.Return();
});
// result = src1 & ~src2
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0xFF;
ctx->r[5] = 0x0F;
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[2] == ctx->r[3]);
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0xF0);
});
// All bits masked out.
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0xAA;
ctx->r[5] = 0xFF;
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[2] == ctx->r[3]);
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0x00);
});
// No bits masked out.
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0x12;
ctx->r[5] = 0x00;
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[2] == ctx->r[3]);
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0x12);
});
}
TEST_CASE("AND_NOT_I16", "[bitwise]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 2,
b.ZeroExtend(b.And(b.Truncate(LoadGPR(b, 4), INT16_TYPE),
b.Not(b.Truncate(LoadGPR(b, 5), INT16_TYPE))),
INT64_TYPE));
StoreGPR(b, 3,
b.ZeroExtend(b.AndNot(b.Truncate(LoadGPR(b, 4), INT16_TYPE),
b.Truncate(LoadGPR(b, 5), INT16_TYPE)),
INT64_TYPE));
b.Return();
});
// result = src1 & ~src2
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0xFF00;
ctx->r[5] = 0x0F0F;
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[2] == ctx->r[3]);
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0xF000);
});
// All bits masked out.
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0xAAAA;
ctx->r[5] = 0xFFFF;
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[2] == ctx->r[3]);
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0x0000);
});
// No bits masked out.
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0x1234;
ctx->r[5] = 0x0000;
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[2] == ctx->r[3]);
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0x1234);
});
}
TEST_CASE("AND_NOT_I32", "[bitwise]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 2,
b.ZeroExtend(b.And(b.Truncate(LoadGPR(b, 4), INT32_TYPE),
b.Not(b.Truncate(LoadGPR(b, 5), INT32_TYPE))),
INT64_TYPE));
StoreGPR(b, 3,
b.ZeroExtend(b.AndNot(b.Truncate(LoadGPR(b, 4), INT32_TYPE),
b.Truncate(LoadGPR(b, 5), INT32_TYPE)),
@@ -506,6 +598,7 @@ TEST_CASE("AND_NOT_I32", "[bitwise]") {
ctx->r[5] = 0x0F0F0F0F;
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[2] == ctx->r[3]);
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0xF000F000);
});
// All bits masked out.
@@ -515,6 +608,7 @@ TEST_CASE("AND_NOT_I32", "[bitwise]") {
ctx->r[5] = 0xFFFFFFFF;
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[2] == ctx->r[3]);
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0x00000000);
});
// No bits masked out.
@@ -524,10 +618,87 @@ TEST_CASE("AND_NOT_I32", "[bitwise]") {
ctx->r[5] = 0x00000000;
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[2] == ctx->r[3]);
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0x12345678);
});
}
TEST_CASE("AND_NOT_I64", "[bitwise]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 2, b.And(LoadGPR(b, 4), b.Not(LoadGPR(b, 5))));
StoreGPR(b, 3, b.AndNot(LoadGPR(b, 4), LoadGPR(b, 5)));
b.Return();
});
// result = src1 & ~src2
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0xFF00FF00FF00FF00;
ctx->r[5] = 0x0F0F0F0F0F0F0F0F;
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[2] == ctx->r[3]);
REQUIRE(ctx->r[3] == 0xF000F000F000F000);
});
// All bits masked out.
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0xAAAAAAAAAAAAAAAA;
ctx->r[5] = 0xFFFFFFFFFFFFFFFF;
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[2] == ctx->r[3]);
REQUIRE(ctx->r[3] == 0x0000000000000000);
});
// No bits masked out.
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0x1234567812345678;
ctx->r[5] = 0x0000000000000000;
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[2] == ctx->r[3]);
REQUIRE(ctx->r[3] == 0x1234567812345678);
});
}
TEST_CASE("AND_NOT_V128", "[bitwise]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 2, b.And(LoadVR(b, 4), b.Not(LoadVR(b, 5))));
StoreVR(b, 3, b.AndNot(LoadVR(b, 4), LoadVR(b, 5)));
b.Return();
});
// result = src1 & ~src2
test.Run(
[](PPCContext* ctx) {
ctx->v[4] = vec128s(0xFF00);
ctx->v[5] = vec128s(0x0F0F);
},
[](PPCContext* ctx) {
REQUIRE(ctx->v[2] == ctx->v[3]);
REQUIRE(ctx->v[3] == vec128s(0xF000));
});
// All bits masked out.
test.Run(
[](PPCContext* ctx) {
ctx->v[4] = vec128b(0xAA);
ctx->v[5] = vec128b(0xFF);
},
[](PPCContext* ctx) {
REQUIRE(ctx->v[2] == ctx->v[3]);
REQUIRE(ctx->v[3] == vec128b(0x00));
});
// No bits masked out.
test.Run(
[](PPCContext* ctx) {
ctx->v[4] = vec128i(0x12345678);
ctx->v[5] = vec128i(0x00000000);
},
[](PPCContext* ctx) {
REQUIRE(ctx->v[2] == ctx->v[3]);
REQUIRE(ctx->v[3] == vec128i(0x12345678));
});
}
// ============================================================================
// TRUNCATE — integer narrowing
// ============================================================================