-
Notifications
You must be signed in to change notification settings - Fork 10.6k
Devirtualize calls to protocol composition type methods #28043
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
Changes from all commits
d9b95a0
bfb3dd7
287bbd4
f3ac3bd
f1c75f6
4b36f3f
f1c15d2
6c48a8d
01a452f
17894bb
febbb70
3ab158e
2913dbc
331a6af
d032de6
644689d
6fcf437
758f1f3
f4e3ca9
a52fe57
33987c5
0992e48
ae64719
e93919b
2ac715c
e38da06
809f569
ddeef64
ac4fa33
4d2c342
a0fb139
65e8d37
37c0eb2
727cd96
dbadd99
bdd9480
ea821b1
1347929
209bfd0
464b585
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
//===--- DevirtualizeProtocolComposition.swift -------------------------------------------===// | ||
// | ||
// This source file is part of the Swift.org open source project | ||
// | ||
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors | ||
// Licensed under Apache License v2.0 with Runtime Library Exception | ||
// | ||
// See https://swift.org/LICENSE.txt for license information | ||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import TestsUtils | ||
|
||
public let DevirtualizeProtocolComposition = [ | ||
BenchmarkInfo(name: "DevirtualizeProtocolComposition", runFunction: run_DevirtualizeProtocolComposition, tags: [.validation, .api]), | ||
] | ||
|
||
protocol Pingable { func ping() -> Int; func pong() -> Int} | ||
|
||
public class Game<T> { | ||
func length() -> Int { return 10 } | ||
} | ||
|
||
public class PingPong: Game<String> { } | ||
|
||
extension PingPong : Pingable { | ||
func ping() -> Int { return 1 } | ||
func pong() -> Int { return 2 } | ||
} | ||
|
||
func playGame<T>(_ x: Game<T> & Pingable) -> Int { | ||
var sum = 0 | ||
for _ in 0..<x.length() { | ||
sum += x.ping() + x.pong() | ||
} | ||
return sum | ||
} | ||
|
||
@inline(never) | ||
public func run_DevirtualizeProtocolComposition(N: Int) { | ||
for _ in 0..<N * 20_000 { | ||
blackHole(playGame(PingPong())) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -775,10 +775,10 @@ bool SILCombiner::canReplaceArg(FullApplySite Apply, | |
/// (@in or @owned convention). | ||
struct ConcreteArgumentCopy { | ||
SILValue origArg; | ||
CopyAddrInst *tempArgCopy; | ||
AllocStackInst *tempArg; | ||
|
||
ConcreteArgumentCopy(SILValue origArg, CopyAddrInst *tempArgCopy) | ||
: origArg(origArg), tempArgCopy(tempArgCopy) { | ||
ConcreteArgumentCopy(SILValue origArg, AllocStackInst *tempArg) | ||
: origArg(origArg), tempArg(tempArg) { | ||
assert(origArg->getType().isAddress()); | ||
} | ||
|
||
|
@@ -817,9 +817,18 @@ struct ConcreteArgumentCopy { | |
SILBuilderWithScope B(apply.getInstruction(), BuilderCtx); | ||
auto loc = apply.getLoc(); | ||
auto *ASI = B.createAllocStack(loc, CEI.ConcreteValue->getType()); | ||
auto *CAI = B.createCopyAddr(loc, CEI.ConcreteValue, ASI, IsNotTake, | ||
IsInitialization_t::IsInitialization); | ||
return ConcreteArgumentCopy(origArg, CAI); | ||
// If the type is an address, simple copy it. | ||
if (CEI.ConcreteValue->getType().isAddress()) { | ||
B.createCopyAddr(loc, CEI.ConcreteValue, ASI, IsNotTake, | ||
IsInitialization_t::IsInitialization); | ||
} else { | ||
// Otherwise, we probably got the value from the source of a store | ||
// instruction so, create a store into the temporary argument. | ||
B.createStrongRetain(loc, CEI.ConcreteValue, B.getDefaultAtomicity()); | ||
B.createStore(loc, CEI.ConcreteValue, ASI, | ||
StoreOwnershipQualifier::Unqualified); | ||
} | ||
return ConcreteArgumentCopy(origArg, ASI); | ||
} | ||
}; | ||
|
||
|
@@ -850,20 +859,19 @@ SILInstruction *SILCombiner::createApplyWithConcreteType( | |
FullApplySite Apply, | ||
const llvm::SmallDenseMap<unsigned, ConcreteOpenedExistentialInfo> &COAIs, | ||
SILBuilderContext &BuilderCtx) { | ||
|
||
// Ensure that the callee is polymorphic. | ||
assert(Apply.getOrigCalleeType()->isPolymorphic()); | ||
|
||
// Create the new set of arguments to apply including their substitutions. | ||
SubstitutionMap NewCallSubs = Apply.getSubstitutionMap(); | ||
SmallVector<SILValue, 8> NewArgs; | ||
bool UpdatedArgs = false; | ||
unsigned ArgIdx = 0; | ||
// Push the indirect result arguments. | ||
for (unsigned EndIdx = Apply.getSubstCalleeConv().getSILArgIndexOfFirstParam(); | ||
ArgIdx < EndIdx; ++ArgIdx) { | ||
NewArgs.push_back(Apply.getArgument(ArgIdx)); | ||
} | ||
|
||
// Transform the parameter arguments. | ||
SmallVector<ConcreteArgumentCopy, 4> concreteArgCopies; | ||
for (unsigned EndIdx = Apply.getNumArguments(); ArgIdx < EndIdx; ++ArgIdx) { | ||
|
@@ -882,16 +890,18 @@ SILInstruction *SILCombiner::createApplyWithConcreteType( | |
NewArgs.push_back(Apply.getArgument(ArgIdx)); | ||
continue; | ||
} | ||
UpdatedArgs = true; | ||
|
||
// Ensure that we have a concrete value to propagate. | ||
assert(CEI.ConcreteValue); | ||
|
||
auto argSub = | ||
ConcreteArgumentCopy::generate(CEI, Apply, ArgIdx, BuilderCtx); | ||
if (argSub) { | ||
concreteArgCopies.push_back(*argSub); | ||
NewArgs.push_back(argSub->tempArgCopy->getDest()); | ||
} else | ||
NewArgs.push_back(argSub->tempArg); | ||
} else { | ||
NewArgs.push_back(CEI.ConcreteValue); | ||
} | ||
|
||
// Form a new set of substitutions where the argument is | ||
// replaced with a concrete type. | ||
|
@@ -914,7 +924,33 @@ SILInstruction *SILCombiner::createApplyWithConcreteType( | |
}); | ||
} | ||
|
||
if (!UpdatedArgs) { | ||
// We need to make sure that we can a) update Apply to use the new args and b) | ||
// at least one argument has changed. If no arguments have changed, we need | ||
// to return nullptr. Otherwise, we will have an infinite loop. | ||
auto substTy = | ||
Apply.getCallee() | ||
->getType() | ||
.substGenericArgs(Apply.getModule(), NewCallSubs, | ||
Apply.getFunction()->getTypeExpansionContext()) | ||
.getAs<SILFunctionType>(); | ||
SILFunctionConventions conv(substTy, | ||
SILModuleConventions(Apply.getModule())); | ||
bool canUpdateArgs = true; | ||
bool madeUpdate = false; | ||
for (unsigned index = 0; index < conv.getNumSILArguments(); ++index) { | ||
// Make sure that *all* the arguments in both the new substitution function | ||
// and our vector of new arguments have the same type. | ||
canUpdateArgs &= | ||
conv.getSILArgumentType(index) == NewArgs[index]->getType(); | ||
// Make sure that we have changed at least one argument. | ||
madeUpdate |= | ||
NewArgs[index]->getType() != Apply.getArgument(index)->getType(); | ||
} | ||
|
||
// If we can't update the args (because of a type mismatch) or the args don't | ||
// change, bail out by removing the instructions we've added and returning | ||
// nullptr. | ||
if (!canUpdateArgs || !madeUpdate) { | ||
// Remove any new instructions created while attempting to optimize this | ||
// apply. Since the apply was never rewritten, if they aren't removed here, | ||
// they will be removed later as dead when visited by SILCombine, causing | ||
|
@@ -961,8 +997,7 @@ SILInstruction *SILCombiner::createApplyWithConcreteType( | |
auto cleanupLoc = RegularLocation::getAutoGeneratedLocation(); | ||
for (ConcreteArgumentCopy &argCopy : llvm::reverse(concreteArgCopies)) { | ||
cleanupBuilder.createDestroyAddr(cleanupLoc, argCopy.origArg); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this will destroy the value that you stored, but you never retained that value. In non-OSSA, all the ownership operations need to be explicit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, something around here needs to be updated. See my below comments. I'm curious how this worked with before this patch. Wouldn't the same issue occur for |
||
cleanupBuilder.createDeallocStack(cleanupLoc, | ||
argCopy.tempArgCopy->getDest()); | ||
cleanupBuilder.createDeallocStack(cleanupLoc, argCopy.tempArg); | ||
} | ||
} | ||
return NewApply.getInstruction(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -69,9 +69,9 @@ findInitExistentialFromGlobalAddr(GlobalAddrInst *GAI, SILInstruction *Insn) { | |
|
||
/// Returns the instruction that initializes the given stack address. This is | ||
/// currently either a init_existential_addr, unconditional_checked_cast_addr, | ||
/// or copy_addr (if the instruction initializing the source of the copy cannot | ||
/// be determined). Returns nullptr if the initializer does not dominate the | ||
/// alloc_stack user \p ASIUser. If the value is copied from another stack | ||
/// store, or copy_addr (if the instruction initializing the source of the copy | ||
/// cannot be determined). Returns nullptr if the initializer does not dominate | ||
/// the alloc_stack user \p ASIUser. If the value is copied from another stack | ||
/// location, \p isCopied is set to true. | ||
/// | ||
/// allocStackAddr may either itself be an AllocStackInst or an | ||
|
@@ -111,6 +111,19 @@ static SILInstruction *getStackInitInst(SILValue allocStackAddr, | |
} | ||
continue; | ||
} | ||
zoecarver marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (auto *store = dyn_cast<StoreInst>(User)) { | ||
if (store->getDest() == allocStackAddr) { | ||
if (SingleWrite) | ||
return nullptr; | ||
SingleWrite = store; | ||
// When we support OSSA here, we need to insert a new copy of the value | ||
// before `store` (and make sure that the copy is destroyed when | ||
// replacing the apply operand). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My previous comment might have been misleading because I didn't realize we're inserting a new destroy. In OSSA, the store will do an implicit copy. But, currently, you need to copy the stored value, probably using a retain_value before the store. To keep the ownership clean what probably ends up happening is this I think: We have an original existential argument value and a new concrete argument value. The original existential argument value is removed from a "consuming" call operand, and a new destroy_addr of that value is generated in its place (right after the call). The copy of the new concrete argument value is emitted. That copy is then consumed by the call argument that it is assigned to. That will be fairly straightforward to update for OSSA later. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's a question I've been wanting to ask for a while: what's the relationship between OSSA and reference counting? Is it handled differently in OSSA? Thanks for laying it all out. That mostly makes sense except for the part about adding a new Again, thank you for all the help and information you've given me in this patch. I'm very grateful. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OSSA is a more strict form of SIL. That's all. It means that each SSA value has its own ownership. So you can't arbitrarily retain/release values and hope they all balance out. A value can only be consumed once (by a destroy_value or owned argument) and cannot be used after it is consumed. It looks to me like the new destroy at the call site is only created when a There's another issue I was worried about, which is a totally separate issue from ConcreteArgumentCopy: getStackInitInst will discover the source of the store instruction. We use that as the new ConcreteValue. But, we don't set
Optimizes to:
That's kind of an abuse of SIL, but it's fine for non-OSSA, and in
But with OSSA it will be:
Optimizes to:
I think this PR is fine as-is since you've added a comment for how to handle OSSA later. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting. Really good to know, thank you for taking the time to explain. I'm happy to work on updating more parts of the codebase to use OSSA if you or someone else can help me get started (the knowledge transfer may be more effort than it's worth, though). |
||
assert(store->getOwnershipQualifier() == | ||
StoreOwnershipQualifier::Unqualified); | ||
} | ||
continue; | ||
} | ||
if (isa<InitExistentialAddrInst>(User)) { | ||
if (SingleWrite) | ||
return nullptr; | ||
|
@@ -144,6 +157,9 @@ static SILInstruction *getStackInitInst(SILValue allocStackAddr, | |
if (BB != allocStackAddr->getParentBlock() && BB != ASIUser->getParent()) | ||
return nullptr; | ||
|
||
if (auto *store = dyn_cast<StoreInst>(SingleWrite)) | ||
return store; | ||
|
||
if (auto *IE = dyn_cast<InitExistentialAddrInst>(SingleWrite)) | ||
return IE; | ||
|
||
|
@@ -180,24 +196,6 @@ static SILInstruction *getStackInitInst(SILValue allocStackAddr, | |
return CAI; | ||
} | ||
|
||
/// Return the address of the value used to initialize the given stack location. | ||
/// If the value originates from init_existential_addr, then it will be a | ||
/// different type than \p allocStackAddr. | ||
static SILValue getAddressOfStackInit(SILValue allocStackAddr, | ||
SILInstruction *ASIUser, bool &isCopied) { | ||
SILInstruction *initI = getStackInitInst(allocStackAddr, ASIUser, isCopied); | ||
if (!initI) | ||
return SILValue(); | ||
|
||
if (auto *IEA = dyn_cast<InitExistentialAddrInst>(initI)) | ||
return IEA; | ||
|
||
if (auto *CAI = dyn_cast<CopyAddrInst>(initI)) | ||
return CAI->getSrc(); | ||
|
||
return SILValue(); | ||
} | ||
|
||
/// Check if the given operand originates from a recognized OpenArchetype | ||
/// instruction. If so, return the Opened, otherwise return nullptr. | ||
OpenedArchetypeInfo::OpenedArchetypeInfo(Operand &use) { | ||
|
@@ -207,11 +205,17 @@ OpenedArchetypeInfo::OpenedArchetypeInfo(Operand &use) { | |
// Handle: | ||
// %opened = open_existential_addr | ||
// %instance = alloc $opened | ||
// copy_addr %opened to %stack | ||
// <copy|store> %opened to %stack | ||
// <opened_use> %instance | ||
if (auto stackInitVal = | ||
getAddressOfStackInit(instance, user, isOpenedValueCopied)) { | ||
openedVal = stackInitVal; | ||
if (auto *initI = getStackInitInst(instance, user, isOpenedValueCopied)) { | ||
// init_existential_addr isn't handled here because it isn't considered an | ||
// "opened" archtype. init_existential_addr should be handled by | ||
// ConcreteExistentialInfo. | ||
|
||
if (auto *CAI = dyn_cast<CopyAddrInst>(initI)) | ||
openedVal = CAI->getSrc(); | ||
if (auto *store = dyn_cast<StoreInst>(initI)) | ||
openedVal = store->getSrc(); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The existential value stored on the stack here might not be the same value on the stack when it is opened. Also, the value stored on the stack might no longer be live at the apply site. There's a whole bunch of complexity in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I think I understand your first comment but, could you elaborate on the following?
I've updated line 222 to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the abstract, we want to avoid producing a sequence like
That shouldn't actually be a problem though because we're not in OSSA form so there will only be 'retain/release_value(r)' operations. As long as none of the retains get eliminated during this transformation, it should be safe. A lot of the complexity in the existing code comes from the fact that it's handling 'open_existential_addr', which can be destroyed with 'destroy_addr'. The main thing here is to prove that the stored value is the value used by the apply. One way to do that is to recognize all uses of the alloc_stack and make sure only one of them is a store. getStackInitInst already does that, but only handles copy_addr, not store. Please try to write some .sil test cases to for corner cases. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you, that was extremely helpful.
The constructor I modified does handle
I've added more checking around the above code but, I suspect it isn't quite right yet. Should the stack alloc instruction ever be used in more than the initial store, the apply, and then the dealloc instructions? If not it may make it a bit easier to filter out what we can optimize. But that might be too harsh of a filter. Anyway, I've added many more tests and have uncovered several bugs. Right now I'm in the process of testing those bugs against master to see if they're my issue and if so, fixing them. I'm going to be pretty busy this week with other things so, turn around on this patch might be a bit slow. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @atrick I've added a dominance order check which should ensure that there aren't those kinds of issues. I've also added one .sil test. I plan on writing some more, what other edge cases should I cover? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Developing a breadth of .sil test cases is extremely helpful. That's what this code was missing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you want to generalize this pattern match, I think it should be part of the existing utilities so there's no new code in this function. FYI: in non-OSSA it should just work. If we have
It's actually ok in non-OSSA to reuse the original value like this:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea-- that's much cleaner. Also, thanks for the information about OSSA/non-OSSA and when copies are needed. Very interesting. |
||
} | ||
if (auto *Open = dyn_cast<OpenExistentialAddrInst>(openedVal)) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally tried to early-exit here if we didn't have an address but, it turned out to be easier just to generalize this a bit to work with stores as well.