Repository navigation
Conversation
a9da7bc to
1888b40
Compare
With Swift 5.0 the new `_modify` accessor became available, which allows a more efficient access to underlying storage when an in place mutation is made (i.e. via `inout`, like `value += 1`), by `yield`ing a reference to the storage itself, instead of performing a copy (`get`) followed by a write (`set`). This is particularly useful in containers where memory operations can become expensive (like collections), but also in containers wrapping expensive operations (like locking/unlocking locks). Such is the case of `Atomic`, where besides optimizing memory access to the underlying storage, it can save a lock/unlock when the `.value` is modified in place (i.e. via `inout`). In this particular case, one can even claim that it’s more correct, because the whole mutation is truly made in a single atomic operation instead of two (as is already the case in the `modify(_:)` API).
1888b40 to
8e0d2f6
Compare
|
While I was making this change, I tried to do the same on I did however manage to implement Essentially, it would end up like this: public var value: Value {
get { return box.value }
set { modify { $0 = newValue } }
_modify {
box.lock.lock()
defer { box.lock.unlock() }
guard !box.isModifying else { fatalError("Nested modifications violate exclusivity of access.") }
box.isModifying = true
defer { box.isModifying = false }
defer { observer.send(value: box._value) }
yield &box._value
}
} |
|
I was trying to find info on the Would you mind posting a link to some official documentation about this accessor? |
|
Unfortunately, I think we're a bit out of luck in regards to official documentation, at least for now 😆 From what I've gathered, this is one of two new generalized accessors (the other one is I can however share some more useful links about this new
Hope this helps! 🍻 |
|
Thanks for the PR! I wasn't actually aware that this was a thing. I'm not a fan of using these not-really-public-API APIs. If |
|
I'm pretty certain that this will go forward, as it was merged into Some revision/renaming will most certainly happen before these are truly "official", but I want to believe the compiler will throw new custom errors and possibly contain fix-its guiding developers to the new accessor name(s) when that happens, because the usage of these new accessors is already spreading in the community. All things considered, I still think your point is fair enough, even though it's not the outcome I would've hoped for 😄. That being said, if you and the rest of RS's core team agree that it's too soon to add this, feel free to close the PR. Cheers! |
shouldn't the |
The Guess I got carried away copy-pasting |
|
Right, I haven't realized that it's inside the defer :/ Well that's a convoluted way to maintain instruction order, anyway. Also, had to double check that swift actually guarantees to execute Anyway, this |
|
Imagine this basic data race example: @Atomic var tweets: [Tweet] = [Tweet(id: "tweet1")]
// Thread 0
if let index = tweets.index(where: { $0.id == "tweet1" }) {
tweets.remove(at: index)
}
// Thread 1
tweets.append(Tweet(id: "tweet0"))While using the I won't dispute the utility value of it for simple use cases, e.g. a head-tail queue. But it is not as great to the magnitude that we should include it even before it being formalised as part of the language. |
|
Maybe I wasn't clear on the motivation behind this PR, but this change isn't meant to replace The example you show is clearly not solved by this change, nor is it the goal of this change to solve it. The critical section on Thread 0 being composed of 2 operations ( From the snippet you shared it seems (and please correct me if I'm wrong 😅) that you are trying to make a property wrapper that is backed by an As a consequence of being more specific, the utility is also more "limited", and most likely won't be used in most real use cases. However, there are some use cases where it can be used and in these cases it brings clear performance and most importantly correctness benefits when mutating the underlying storage. Performance by saving one unnecessary lock/unlock ( I understand your concerns about this still not being "final" API (as was already mentioned before in the thread), and I am conscious that it's "usefulness" or "magnitude" is not very significant for most cases. I opened this PR because it's a simple change that brings a bit of performance and correctness for some use cases, that's all. That being said, I'm not quite sure about the motivation behind your comment, in the sense that IMO nothing new was brought to the discussion. I will gladly close the PR if you want, or feel free to close it if you don't see any value at this point. 🍻 |
|
I guess the main point I am trying to get across is that:
I do reckon that there are plenty of use cases, and personally encountered plenty at work which I wish to have this. But as a public package, we've also been trying to avoid the use of compiler private features, given the stability and support are not guaranteed. For instance, the Apple-led, performance-critical SwiftNIO has made a decision not to do so on a similar ground. So in this regard, I do hesitate to accept this. |
|
I understand, @andersio. Thanks for your feedback. Do you think I should close this PR, or wait for these accessors to be formally added to Swift and then update it? |
|
Closing this PR for now. @p4checo thank you for the contribution. |
Motivation
With Swift 5.0 the new
_modifyaccessor became available, which allows a more efficient access to underlying storage when an in place mutation is made (i.e. viainout, likevalue += 1), byyieldinga reference to the storage itself, instead of performing a copy (
get) followed by a write (set).This is particularly useful in containers where memory operations can become expensive (like collections), but also in containers wrapping expensive operations (like locking/unlocking locks).
Such is the case of
Atomic, where besides optimizing memory access to the underlying storage, it can save a lock/unlock when the.valueis modified in place (i.e. viainout). In this particular case, one can even claim that it’s more correct, because the whole mutation is truly made in a single atomic operation instead of two (as is already the case in themodify(_:)API).Changes
_modifyaccessor toAtomic.valueChecklist