I am currently working on a performance-sensitive project (a high-speed data scrubber on macOS/iOS) where I use a custom ~Copyable struct to measure the time spent in different functions. My goal is to use the RAII pattern: I initialize the probe at the start of a scope, and I want the deinit (which logs the duration) to fire exactly when the scope ends.
However, I’ve noticed that the Swift compiler is very efficient and destroys my probe as soon as it's no longer "used" in the code, which is often way before the end of the function. This makes my performance logs inaccurate as they only measure the initialization time instead of the full scope duration.
To illustrate, here is what I’m trying to achieve:
swift
// 1. What I wish worked (clean RAII)
func performSearch() async {
let _ = ScopeProbe("Search Operation") // deinit fires too early here
await someLongRunningTask()
// I want deinit to fire here, but it fires right after the init
}
// 2. What I have to do now to get accurate results
func performSearch() async {
let probe = ScopeProbe("Search Operation")
// I tried this, but it often fails with:
// "'probe' is borrowed and cannot be consumed"
// defer { _ = consume probe }
// So I have to use this instead:
defer { _fixLifetime(probe) }
await someLongRunningTask()
}
The first version is much cleaner and less prone to errors, especially in complex functions with multiple return points or throws. However, even with a named variable (e.g., let probe = ...), if the variable isn't accessed later, the Lexical Lifetime optimization seems to reclaim it eagerly.
In a synchronous world, I know I could use withExtendedLifetime, but my functions are async and I would like to avoid wrapping everything in closures which adds indentation and complexity.
Is there a way to tell the compiler: "This specific variable defines a scope and should live until the end of the braces", without having to manually manage its consumption? I'm curious to know if I'm missing an idiomatic Swift 6 pattern or if this is a known trade-off of the current ownership model.
As far as I have observed the discussion around ~Copyable, this is explicitly not something that Swift offers. (More flexibility to tweak the compiler/optimizer.) You should use a closure or defer for such use cases as there are no guarantees for the duration of implicit lifetimes.
i believe when it comes to ~Copyable structs at least this is incorrect. non-copyable structs have 'lexical lifetimes' which are well defined. this thread may be of interest, and i'm basing this assertion primarily on what Andy said here:
i believe this is an implementation artifact of the particular pattern you're using where the probe is never given a name. if you name the variable you should see the behavior you expect where its deinit runs at the end of scope:
func atExit() {
let p = ScopeProbe("...")
// do stuff ...
// deinit runs here
}
awkwardly, if you never use the variable, this will generate an unused variable warning, so if you want to eliminate that you'll also need a use.
IIUC this would be a bug, since the purpose of lexical lifetimes is to anchor lifetimes to the corresponding lexical scope. do you have an example that reproduces this behavior?
in terms of more ergonomic options – the class-based approach with defer is decent (though perhaps less performant). you may want to look into a macro (and maybe a function body macro).
interesting, thank you for pointing that out. well that certainly adds a wrinkle into my mental model for the meaning of 'lexical lifetime'... i guess i can see the problem that Michael mentioned in that thread – if you promise that the lifetime ends at the end of the variable's lexical scope, but there are conditional execution paths in which its lifetime may end sooner (consumed somewhere), then you'd need to insert runtime book-keeping to avoid destroying it multiple times.
however i think it's still true that the language at least aspires to promise that the non-copyable struct lifetimes are well defined, though what those lifetimes actually are is not as straightforward as i thought.
Thank you all for participating in this topic. I’ve been AFK this weekend.
@jamieQ you are right, naming the probe makes it live until the end of the scope. But as you noted, you must do something with it; otherwise, a warning is issued. And I don’t like warnings…
@ole thanks for the tip on withExtendedLifetime(). For lack of a better alternative, that is what I will do right now within a defer statement.
I wish that let _ wouldn't be optimized away by the compiler for ~Copyable objects.