Skip to content

Commit 2d1cbb6

Browse files
authored
Merge pull request #66779 from jckarter/noncopyable-address-only-borrowed-capture-5.9
[5.9] ClosureLifetimeFixup: Remove copy of borrowed move-only nonescaping captures when possible.
2 parents 618cc91 + 2a06186 commit 2d1cbb6

File tree

6 files changed

+285
-5
lines changed

6 files changed

+285
-5
lines changed

include/swift/SILOptimizer/Utils/InstOptUtils.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,9 @@ void insertDestroyOfCapturedArguments(
289289
SILLocation loc = RegularLocation::getAutoGeneratedLocation());
290290

291291
void insertDeallocOfCapturedArguments(PartialApplyInst *pai,
292-
DominanceInfo *domInfo);
292+
DominanceInfo *domInfo,
293+
llvm::function_ref<bool(SILValue)> shouldInsertDestroy =
294+
[](SILValue arg) -> bool { return true; });
293295

294296
/// This iterator 'looks through' one level of builtin expect users exposing all
295297
/// users of the looked through builtin expect instruction i.e it presents a

lib/SILOptimizer/Mandatory/ClosureLifetimeFixup.cpp

Lines changed: 177 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
#include "swift/Basic/Defer.h"
1616
#include "swift/SIL/DebugUtils.h"
1717
#include "swift/SIL/InstructionUtils.h"
18+
#include "swift/SIL/PrunedLiveness.h"
1819
#include "swift/SIL/SILArgument.h"
1920
#include "swift/SIL/SILBuilder.h"
2021
#include "swift/SIL/SILInstruction.h"
22+
#include "swift/SIL/SILValue.h"
2123
#include "swift/SIL/BasicBlockDatastructures.h"
2224
#include "swift/SILOptimizer/PassManager/Passes.h"
2325
#include "swift/SILOptimizer/PassManager/Transforms.h"
@@ -712,7 +714,171 @@ static SILValue tryRewriteToPartialApplyStack(
712714
// lifetime ends.
713715
SmallVector<SILInstruction *, 4> lifetimeEnds;
714716
collectStackClosureLifetimeEnds(lifetimeEnds, closureOp);
717+
718+
// For noncopyable address-only captures, see if we can eliminate the copy
719+
// that SILGen emitted to allow the original partial_apply to take ownership.
720+
// We do this here because otherwise the move checker will see the copy as an
721+
// attempt to consume the value, which we don't want.
722+
SmallVector<SILBasicBlock *, 8> discoveredBlocks;
723+
SSAPrunedLiveness closureLiveness(cvt->getFunction(), &discoveredBlocks);
724+
closureLiveness.initializeDef(closureOp);
725+
726+
SmallSetVector<SILValue, 4> borrowedOriginals;
727+
728+
for (unsigned i : indices(newPA->getArgumentOperands())) {
729+
auto &arg = newPA->getArgumentOperands()[i];
730+
SILValue copy = arg.get();
731+
// The temporary should be a local stack allocation.
732+
LLVM_DEBUG(llvm::dbgs() << "considering whether to eliminate copy of capture\n";
733+
copy->printInContext(llvm::dbgs());
734+
llvm::dbgs() << "\n");
735+
736+
auto stack = dyn_cast<AllocStackInst>(copy);
737+
if (!stack) {
738+
LLVM_DEBUG(llvm::dbgs() << "-- not an alloc_stack\n");
739+
continue;
740+
}
741+
742+
// This would be a nice optimization to attempt for all types, but for now,
743+
// limit the effect to move-only types.
744+
if (!copy->getType().isMoveOnly()) {
745+
LLVM_DEBUG(llvm::dbgs() << "-- not move-only\n");
746+
continue;
747+
}
748+
749+
// Is the capture a borrow?
750+
auto paramIndex = newPA
751+
->getArgumentIndexForOperandIndex(i + newPA->getArgumentOperandNumber())
752+
.getValue();
753+
if (!newPA->getOrigCalleeType()->getParameters()[paramIndex]
754+
.isIndirectInGuaranteed()) {
755+
LLVM_DEBUG(llvm::dbgs() << "-- not an in_guaranteed parameter\n";
756+
newPA->getOrigCalleeType()->getParameters()[paramIndex]
757+
.print(llvm::dbgs());
758+
llvm::dbgs() << "\n");
759+
continue;
760+
}
715761

762+
// It needs to have been initialized by copying from somewhere else.
763+
CopyAddrInst *initialization = nullptr;
764+
MarkDependenceInst *markDep = nullptr;
765+
for (auto *use : stack->getUses()) {
766+
// Since we removed the `dealloc_stack`s from the capture arguments,
767+
// the only uses of this stack slot should be the initialization, the
768+
// partial application, and possibly a mark_dependence from the
769+
// buffer to the partial application.
770+
if (use->getUser() == newPA) {
771+
continue;
772+
}
773+
if (auto mark = dyn_cast<MarkDependenceInst>(use->getUser())) {
774+
// If we're marking dependence of the current partial_apply on this
775+
// stack slot, that's fine.
776+
if (mark->getValue() != newPA
777+
|| mark->getBase() != stack) {
778+
LLVM_DEBUG(llvm::dbgs() << "-- had unexpected mark_dependence use\n";
779+
use->getUser()->print(llvm::dbgs());
780+
llvm::dbgs() << "\n");
781+
782+
break;
783+
}
784+
markDep = mark;
785+
continue;
786+
}
787+
788+
// If we saw more than just the initialization, this isn't a pattern we
789+
// recognize.
790+
if (initialization) {
791+
LLVM_DEBUG(llvm::dbgs() << "-- had non-initialization, non-partial-apply use\n";
792+
use->getUser()->print(llvm::dbgs());
793+
llvm::dbgs() << "\n");
794+
795+
initialization = nullptr;
796+
break;
797+
}
798+
if (auto possibleInit = dyn_cast<CopyAddrInst>(use->getUser())) {
799+
// Should copy the source and initialize the destination.
800+
if (possibleInit->isTakeOfSrc()
801+
|| !possibleInit->isInitializationOfDest()) {
802+
LLVM_DEBUG(llvm::dbgs() << "-- had non-initialization, non-partial-apply use\n";
803+
use->getUser()->print(llvm::dbgs());
804+
llvm::dbgs() << "\n");
805+
806+
break;
807+
}
808+
// This is the initialization if there are no other uses.
809+
initialization = possibleInit;
810+
continue;
811+
}
812+
LLVM_DEBUG(llvm::dbgs() << "-- unrecognized use\n");
813+
break;
814+
}
815+
if (!initialization) {
816+
LLVM_DEBUG(llvm::dbgs() << "-- failed to find single initializing use\n");
817+
continue;
818+
}
819+
820+
// The source should have no writes in the duration of the partial_apply's
821+
// liveness.
822+
auto orig = initialization->getSrc();
823+
LLVM_DEBUG(llvm::dbgs() << "++ found original:\n";
824+
orig->print(llvm::dbgs());
825+
llvm::dbgs() << "\n");
826+
827+
bool origIsUnusedDuringClosureLifetime = true;
828+
829+
class OrigUnusedDuringClosureLifetimeWalker final
830+
: public TransitiveAddressWalker
831+
{
832+
SSAPrunedLiveness &closureLiveness;
833+
bool &origIsUnusedDuringClosureLifetime;
834+
public:
835+
OrigUnusedDuringClosureLifetimeWalker(SSAPrunedLiveness &closureLiveness,
836+
bool &origIsUnusedDuringClosureLifetime)
837+
: closureLiveness(closureLiveness),
838+
origIsUnusedDuringClosureLifetime(origIsUnusedDuringClosureLifetime)
839+
{}
840+
841+
virtual bool visitUse(Operand *origUse) override {
842+
LLVM_DEBUG(llvm::dbgs() << "looking at use\n";
843+
origUse->getUser()->printInContext(llvm::dbgs());
844+
llvm::dbgs() << "\n");
845+
846+
// If the user doesn't write to memory, then it's harmless.
847+
if (!origUse->getUser()->mayWriteToMemory()) {
848+
return true;
849+
}
850+
if (closureLiveness.isWithinBoundary(origUse->getUser())) {
851+
origIsUnusedDuringClosureLifetime = false;
852+
LLVM_DEBUG(llvm::dbgs() << "-- original has other possibly writing use during closure lifetime\n";
853+
origUse->getUser()->print(llvm::dbgs());
854+
llvm::dbgs() << "\n");
855+
return false;
856+
}
857+
return true;
858+
}
859+
};
860+
861+
OrigUnusedDuringClosureLifetimeWalker origUseWalker(closureLiveness,
862+
origIsUnusedDuringClosureLifetime);
863+
auto walkResult = std::move(origUseWalker).walk(orig);
864+
865+
if (walkResult == AddressUseKind::Unknown
866+
|| !origIsUnusedDuringClosureLifetime) {
867+
continue;
868+
}
869+
870+
// OK, we can use the original. Eliminate the copy and replace it with the
871+
// original.
872+
LLVM_DEBUG(llvm::dbgs() << "++ replacing with original!\n");
873+
arg.set(orig);
874+
if (markDep) {
875+
markDep->setBase(orig);
876+
}
877+
initialization->eraseFromParent();
878+
stack->eraseFromParent();
879+
borrowedOriginals.insert(orig);
880+
}
881+
716882
/* DEBUG
717883
llvm::errs() << "=== found lifetime ends for\n";
718884
closureOp->dump();
@@ -724,7 +890,11 @@ static SILValue tryRewriteToPartialApplyStack(
724890
*/
725891
SILBuilderWithScope builder(std::next(destroy->getIterator()));
726892
insertDestroyOfCapturedArguments(newPA, builder,
727-
[&](SILValue arg) -> bool { return true; },
893+
[&](SILValue arg) -> bool {
894+
// Don't need to destroy if we borrowed
895+
// in place .
896+
return !borrowedOriginals.count(arg);
897+
},
728898
newPA->getLoc());
729899
}
730900
/* DEBUG
@@ -750,7 +920,12 @@ static SILValue tryRewriteToPartialApplyStack(
750920
return closureOp;
751921

752922
insertDeallocOfCapturedArguments(
753-
newPA, dominanceAnalysis->get(closureUser->getFunction()));
923+
newPA, dominanceAnalysis->get(closureUser->getFunction()),
924+
[&](SILValue arg) -> bool {
925+
// Don't need to destroy if we borrowed
926+
// in place.
927+
return !borrowedOriginals.count(arg);
928+
});
754929

755930
return closureOp;
756931
}

lib/SILOptimizer/Mandatory/MoveOnlyAddressCheckerUtils.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3140,7 +3140,7 @@ bool MoveOnlyAddressCheckerPImpl::performSingleCheck(
31403140
}
31413141

31423142
// Then gather all uses of our address by walking from def->uses. We use this
3143-
// to categorize the uses of this address into their ownership behavior (e.x.:
3143+
// to categorize the uses of this address into their ownership behavior (e.g.:
31443144
// init, reinit, take, destroy, etc.).
31453145
GatherUsesVisitor visitor(*this, addressUseState, markedAddress,
31463146
diagnosticEmitter);

lib/SILOptimizer/Utils/InstOptUtils.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1495,7 +1495,9 @@ void swift::insertDestroyOfCapturedArguments(
14951495
}
14961496

14971497
void swift::insertDeallocOfCapturedArguments(PartialApplyInst *pai,
1498-
DominanceInfo *domInfo) {
1498+
DominanceInfo *domInfo,
1499+
llvm::function_ref<bool(SILValue)> shouldInsertDestroy)
1500+
{
14991501
assert(pai->isOnStack());
15001502

15011503
ApplySite site(pai);
@@ -1509,6 +1511,9 @@ void swift::insertDeallocOfCapturedArguments(PartialApplyInst *pai,
15091511
continue;
15101512

15111513
SILValue argValue = arg.get();
1514+
if (!shouldInsertDestroy(argValue)) {
1515+
continue;
1516+
}
15121517
if (auto moveWrapper =
15131518
dyn_cast<MoveOnlyWrapperToCopyableAddrInst>(argValue))
15141519
argValue = moveWrapper->getOperand();
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// RUN: %target-run-simple-swift
2+
// REQUIRES: executable_test
3+
4+
internal func _myPrecondition(
5+
_ body: @autoclosure () -> Bool
6+
) {
7+
guard body() else {
8+
fatalError("condition failed")
9+
}
10+
}
11+
12+
var deinitCounter = 0
13+
14+
struct MyCounter<T>: ~Copyable {
15+
let expectedCount = 1
16+
let box: T
17+
init(_ t: T) {
18+
self.box = t
19+
}
20+
deinit {
21+
print("hello")
22+
deinitCounter += 1
23+
_myPrecondition(deinitCounter == self.expectedCount)
24+
}
25+
}
26+
27+
func test() {
28+
_ = MyCounter(4343)
29+
}
30+
31+
// CHECK: hello
32+
test()
33+
// CHECK-NEXT: great success
34+
print("great success")
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// RUN: %target-swift-frontend -emit-sil %s | %FileCheck %s
2+
3+
// When ClosureLifetimeFixup promotes a closure to the stack, we should
4+
// eliminate the temporary copy of any borrowed move-only captures, so that
5+
// they get checked as borrows and not consumes.
6+
7+
struct E<T>: ~Copyable {
8+
var t: T
9+
}
10+
11+
var escaper: () -> () = {}
12+
13+
func nonescaping(_ x: () -> ()) { }
14+
func escaping(_ x: @escaping () -> ()) { escaper = x }
15+
16+
func borrow<T>(_: borrowing E<T>) {}
17+
18+
// CHECK-LABEL: sil {{.*}}16testNeverEscaped
19+
func testNeverEscaped<T>(_ e: borrowing E<T>) {
20+
// CHECK: partial_apply {{.*}}[on_stack] {{.*}}(%0) :
21+
nonescaping {
22+
borrow(e)
23+
}
24+
}
25+
26+
func nonescaping(_ x: () -> (), and y: () -> ()) {}
27+
28+
// CHECK-LABEL: sil {{.*}}21testNeverEscapedTwice
29+
func testNeverEscapedTwice<T>(_ e: borrowing E<T>) {
30+
// CHECK: partial_apply {{.*}}[on_stack] {{.*}}(%0) :
31+
// CHECK: partial_apply {{.*}}[on_stack] {{.*}}(%0) :
32+
nonescaping({ borrow(e) }, and: { borrow(e) })
33+
}
34+
35+
// CHECK-LABEL: sil {{.*}}16testEscapedLater
36+
func testEscapedLater<T>(_ e: consuming E<T>) {
37+
// CHECK: [[BOX:%.*]] = alloc_box
38+
// CHECK: [[CONTENTS:%.*]] = project_box [[BOX]]
39+
// CHECK: partial_apply {{.*}}[on_stack] {{.*}}([[CONTENTS]])
40+
nonescaping {
41+
borrow(e)
42+
}
43+
44+
escaping {
45+
borrow(e)
46+
}
47+
}
48+
49+
// CHECK-LABEL: sil {{.*}}17testEscapedLater2
50+
func testEscapedLater2<T>(_ e: consuming E<T>) {
51+
// CHECK: [[BOX:%.*]] = alloc_box
52+
// CHECK: [[CONTENTS:%.*]] = project_box [[BOX]]
53+
// CHECK: partial_apply {{.*}}[on_stack] {{.*}}([[CONTENTS]])
54+
let f = e
55+
56+
nonescaping {
57+
borrow(f)
58+
}
59+
60+
escaping {
61+
borrow(f)
62+
}
63+
}
64+

0 commit comments

Comments
 (0)