Skip to content

[SILOptimizer]: slow OSSA lifetime canonicalization mitigation #82313

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
15 changes: 15 additions & 0 deletions include/swift/SIL/BasicBlockUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ class DeadEndBlocks {
const SILFunction *f;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

posting here for discussion threading purposes:

@nate-chandler any pointers as to where/what new tests should be added? i did confirm that an existing canonicalization test fails if the new hasAnyDeadEnds() function is hard-coded to return false, so it seems there is some existing coverage.

Copy link
Contributor

@nate-chandler nate-chandler Jun 18, 2025

Choose a reason for hiding this comment

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

Yeah, the existing coverage of canonicalization is sufficient, I think.

If you want, you could add a new FunctionTest of the utility function DeadEndBlocks::isEmpty() in BasicBlockUtils.cpp. The full details for how to do that are in include/swift/SIL/Test.h, but briefly you'd add something like

namespace swift::test {
FunctionTest HasAnyDeadEndBlocksTest("has_any_dead_dends",
                            [](auto &function, auto &arguments, auto &test) {
                              auto *deb = test.getDeadEndBlocks();
                              llvm::outs() << deb->isEmpty() ? "no dead ends\n" : "has dead ends\n";
                            });
} // end namespace swift::test

and then write a SIL test case

// test/SILOptimizer/dead-end-blocks.sil

// RUN: %target-sil-opt                     \
// RUN:     -test-runner                    \
// RUN:     %s                              \
// RUN:     -o /dev/null                    \
// RUN: 2>&1 | %FileCheck %s


// CHECK-LABEL: begin running test {{.*}} on some_function_with_dead_ends
// CHECK:         no dead ends
// CHECK-LABEL: end running test {{.*}} on some_function_with_dead_ends
sil @some_function_with_dead_ends ...
  specify_test "has_any_dead_ends"

...

// CHECK-LABEL: begin running test {{.*}} on some_function_with_dead_ends
// CHECK:         has dead ends
// CHECK-LABEL: end running test {{.*}} on some_function_with_dead_ends
sil @some_function_without_dead_ends ...
  specify_test "has_any_dead_ends"

Copy link
Contributor

Choose a reason for hiding this comment

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

If you want to add more canonicalization tests, they would go in test/SILOptimizer/canonicalize_ossa_lifetime_unit.sil.

bool didComputeValue = false;

/// When non-null, indicates whether dead-end blocks are present
/// in the current function.
std::optional<bool> hasAnyDeadEnds = std::nullopt;

void compute();

public:
Expand All @@ -85,6 +89,17 @@ class DeadEndBlocks {
return reachableBlocks.count(block) == 0;
}

/// Returns true iff none of the function's blocks is a dead-end.
/// Note: The underlying value is lazily computed & cached.
bool isEmpty() {
if (!hasAnyDeadEnds.has_value()) {
hasAnyDeadEnds = llvm::any_of(
*f, [this](const SILBasicBlock &BB) { return isDeadEnd(&BB); });
}

return !hasAnyDeadEnds.value();
}

/// Return true if this dead end blocks has computed its internal cache yet.
///
/// Used to determine if we need to verify a DeadEndBlocks.
Expand Down
4 changes: 4 additions & 0 deletions include/swift/SILOptimizer/Utils/CanonicalizeOSSALifetime.h
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,10 @@ class CanonicalizeOSSALifetime final {
return !endingLifetimeAtExplicitEnds();
}

bool hasAnyDeadEnds() const {
return !deadEndBlocksAnalysis->get(function)->isEmpty();
}

bool respectsDeinitBarriers() const {
if (!currentDef->isLexical())
return false;
Expand Down
10 changes: 10 additions & 0 deletions lib/SIL/Utils/BasicBlockUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,16 @@ static FunctionTest DeadEndBlocksTest("dead_end_blocks", [](auto &function,
}
#endif
});

// Arguments:
// - none
// Dumps:
// - message
static FunctionTest HasAnyDeadEndBlocksTest(
"has_any_dead_ends", [](auto &function, auto &arguments, auto &test) {
auto deb = test.getDeadEndBlocks();
llvm::outs() << (deb->isEmpty() ? "no dead ends\n" : "has dead ends\n");
});
} // end namespace swift::test

//===----------------------------------------------------------------------===//
Expand Down
2 changes: 1 addition & 1 deletion lib/SILOptimizer/Utils/CanonicalizeOSSALifetime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1406,7 +1406,7 @@ bool CanonicalizeOSSALifetime::computeLiveness() {
clear();
return false;
}
if (respectsDeadEnds()) {
if (respectsDeadEnds() && hasAnyDeadEnds()) {
if (respectsDeinitBarriers()) {
extendLexicalLivenessToDeadEnds();
}
Expand Down
60 changes: 60 additions & 0 deletions test/SILOptimizer/dead_end_blocks.sil
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,64 @@ exit:
return %retval : $()
}

// no dead ends - simple return
// CHECK-LABEL: begin running test {{.*}} on simple_function: has_any_dead_ends
// CHECK: no dead ends
// CHECK-LABEL: end running test {{.*}} on simple_function: has_any_dead_ends
sil @simple_function : $@convention(thin) () -> () {
entry:
specify_test "has_any_dead_ends"
%retval = tuple ()
return %retval : $()
}

// dead ends - unreachable blocks
// CHECK-LABEL: begin running test {{.*}} on function_with_dead_ends: has_any_dead_ends
// CHECK: has dead ends
// CHECK-LABEL: end running test {{.*}} on function_with_dead_ends: has_any_dead_ends
sil @function_with_dead_ends : $@convention(thin) () -> () {
entry:
specify_test "has_any_dead_ends"
cond_br undef, die, exit

die:
unreachable

exit:
%retval = tuple ()
return %retval : $()
}

// dead ends – infinite loop
// CHECK-LABEL: begin running test {{.*}} on function_with_loop: has_any_dead_ends
// CHECK: has dead ends
// CHECK-LABEL: end running test {{.*}} on function_with_loop: has_any_dead_ends
sil @function_with_loop : $@convention(thin) () -> () {
entry:
specify_test "has_any_dead_ends"
cond_br undef, exit, loop

loop:
br loop

exit:
%retval = tuple ()
return %retval : $()
}

// no dead ends – conditional branches but all paths return
// CHECK-LABEL: begin running test {{.*}} on branching_no_dead_ends: has_any_dead_ends
// CHECK: no dead ends
// CHECK-LABEL: end running test {{.*}} on branching_no_dead_ends: has_any_dead_ends
sil @branching_no_dead_ends : $@convention(thin) () -> () {
entry:
specify_test "has_any_dead_ends"
cond_br undef, then, else

then:
br else

else:
%retval2 = tuple ()
return %retval2 : $()
}