Stack Allocation for Non-Escaping Swift Closures
aschwaighofer has a pull request for stack-allocating Swift closures.
Short history of non-escaping functions:
- Swift 4.1 and earlier: type checker enforcement; same ABI as escaping
- Swift 4.2: new ABI - the context is a trivial pointer and not ref-counted like with escaping
- now: non-escaping contexts allocated on stackThe ABI change was key here - Arnold frontloaded the changes before we started locking down, now stack-allocation is “just” an optimization
And ancient pre-history for those who weren’t around at the time:
- Swift 2.2 and earlier: all function values escaping by default, opt-in @noescape attribute for parameter types
- Swift 3: @noescape becomes default for function parameters, @escaping added to opt-inMore trivia: In ancient Swift the accepted idiom to turn a non-escaping function into an escaping one was unfortunately an unsafeBitCast(). The compiler added a special withoutActuallyEscaping form and started screaming about casts in 4.0 so that we could stage in the ABI change
Previously: Optional Non-Escaping Swift Closures.
Update (2019-01-23): Matt Gallagher:
I played around with Swift master’s new stack allocated closure contexts today. My “capturing closure” mutex test case from this article improved 10x from 2.051 seconds to 0.212 seconds. Putting it within 20% of the inlined version.
Unfortunately, I needed to disable runtime exclusivity checking to get this performance. With exclusivity on, performance was 0.384 seconds (nearly 100% slower). Seems like this code should be statically checkable for exclusivity. Hope this improves.
Another unfortunate point: DispatchQueue.sync’s closure still isn’t optimized to the stack. I think this is a consequence of the stdlib’s interface around
dispatch_queue_sync
. I hope it gets resolved soon. I’d rather just use DispatchQueue.sync and not worry about performance.
Stack allocation doesn’t work yet for Objective-C blocks. I suspect that also applies to wrappers like DispatchQueue.sync.