Skip to content

[SimplifyCFG] Supporting hoisting/sinking callbases with differing attrs #109472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions llvm/include/llvm/IR/InstrTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -1461,6 +1461,22 @@ class CallBase : public Instruction {
///
void setAttributes(AttributeList A) { Attrs = A; }

/// Try to intersect the attributes from 'this' CallBase and the
/// 'Other' CallBase. Sets the intersected attributes to 'this' and
/// return true if successful. Doesn't modify 'this' and returns
/// false if unsuccessful.
bool tryIntersectAttributes(const CallBase *Other) {
if (this == Other)
return true;
AttributeList AL = getAttributes();
AttributeList ALOther = Other->getAttributes();
auto Intersected = AL.intersectWith(getContext(), ALOther);
if (!Intersected)
return false;
setAttributes(*Intersected);
return true;
}

/// Determine whether this call has the given attribute. If it does not
/// then determine if the called function has the attribute, but only if
/// the attribute is allowed for the call.
Expand Down
14 changes: 9 additions & 5 deletions llvm/include/llvm/IR/Instruction.h
Original file line number Diff line number Diff line change
Expand Up @@ -881,16 +881,20 @@ class Instruction : public User,
/// This is like isIdenticalTo, except that it ignores the
/// SubclassOptionalData flags, which may specify conditions under which the
/// instruction's result is undefined.
bool isIdenticalToWhenDefined(const Instruction *I) const LLVM_READONLY;
bool
isIdenticalToWhenDefined(const Instruction *I,
bool IntersectAttrs = false) const LLVM_READONLY;

/// When checking for operation equivalence (using isSameOperationAs) it is
/// sometimes useful to ignore certain attributes.
enum OperationEquivalenceFlags {
/// Check for equivalence ignoring load/store alignment.
CompareIgnoringAlignment = 1<<0,
CompareIgnoringAlignment = 1 << 0,
/// Check for equivalence treating a type and a vector of that type
/// as equivalent.
CompareUsingScalarTypes = 1<<1
CompareUsingScalarTypes = 1 << 1,
/// Check for equivalence with intersected callbase attrs.
CompareUsingIntersectedAttrs = 1 << 2,
};

/// This function determines if the specified instruction executes the same
Expand All @@ -911,8 +915,8 @@ class Instruction : public User,
/// @returns true if the specific instruction has the same opcde specific
/// characteristics as the current one. Determine if one instruction has the
/// same state as another.
bool hasSameSpecialState(const Instruction *I2,
bool IgnoreAlignment = false) const LLVM_READONLY;
bool hasSameSpecialState(const Instruction *I2, bool IgnoreAlignment = false,
bool IntersectAttrs = false) const LLVM_READONLY;

/// Return true if there are any uses of this instruction in blocks other than
/// the specified block. Note that PHI nodes are considered to evaluate their
Expand Down
32 changes: 22 additions & 10 deletions llvm/lib/IR/Instruction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -785,11 +785,21 @@ const char *Instruction::getOpcodeName(unsigned OpCode) {
/// This must be kept in sync with FunctionComparator::cmpOperations in
/// lib/Transforms/IPO/MergeFunctions.cpp.
bool Instruction::hasSameSpecialState(const Instruction *I2,
bool IgnoreAlignment) const {
bool IgnoreAlignment,
bool IntersectAttrs) const {
auto I1 = this;
assert(I1->getOpcode() == I2->getOpcode() &&
"Can not compare special state of different instructions");

auto CheckAttrsSame = [IntersectAttrs](const CallBase *CB0,
const CallBase *CB1) {
return IntersectAttrs
? CB0->getAttributes()
.intersectWith(CB0->getContext(), CB1->getAttributes())
.has_value()
: CB0->getAttributes() == CB1->getAttributes();
};

if (const AllocaInst *AI = dyn_cast<AllocaInst>(I1))
return AI->getAllocatedType() == cast<AllocaInst>(I2)->getAllocatedType() &&
(AI->getAlign() == cast<AllocaInst>(I2)->getAlign() ||
Expand All @@ -811,15 +821,15 @@ bool Instruction::hasSameSpecialState(const Instruction *I2,
if (const CallInst *CI = dyn_cast<CallInst>(I1))
return CI->isTailCall() == cast<CallInst>(I2)->isTailCall() &&
CI->getCallingConv() == cast<CallInst>(I2)->getCallingConv() &&
CI->getAttributes() == cast<CallInst>(I2)->getAttributes() &&
CheckAttrsSame(CI, cast<CallInst>(I2)) &&
CI->hasIdenticalOperandBundleSchema(*cast<CallInst>(I2));
if (const InvokeInst *CI = dyn_cast<InvokeInst>(I1))
return CI->getCallingConv() == cast<InvokeInst>(I2)->getCallingConv() &&
CI->getAttributes() == cast<InvokeInst>(I2)->getAttributes() &&
CheckAttrsSame(CI, cast<InvokeInst>(I2)) &&
CI->hasIdenticalOperandBundleSchema(*cast<InvokeInst>(I2));
if (const CallBrInst *CI = dyn_cast<CallBrInst>(I1))
return CI->getCallingConv() == cast<CallBrInst>(I2)->getCallingConv() &&
CI->getAttributes() == cast<CallBrInst>(I2)->getAttributes() &&
CheckAttrsSame(CI, cast<CallBrInst>(I2)) &&
CI->hasIdenticalOperandBundleSchema(*cast<CallBrInst>(I2));
if (const InsertValueInst *IVI = dyn_cast<InsertValueInst>(I1))
return IVI->getIndices() == cast<InsertValueInst>(I2)->getIndices();
Expand Down Expand Up @@ -857,10 +867,10 @@ bool Instruction::isIdenticalTo(const Instruction *I) const {
SubclassOptionalData == I->SubclassOptionalData;
}

bool Instruction::isIdenticalToWhenDefined(const Instruction *I) const {
bool Instruction::isIdenticalToWhenDefined(const Instruction *I,
bool IntersectAttrs) const {
if (getOpcode() != I->getOpcode() ||
getNumOperands() != I->getNumOperands() ||
getType() != I->getType())
getNumOperands() != I->getNumOperands() || getType() != I->getType())
return false;

// If both instructions have no operands, they are identical.
Expand All @@ -879,15 +889,17 @@ bool Instruction::isIdenticalToWhenDefined(const Instruction *I) const {
otherPHI->block_begin());
}

return this->hasSameSpecialState(I);
return this->hasSameSpecialState(I, /*IgnoreAlignment=*/false,
IntersectAttrs);
}

// Keep this in sync with FunctionComparator::cmpOperations in
// lib/Transforms/IPO/MergeFunctions.cpp.
bool Instruction::isSameOperationAs(const Instruction *I,
unsigned flags) const {
bool IgnoreAlignment = flags & CompareIgnoringAlignment;
bool UseScalarTypes = flags & CompareUsingScalarTypes;
bool UseScalarTypes = flags & CompareUsingScalarTypes;
bool IntersectAttrs = flags & CompareUsingIntersectedAttrs;

if (getOpcode() != I->getOpcode() ||
getNumOperands() != I->getNumOperands() ||
Expand All @@ -905,7 +917,7 @@ bool Instruction::isSameOperationAs(const Instruction *I,
getOperand(i)->getType() != I->getOperand(i)->getType())
return false;

return this->hasSameSpecialState(I, IgnoreAlignment);
return this->hasSameSpecialState(I, IgnoreAlignment, IntersectAttrs);
}

bool Instruction::isUsedOutsideOfBlock(const BasicBlock *BB) const {
Expand Down
19 changes: 17 additions & 2 deletions llvm/lib/Transforms/Utils/SimplifyCFG.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1594,7 +1594,7 @@ static void hoistLockstepIdenticalDbgVariableRecords(

static bool areIdenticalUpToCommutativity(const Instruction *I1,
const Instruction *I2) {
if (I1->isIdenticalToWhenDefined(I2))
if (I1->isIdenticalToWhenDefined(I2, /*IntersectAttrs=*/true))
return true;

if (auto *Cmp1 = dyn_cast<CmpInst>(I1))
Expand Down Expand Up @@ -1910,6 +1910,14 @@ bool SimplifyCFGOpt::hoistCommonCodeFromSuccessors(Instruction *TI,
if (!I2->use_empty())
I2->replaceAllUsesWith(I1);
I1->andIRFlags(I2);
if (auto *CB = dyn_cast<CallBase>(I1)) {
bool Success = CB->tryIntersectAttributes(cast<CallBase>(I2));
assert(Success && "We should not be trying to hoist callbases "
"with non-intersectable attributes");
// For NDEBUG Compile.
Copy link
Contributor

Choose a reason for hiding this comment

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

Using [[maybe_unused]] may be cleaner.

(void)Success;
}

combineMetadataForCSE(I1, I2, true);
// I1 and I2 are being combined into a single instruction. Its debug
// location is the merged locations of the original instructions.
Expand Down Expand Up @@ -2130,7 +2138,7 @@ static bool canSinkInstructions(
const Instruction *I0 = Insts.front();
const auto I0MMRA = MMRAMetadata(*I0);
for (auto *I : Insts) {
if (!I->isSameOperationAs(I0))
if (!I->isSameOperationAs(I0, Instruction::CompareUsingIntersectedAttrs))
return false;

// swifterror pointers can only be used by a load or store; sinking a load
Expand Down Expand Up @@ -2287,6 +2295,13 @@ static void sinkLastInstruction(ArrayRef<BasicBlock*> Blocks) {
I0->applyMergedLocation(I0->getDebugLoc(), I->getDebugLoc());
combineMetadataForCSE(I0, I, true);
I0->andIRFlags(I);
if (auto *CB = dyn_cast<CallBase>(I0)) {
bool Success = CB->tryIntersectAttributes(cast<CallBase>(I));
assert(Success && "We should not be trying to sink callbases "
"with non-intersectable attributes");
// For NDEBUG Compile.
(void)Success;
}
}

for (User *U : make_early_inc_range(I0->users())) {
Expand Down
202 changes: 202 additions & 0 deletions llvm/test/Transforms/SimplifyCFG/hoist-cb-diff-attrs.ll
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --function-signature --check-attributes --check-globals
; RUN: opt < %s -passes='simplifycfg<hoist-common-insts>' -simplifycfg-require-and-preserve-domtree=1 -S | FileCheck %s

declare ptr @foo(ptr %p, i64 %x)
declare void @side.effect()

define ptr @test_hoist_int_attrs(i1 %c, ptr %p, i64 %x) {
; CHECK-LABEL: define {{[^@]+}}@test_hoist_int_attrs
; CHECK-SAME: (i1 [[C:%.*]], ptr [[P:%.*]], i64 [[X:%.*]]) {
; CHECK-NEXT: [[R:%.*]] = call ptr @foo(ptr align 32 dereferenceable(50) dereferenceable_or_null(100) [[P]], i64 range(i64 10, 100000) [[X]]) #[[ATTR0:[0-9]+]]
; CHECK-NEXT: br i1 [[C]], label [[COMMON_RET:%.*]], label [[ELSE:%.*]]
; CHECK: common.ret:
; CHECK-NEXT: ret ptr [[R]]
; CHECK: else:
; CHECK-NEXT: call void @side.effect()
; CHECK-NEXT: br label [[COMMON_RET]]
;
br i1 %c, label %if, label %else
if:
%r = call ptr @foo(ptr dereferenceable(50) align 64 dereferenceable_or_null(100) %p, i64 range(i64 10, 1000) %x) memory(read)
Copy link
Contributor

Choose a reason for hiding this comment

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

Having both dereferenceable and dereferenceable_or_null on the same argument is pretty weird. I'd use a separate argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair enough. Realize, we could actually intersect dereferenceable with derefenceable_or_null -> derefereanceable_or_null.

Might make a PR for it.

ret ptr %r

else:
%r2 = call ptr @foo(ptr dereferenceable(100) align 32 dereferenceable_or_null(200) %p, i64 range(i64 10000, 100000) %x) memory(write)
call void @side.effect()
ret ptr %r2
}

define ptr @test_hoist_int_attrs2(i1 %c, ptr %p, i64 %x) {
; CHECK-LABEL: define {{[^@]+}}@test_hoist_int_attrs2
; CHECK-SAME: (i1 [[C:%.*]], ptr [[P:%.*]], i64 [[X:%.*]]) {
; CHECK-NEXT: [[R:%.*]] = call ptr @foo(ptr dereferenceable(50) [[P]], i64 range(i64 10, 1000) [[X]]) #[[ATTR1:[0-9]+]]
; CHECK-NEXT: br i1 [[C]], label [[COMMON_RET:%.*]], label [[ELSE:%.*]]
; CHECK: common.ret:
; CHECK-NEXT: ret ptr [[R]]
; CHECK: else:
; CHECK-NEXT: call void @side.effect()
; CHECK-NEXT: br label [[COMMON_RET]]
;
br i1 %c, label %if, label %else
if:
%r = call ptr @foo(ptr dereferenceable(50) %p, i64 range(i64 10, 1000) %x) memory(read)
ret ptr %r

else:
%r2 = call ptr @foo(ptr dereferenceable(100) align 32 dereferenceable_or_null(200) %p, i64 range(i64 11, 100) %x) memory(none)
call void @side.effect()
ret ptr %r2
}

define ptr @test_hoist_bool_attrs2(i1 %c, ptr %p, i64 %x) {
; CHECK-LABEL: define {{[^@]+}}@test_hoist_bool_attrs2
; CHECK-SAME: (i1 [[C:%.*]], ptr [[P:%.*]], i64 [[X:%.*]]) {
; CHECK-NEXT: [[R:%.*]] = call noundef ptr @foo(ptr nonnull [[P]], i64 noundef [[X]]) #[[ATTR2:[0-9]+]]
; CHECK-NEXT: br i1 [[C]], label [[COMMON_RET:%.*]], label [[ELSE:%.*]]
; CHECK: common.ret:
; CHECK-NEXT: ret ptr [[R]]
; CHECK: else:
; CHECK-NEXT: call void @side.effect()
; CHECK-NEXT: br label [[COMMON_RET]]
;
br i1 %c, label %if, label %else
if:
%r = call noundef ptr @foo(ptr readnone nonnull noundef %p, i64 noundef %x) cold mustprogress nocallback nofree nosync willreturn
ret ptr %r

else:
%r2 = call noundef nonnull ptr @foo(ptr readonly nonnull %p, i64 noundef %x) mustprogress nocallback nofree willreturn
call void @side.effect()
ret ptr %r2
}

define ptr @test_hoist_bool_attrs3(i1 %c, ptr %p, i64 %x) {
; CHECK-LABEL: define {{[^@]+}}@test_hoist_bool_attrs3
; CHECK-SAME: (i1 [[C:%.*]], ptr [[P:%.*]], i64 [[X:%.*]]) {
; CHECK-NEXT: [[R:%.*]] = call nonnull ptr @foo(ptr [[P]], i64 noundef [[X]]) #[[ATTR3:[0-9]+]]
; CHECK-NEXT: br i1 [[C]], label [[COMMON_RET:%.*]], label [[ELSE:%.*]]
; CHECK: common.ret:
; CHECK-NEXT: ret ptr [[R]]
; CHECK: else:
; CHECK-NEXT: call void @side.effect()
; CHECK-NEXT: br label [[COMMON_RET]]
;
br i1 %c, label %if, label %else
if:
%r = call nonnull ptr @foo(ptr readonly noundef %p, i64 noundef %x) cold nocallback nofree nosync willreturn alwaysinline
ret ptr %r

else:
%r2 = call noundef nonnull ptr @foo(ptr writeonly nonnull %p, i64 noundef %x) nosync willreturn alwaysinline
call void @side.effect()
ret ptr %r2
}

define ptr @test_hoist_bool_attrs_fail_non_droppable(i1 %c, ptr %p, i64 %x) {
; CHECK-LABEL: define {{[^@]+}}@test_hoist_bool_attrs_fail_non_droppable
; CHECK-SAME: (i1 [[C:%.*]], ptr [[P:%.*]], i64 [[X:%.*]]) {
; CHECK-NEXT: br i1 [[C]], label [[IF:%.*]], label [[ELSE:%.*]]
; CHECK: common.ret:
; CHECK-NEXT: [[COMMON_RET_OP:%.*]] = phi ptr [ [[R:%.*]], [[IF]] ], [ [[R2:%.*]], [[ELSE]] ]
; CHECK-NEXT: ret ptr [[COMMON_RET_OP]]
; CHECK: if:
; CHECK-NEXT: [[R]] = call nonnull ptr @foo(ptr noundef readonly [[P]], i64 noundef [[X]]) #[[ATTR4:[0-9]+]]
; CHECK-NEXT: br label [[COMMON_RET:%.*]]
; CHECK: else:
; CHECK-NEXT: [[R2]] = call noundef nonnull ptr @foo(ptr nonnull writeonly [[P]], i64 noundef [[X]]) #[[ATTR5:[0-9]+]]
; CHECK-NEXT: call void @side.effect()
; CHECK-NEXT: br label [[COMMON_RET]]
;
br i1 %c, label %if, label %else
if:
%r = call nonnull ptr @foo(ptr readonly noundef %p, i64 noundef %x) cold nocallback nofree nosync willreturn alwaysinline
ret ptr %r

else:
%r2 = call noundef nonnull ptr @foo(ptr writeonly nonnull %p, i64 noundef %x) nosync willreturn
call void @side.effect()
ret ptr %r2
}

define ptr @test_hoist_bool_attrs_fail_non_droppable2(i1 %c, ptr %p, i64 %x) {
; CHECK-LABEL: define {{[^@]+}}@test_hoist_bool_attrs_fail_non_droppable2
; CHECK-SAME: (i1 [[C:%.*]], ptr [[P:%.*]], i64 [[X:%.*]]) {
; CHECK-NEXT: br i1 [[C]], label [[IF:%.*]], label [[ELSE:%.*]]
; CHECK: common.ret:
; CHECK-NEXT: [[COMMON_RET_OP:%.*]] = phi ptr [ [[R:%.*]], [[IF]] ], [ [[R2:%.*]], [[ELSE]] ]
; CHECK-NEXT: ret ptr [[COMMON_RET_OP]]
; CHECK: if:
; CHECK-NEXT: [[R]] = call nonnull ptr @foo(ptr noundef readonly [[P]], i64 noundef [[X]]) #[[ATTR6:[0-9]+]]
; CHECK-NEXT: br label [[COMMON_RET:%.*]]
; CHECK: else:
; CHECK-NEXT: [[R2]] = call noundef nonnull ptr @foo(ptr nonnull writeonly byval(i64) [[P]], i64 noundef [[X]]) #[[ATTR5]]
; CHECK-NEXT: call void @side.effect()
; CHECK-NEXT: br label [[COMMON_RET]]
;
br i1 %c, label %if, label %else
if:
%r = call nonnull ptr @foo(ptr readonly noundef %p, i64 noundef %x) cold nocallback nofree nosync willreturn
ret ptr %r

else:
%r2 = call noundef nonnull ptr @foo(ptr byval(i64) writeonly nonnull %p, i64 noundef %x) nosync willreturn
call void @side.effect()
ret ptr %r2
}

define ptr @test_hoist_bool_attrs_fail_non_droppable3(i1 %c, ptr %p, i64 %x) {
; CHECK-LABEL: define {{[^@]+}}@test_hoist_bool_attrs_fail_non_droppable3
; CHECK-SAME: (i1 [[C:%.*]], ptr [[P:%.*]], i64 [[X:%.*]]) {
; CHECK-NEXT: br i1 [[C]], label [[IF:%.*]], label [[ELSE:%.*]]
; CHECK: common.ret:
; CHECK-NEXT: [[COMMON_RET_OP:%.*]] = phi ptr [ [[R:%.*]], [[IF]] ], [ [[R2:%.*]], [[ELSE]] ]
; CHECK-NEXT: ret ptr [[COMMON_RET_OP]]
; CHECK: if:
; CHECK-NEXT: [[R]] = call nonnull ptr @foo(ptr noundef readonly byval(i32) [[P]], i64 noundef [[X]]) #[[ATTR6]]
; CHECK-NEXT: br label [[COMMON_RET:%.*]]
; CHECK: else:
; CHECK-NEXT: [[R2]] = call noundef nonnull ptr @foo(ptr nonnull writeonly byval(i64) [[P]], i64 noundef [[X]]) #[[ATTR5]]
; CHECK-NEXT: call void @side.effect()
; CHECK-NEXT: br label [[COMMON_RET]]
;
br i1 %c, label %if, label %else
if:
%r = call nonnull ptr @foo(ptr byval(i32) readonly noundef %p, i64 noundef %x) cold nocallback nofree nosync willreturn
ret ptr %r

else:
%r2 = call noundef nonnull ptr @foo(ptr byval(i64) writeonly nonnull %p, i64 noundef %x) nosync willreturn
call void @side.effect()
ret ptr %r2
}

define ptr @test_hoist_bool_attrs4(i1 %c, ptr %p, i64 %x) {
; CHECK-LABEL: define {{[^@]+}}@test_hoist_bool_attrs4
; CHECK-SAME: (i1 [[C:%.*]], ptr [[P:%.*]], i64 [[X:%.*]]) {
; CHECK-NEXT: [[R:%.*]] = call nonnull ptr @foo(ptr byval(i64) [[P]], i64 noundef [[X]]) #[[ATTR5]]
; CHECK-NEXT: br i1 [[C]], label [[COMMON_RET:%.*]], label [[ELSE:%.*]]
; CHECK: common.ret:
; CHECK-NEXT: ret ptr [[R]]
; CHECK: else:
; CHECK-NEXT: call void @side.effect()
; CHECK-NEXT: br label [[COMMON_RET]]
;
br i1 %c, label %if, label %else
if:
%r = call nonnull ptr @foo(ptr byval(i64) readonly noundef %p, i64 noundef %x) cold nocallback nofree nosync willreturn
ret ptr %r

else:
%r2 = call noundef nonnull ptr @foo(ptr byval(i64) writeonly nonnull %p, i64 noundef %x) nosync willreturn
call void @side.effect()
ret ptr %r2
}
;.
; CHECK: attributes #[[ATTR0]] = { memory(readwrite) }
; CHECK: attributes #[[ATTR1]] = { memory(read) }
; CHECK: attributes #[[ATTR2]] = { mustprogress nocallback nofree willreturn }
; CHECK: attributes #[[ATTR3]] = { alwaysinline nosync willreturn }
; CHECK: attributes #[[ATTR4]] = { alwaysinline cold nocallback nofree nosync willreturn }
; CHECK: attributes #[[ATTR5]] = { nosync willreturn }
; CHECK: attributes #[[ATTR6]] = { cold nocallback nofree nosync willreturn }
;.
Loading
Loading