Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
cf2f9db
[InstCombine] Add pre-commit tests for boolean canonicalization (NFC)
yafet-a Jul 19, 2025
3a55b19
[InstCombine] Optimised expressions in issue #97044
yafet-a Jul 21, 2025
02807e3
3 input handled via truth table
yafet-a Jul 29, 2025
af90743
Merge branch 'main' into users/yafet-a/boolean-optimisation
yafet-a Jul 29, 2025
d066a85
Move simple expression check to caller
yafet-a Aug 7, 2025
4c86e54
removed recursion + smallptrset used
yafet-a Aug 7, 2025
7a2dc67
moved calls to consistent location in each visit function + added cal…
yafet-a Aug 7, 2025
6772db5
traverse only if node belongs to expr tree
yafet-a Aug 11, 2025
5405486
Refactor
yafet-a Aug 11, 2025
cb1e164
Merge branch 'main' into users/yafet-a/boolean-optimisation
yafet-a Aug 11, 2025
650a7ab
check for instructions being in the same bb to avoid comesBefore cros…
yafet-a Aug 13, 2025
18e576e
review (batch evaluation, refactors)
yafet-a Aug 14, 2025
28d4a0f
Add negative tests
yafet-a Aug 14, 2025
1fee55f
correctly checking for vars in same bb in extractThreeVariables()
yafet-a Aug 14, 2025
a39a3b4
reuse visited set in extractThreeVariables
yafet-a Aug 15, 2025
23feb15
multi-use tests + negative tests with and/or Var, Const nodes
yafet-a Aug 15, 2025
1a94bba
Computed Map validation in extractThreeVar
yafet-a Aug 15, 2025
d19190d
Pass instructions by reference instead of returning vectors
yafet-a Aug 19, 2025
9296d9b
early check for invalid num of variables
yafet-a Aug 20, 2025
48bd1ca
Improved sorting
yafet-a Aug 20, 2025
464d95e
treat non-bitwise ops as leaf nodes with use-count heuristic
yafet-a Aug 20, 2025
2a905fb
format
yafet-a Aug 20, 2025
fc2aac4
format-2
yafet-a Aug 20, 2025
d557827
validate no cross-BB instruction order comparison for computation ins…
yafet-a Aug 26, 2025
5c40046
(NFC: Styling) + Structural Similarity Check for loop
yafet-a Aug 29, 2025
7bf8caf
Traverse root operands to avoid treating them as leaf variables
yafet-a Aug 29, 2025
abd628d
NFC: negative test for treating root operands as leaf variables
yafet-a Aug 29, 2025
64fbafd
style: header comments
yafet-a Sep 1, 2025
ecd9669
[Tests] vector tests
yafet-a Sep 1, 2025
2b18e01
[NIT] improving header comments
yafet-a Sep 1, 2025
b238960
Merge branch 'main' into users/yafet-a/boolean-optimisation
ElvinaYakubova Oct 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 246 additions & 0 deletions llvm/lib/Transforms/InstCombine/InstCombineAndOrXor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "llvm/IR/PatternMatch.h"
#include "llvm/Transforms/InstCombine/InstCombiner.h"
#include "llvm/Transforms/Utils/Local.h"
#include <bitset>

using namespace llvm;
using namespace PatternMatch;
Expand Down Expand Up @@ -50,6 +51,242 @@ static Value *getFCmpValue(unsigned Code, Value *LHS, Value *RHS,
return Builder.CreateFCmpFMF(NewPred, LHS, RHS, FMF);
}

/// This is to create optimal 3-variable boolean logic from truth tables.
/// Currently it supports the cases for canonicalizing to the form ~((Op1 | Op2)
/// ^ Op0). More cases can be systematically added based on real-world
/// justification for specific 3 input cases.
static Value *createLogicFromTable3Var(const std::bitset<8> &Table, Value *Op0,
Value *Op1, Value *Op2, Value *Root,
IRBuilderBase &Builder) {
uint8_t TruthValue = Table.to_ulong();
auto FoldConstant = [&](bool Val) {
Type *Ty = Op0->getType();
return Val ? ConstantInt::getTrue(Ty) : ConstantInt::getFalse(Ty);
};

Value *Result = nullptr;
switch (TruthValue) {
default:
return nullptr;
case 0x00: // Always FALSE
Result = FoldConstant(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Result = FoldConstant(false);
Result = ConstantInt::getFalse(Op0->getType());

I don't think we need a helper function for this... (also for true below).

break;
case 0xFF: // Always TRUE
Result = FoldConstant(true);
break;
case 0xE1: // ~((Op1 | Op2) ^ Op0)
{
Value *Or = Builder.CreateOr(Op1, Op2);
Value *Xor = Builder.CreateXor(Or, Op0);
Result = Builder.CreateNot(Xor);
} break;
case 0x60: // Op0 & (Op1 ^ Op2)
{
Value *Xor = Builder.CreateXor(Op1, Op2);
Result = Builder.CreateAnd(Op0, Xor);
} break;
case 0xD2: // ((Op1 | Op2) ^ Op0) ^ Op1
{
Value *Or = Builder.CreateOr(Op1, Op2);
Value *Xor1 = Builder.CreateXor(Or, Op0);
Result = Builder.CreateXor(Xor1, Op1);
} break;
}

return Result;
}

/// Extracts exactly 3 variables for truth table optimization from a boolean
/// expression tree. Traverses single-use instructions, handles non-bitwise ops
/// as leaf variables, and validates the expression tree structure before
/// returning the variables in deterministic order. Returns {nullptr, nullptr,
/// nullptr} if the pattern doesn't match 3-variable optimization criteria in
/// order to enable an early return.
static std::tuple<Value *, Value *, Value *>
extractThreeVariablesAndInstructions(
Value *Root, SmallVectorImpl<Instruction *> &Instructions) {
SmallPtrSet<Value *, 3> Variables;
SmallPtrSet<Value *, 32> Visited;
SmallPtrSet<Value *, 8> RootOperands;
SmallVector<Value *> Worklist;
Worklist.push_back(Root);

// Traverse root operands to avoid treating them as leaf variables to prevent
// infinite cycles.
if (auto *RootInst = dyn_cast<Instruction>(Root))
for (Use &U : RootInst->operands())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (Use &U : RootInst->operands())
for (Value *Op : RootInst->operands())

If you're not using the Use, prefer directly iterating over Value *.

RootOperands.insert(U.get());

while (!Worklist.empty()) {
Value *V = Worklist.pop_back_val();

if (!Visited.insert(V).second)
continue;

// Due to lack of cost-based heuristic, only traverse if it belongs to this
// expression tree.
bool ShouldTraverse = (V == Root || V->hasOneUse());

if (Value *NotV; match(V, m_Not(m_Value(NotV)))) {
if (auto *I = dyn_cast<Instruction>(V))
Instructions.push_back(I);
if (ShouldTraverse)
Worklist.push_back(NotV);
continue;
}
if (auto *BO = dyn_cast<BinaryOperator>(V)) {
if (!BO->isBitwiseLogicOp()) {
if (V == Root)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this happen? Wouldn't this imply root is a non-bitwise op? I think this can be an assert.

return {nullptr, nullptr, nullptr};
if (!RootOperands.count(V))
Variables.insert(V);
continue;
}

Instructions.push_back(BO);

if (ShouldTraverse) {
Worklist.push_back(BO->getOperand(0));
Worklist.push_back(BO->getOperand(1));
}
} else if ((isa<Argument>(V) || isa<Instruction>(V)) && V != Root) {
if (!RootOperands.count(V))
Variables.insert(V);
}
}

if (Variables.size() != 3)
return {nullptr, nullptr, nullptr};
// Check that all instructions (both variables and computation instructions)
// are in the same BB.
SmallVector<Value *, 3> SortedVars(Variables.begin(), Variables.end());
BasicBlock *FirstBB = nullptr;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can simplify this by always comparing to Root->getParent() (after changing Root to an Instruction argument).


auto CheckSameBB = [&FirstBB](Instruction *I) -> bool {
if (!FirstBB)
FirstBB = I->getParent();
else if (I->getParent() != FirstBB)
return false;
return true;
};

for (Value *V : SortedVars)
if (auto *I = dyn_cast<Instruction>(V); I && !CheckSameBB(I))
return {nullptr, nullptr, nullptr};

for (Instruction *I : Instructions)
if (!CheckSameBB(I))
return {nullptr, nullptr, nullptr};

// Validation that all collected instructions have operands that will be in
// Computed map.
SmallPtrSet<Value *, 32> ValidOperands(Variables.begin(), Variables.end());
ValidOperands.insert(Instructions.begin(), Instructions.end());

for (Instruction *I : Instructions) {
Value *NotV;
bool IsNot = match(I, m_Not(m_Value(NotV)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline IsNot used in one place.


if (!IsNot) {
for (Use &U : I->operands()) {
if (!ValidOperands.count(U.get()))
return {nullptr, nullptr, nullptr};
}
} else if (!ValidOperands.count(NotV)) {
// For NOT: only check the variable operand (constant -1 is handled by
// pattern matcher).
return {nullptr, nullptr, nullptr};
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand correctly that what this actually does is discarding cases with constant operands, as everything else should have already been handled in the initial loop? If so, can we bail out on constant operands there already?


llvm::sort(SortedVars, [](Value *A, Value *B) {
if (isa<Argument>(A) != isa<Argument>(B))
return isa<Argument>(A);

if (isa<Argument>(A))
return cast<Argument>(A)->getArgNo() < cast<Argument>(B)->getArgNo();

return cast<Instruction>(A)->comesBefore(cast<Instruction>(B));
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for sorting the instructions is obvious, but I don't really understand why we need/want sorted variables.

Is this working around the fact that createLogicFromTable3Var only handles a subset of all cases and thus different variable order will fail the transform? If so, I don't think this is a good idea, as the result will be very fragile. E.g. if I take the first test and do this change, it's going to fail:

-define i32 @test0_4way_or(i32 %x, i32 %y, i32 %z) {
+define i32 @test0_4way_or(i32 %z, i32 %y, i32 %x) {

I think if we do any sorting for the variables it needs to happen at the level of createLogicFromTable3Var(), where we can e.g. try to swap variables to avoid having to handle all 255 cases, and only handle the non-commuted ones.


// Sort instructions (Useful until all 256 cases are added).
llvm::sort(Instructions,
[](Instruction *A, Instruction *B) { return A->comesBefore(B); });

return {SortedVars[0], SortedVars[1], SortedVars[2]};
}

/// Computes the 8-bit truth table for a 3-variable boolean expression using
/// symbolic execution. Assigns each variable a bit pattern representing when
/// it's true across all 8 input combinations, then simulates each instruction
/// with bitwise operations to obtain the final truth table. Returns the
/// resulting pattern where each bit represents the output for one input
/// combination.
static std::optional<std::bitset<8>>
evaluateBooleanExpression(Value *Expr, Value *Op0, Value *Op1, Value *Op2,
const SmallVector<Instruction *> &Instructions) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const SmallVector<Instruction *> &Instructions) {
ArrayRef<Instruction *> Instructions) {


// Initialize bit-vector values for the 3 variables as:
// Op0: 0b11110000 (true for combinations 000,001,010,011)
// Op1: 0b11001100 (true for combinations 000,001,100,101)
// Op2: 0b10101010 (true for combinations 000,010,100,110)
SmallDenseMap<Value *, std::bitset<8>> Computed;
Computed[Op0] = std::bitset<8>(0xF0); // 11110000
Computed[Op1] = std::bitset<8>(0xCC); // 11001100
Computed[Op2] = std::bitset<8>(0xAA); // 10101010

for (Instruction *I : Instructions) {
Value *NotV;
if (match(I, m_Not(m_Value(NotV)))) {
Computed[I] = ~Computed.at(NotV); // Bitwise NOT
} else if (auto *BO = dyn_cast<BinaryOperator>(I)) {
auto &LHS = Computed.at(BO->getOperand(0));
auto &RHS = Computed.at(BO->getOperand(1));

switch (BO->getOpcode()) {
case Instruction::And:
Computed[I] = LHS & RHS; // Bitwise AND
break;
case Instruction::Or:
Computed[I] = LHS | RHS; // Bitwise OR
break;
case Instruction::Xor:
Computed[I] = LHS ^ RHS; // Bitwise XOR
break;
default:
llvm_unreachable("Unexpected opcode in boolean expression evaluation");
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the case where it's neither Not nor BinaryOperator? I assume it can't happen, in which case we should make that dyn_cast above a cast.

}

return std::bitset<8>(Computed.at(Expr));
}

/// Entry point for the 3-variable boolean expression folding and handles early
/// returns.
static Value *foldThreeVarBoolExpr(Instruction &Root,
InstCombiner::BuilderTy &Builder) {

auto &BO = cast<BinaryOperator>(Root);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Directly accept BinaryOperator as the argument? (It will downcast to Instruction where needed.)

assert(BO.isBitwiseLogicOp() && "Unexpected opcode for boolean expression");

if (!isa<BinaryOperator>(BO.getOperand(0)) ||
!isa<BinaryOperator>(BO.getOperand(1)))
return nullptr;

SmallVector<Instruction *> Instructions;
auto [Op0, Op1, Op2] =
extractThreeVariablesAndInstructions(&Root, Instructions);
if (!Op0 || !Op1 || !Op2)
return nullptr;

auto Table = evaluateBooleanExpression(&Root, Op0, Op1, Op2, Instructions);
if (!Table)
return nullptr;

return createLogicFromTable3Var(*Table, Op0, Op1, Op2, &Root, Builder);
}

/// Emit a computation of: (V >= Lo && V < Hi) if Inside is true, otherwise
/// (V < Lo || V >= Hi). This method expects that Lo < Hi. IsSigned indicates
/// whether to treat V, Lo, and Hi as signed or not.
Expand Down Expand Up @@ -2421,6 +2658,9 @@ Instruction *InstCombinerImpl::visitAnd(BinaryOperator &I) {
if (Instruction *Phi = foldBinopWithPhiOperands(I))
return Phi;

if (Value *Canonical = foldThreeVarBoolExpr(I, Builder))
return replaceInstUsesWith(I, Canonical);

// See if we can simplify any instructions used by the instruction whose sole
// purpose is to compute bits we don't care about.
if (SimplifyDemandedInstructionBits(I))
Expand Down Expand Up @@ -4006,6 +4246,9 @@ Instruction *InstCombinerImpl::visitOr(BinaryOperator &I) {
if (Instruction *Phi = foldBinopWithPhiOperands(I))
return Phi;

if (Value *Canonical = foldThreeVarBoolExpr(I, Builder))
return replaceInstUsesWith(I, Canonical);

// See if we can simplify any instructions used by the instruction whose sole
// purpose is to compute bits we don't care about.
if (SimplifyDemandedInstructionBits(I))
Expand Down Expand Up @@ -5156,6 +5399,9 @@ Instruction *InstCombinerImpl::visitXor(BinaryOperator &I) {
if (Instruction *Phi = foldBinopWithPhiOperands(I))
return Phi;

if (Value *Canonical = foldThreeVarBoolExpr(I, Builder))
return replaceInstUsesWith(I, Canonical);

if (Instruction *NewXor = foldXorToXor(I, Builder))
return NewXor;

Expand Down
Loading