When looking at the retains and releases in my code, I've noticed one particular pattern coming up fairly frequently. Consider the following:
func test(_ array: Array<Int>) {
someFunction(array.someSlice)
}
func someFunction(_ c: ArraySlice<Int>) {
blackHole(c)
}
extension Collection {
@inline(never) // Simulate a function the compiler decides not to inline.
var someSlice: SubSequence {
self[startIndex..<index(startIndex, offsetBy: 5)]
}
}
@_silgen_name("blackHole")
func blackHole<T>(_: T)
What I'm seeing is that the compiler creates the slice, retains it, calls blackHole
, and releases the slice afterwards (this happens both in a standard -O
build, and with OSSA modules enabled). The retain/release disappears if I use the entire array directly, or if the compiler happens to inline the someSlice
getter. (Godbolt).
I'm not entirely sure why these refcounting operations are even happening - as I understand it, both someFunction
and blackHole
should accept their arguments at +0. test
already has a +0 value provided to it, so the lifetime math trivially checks out.
I'm also not sure how to resolve it. I've tried adding a _read
accessor to someSlice
, making it a __consuming func
, and marking parameters as __shared
(i.e. nonconsuming
), but none of those things helped. Is the idea that the someSlice
property should return a ref SubSequence
? And if so, wouldn't that prohibit anybody from ever extending the slice's lifetime?
I don't want that - it's not what this code is supposed to express. Within test
, the slice's lifetime is guaranteed by the array's lifetime, so when calling someFunction
/blackHole
, I don't want any retains or releases added since the lifetime is not extended. At the same time, the slice doesn't point to a stack buffer or whatever, so any callees are free to escape the value if they so choose (provided they retain it first).
The only thing I can think of is that the retains are caused by the ArraySlice initializer, which of course needs to store its base collection. But slices in Swift are just facades - they adjust the startIndex and endIndex properties used for the Collection conformance, but otherwise just forward everything to the base collection. If the slice is causing the extra ARC traffic and there's no way in sight to remove that, it means that structs in Swift are very far from "zero-cost" abstractions since they do not carry lifetime information.