First, we do need some clear motivating examples to discuss. Some test cases are here:
But it isn't clear which ones are solved by the current PR:
https://github.com/apple/swift/pull/32844
And it isn't clear which optimization that PR is improving.
Ideally, I think AccessEnforcementSelection would completely eliminate dynamic checks for locally allocated references based on its own analysis without needing your flag. But it currently doesn't work on references at all, and still only runs very early in the pipeline. In the meantime, some of the other AccessEnforcementOpts will reduce the number of checks with your flag, but probably won't eliminate them completely.
I suspect there are some decent motivating examples, so assuming you can uncover those, I'll proceed to give a lengthy argument for adding the flag...
The alloc_ref [uniq] flag says that a reference is uniquely identified, which primarily implies lack of aliasing. It also happens to imply (CoW) uniqueness at all use points, no additional analysis needed, but that isn't the primary intention.
Aliasing and escaping are different things. Trivially:
@inline(never)
func bar(_ o: MyClass) -> MyClass {
return o
}
public func foo() {
let o1 = MyClass()
o1.x = 1
let o2 = bar(o1)
o2.x = 2
}
o1 is a local nonescaping reference, but we cannot treat it as a distinct object from o2.
The immediate goal of the flag is to recognize the base of a memory access as fundamentally "uniquely identified". See AccessedStorage::isUniquelyIdentified. This is important because it allows a storage location to be uniqued by a hash key. This does not preclude any further alias analysis, but it is useful for some well-defined subset of local allocations, and will likely also be helpful for -Onone diagnostics.
Adding an instruction flag goes against my intuition. I almost never want to use the IR as an analysis cache. But let's seriously evaluate this case.
SIL should primarily function as a high-level IR, meaning that important Swift-specific properties should be easily discoverable from the SIL structure whenever possible. If it's easy to discover that some well-defined set of local references are "uniquely identified", then that should be a property of the SIL. It should not require interprocedural global dataflow, as would be expected with a mid-level IR. There is no question that we still need additional analyses to propagate reference uniqueness, but giving alloc_ref a property that supersedes the analysis nicely short-circuits it.
I expect checking if a pointer source is uniquely identified to be increasingly pervasive throughout the compiler. It is checked within utilities that don't have access to analyses. And, whenever this property is true, EscapeAnalysis queries will be cheaper. It's also important that we limit the interprocedural AliasAnalysis and EscapeAnalysis to a few key points in the pipeline.
In other words, just because StackPromotion was able to optimize something doesn't make it a "property of SIL".
Structural SIL properties should be computed on-the-fly if possible as long as that doesn't increase algorithmic complexity. For example, if discovering a property only requires walking the use-def chain without handling phis, then that should always be done on-the-fly. The proposed "unique" property requires analyzing all uses if the alloc_ref. That certainly could be done on the fly, but I have to admit, caching this in a flag will be cheaper, and I do want identifying AccessStorage to be as cheap as possible--it's already used within quadratic algorithms.
A viable alternative would be to have every pass that uses AccessedStorage or that otherwise checks if a reference is "uniquely identified" separately compute and cache this property. That's a fair amount of design complexity and realistically we'll end up missing out on it for a some passes.
To meet the bar for adding an instruction flag, we need it to be high-value, low-risk, low-maintenance.
The flag is high value. Identifying unique storage locations enables powerful optimization. I'm expecting @zoecarver to show examples where dynamic exclusivity checks to "automatically" disappear for local objects. The only uncertainty I have is how common it really is to have locally allocated references that are never passed as arguments or captured. Note, though that capturing an inout property of the object does not count as capturing the reference.
The flag is low-risk. Once set, nothing should invalidate it, and it is easily checked in the SIL verifier with the same logic that sets the flag.
QUESTION: Can anyone think of a SIL transformation that would violate the uniqueness of a local reference?
The flag is low-maintenance. The only cost of this proposal is the implementation of the flag itself: [SIL] Add a "unique" flag to alloc_ref instructions. by zoecarver · Pull Request #32844 · apple/swift · GitHub
It it easily verified with the same logic that originally determines uniqueness.
I'm open to other alternatives, but this flag is potentially a valuable addition to the current SIL infrastructure. Worst case, we get some nice improvements for a while and decide to remove the flag later when every optimization that matters has its own more powerful "unique reference analysis".
This is somewhat moot, but for those who are interested, the EscapeAnalysis public API (canEscapeTo) does not currently determine local reference uniqueness in the situation we want:
class MyClass {
var x = 0
}
@inline(never)
func bar(_ i: inout Int) {
i = 2
}
public func foo() {
let c1 = MyClass()
let c2 = MyClass()
bar(&c1.x)
c2.x = 3
}
Here, @zoecarver 's logic easily identifies the locally allocated references as unique. But according to our current AliasAnalysis, which uses EscapeAnalysis, the accesses to c1.x and c2.x appear to alias and the release of c1 and c2 both appear to escape both c1.x and c2.x.
Under the hood, there is enough information in EscapeAnalysis connection graph to determine that only the property escapes, not the object itself. But the public API is not designed to make use of that information.
StackPromotion does work in these cases because it actually peeks under the hood to look at the EscapeAnalysis connection graph (the pass and analysis are tightly coupled).