-
Notifications
You must be signed in to change notification settings - Fork 424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
compareExchange interface does not match C/C++ #13836
Comments
I don't have a strong preference between the tuple return and ref versions, but I think I slightly prefer the ref version. I think we should, at the end of the day, have 2 variants of compareExchange:
All this taken together leads to:
|
Based on exchanges in #10001, @LouisJenkinsCS is likely to have an opinion here. |
I'm in agreement with @mppf that there should be another variant, but I think the original should be unchanged and that instead |
I'm catching up on this issue... It feels unfortunate to me to live with a difference like this from C/C++ for all time given how similar our interface is otherwise, making me want to make the two match now before more time goes on. I also think there's a potential correctness/confusion issue if we leave it as-is which I'll get to below. @mppf: Do you recall the historical reason for why our compareExchange()'s interface differs from C++'s in the first place? (it looks like it's had this signature since you contributed the code). Best guess number 1: Was this before we had So thinking about what the new interface would look like: proc compareExchangeStrong(ref expected: T, desired: T): bool It seems like the two ways that this change could break existing code would be (a) a user didn't pass an l-value into the The first is a breaking change that would be easy to deal with (though still annoying) because your code wouldn't compile until you fixed it, so the lack of a deprecation warning doesn't seem like a big deal... you wouldn't be surprised once you understood what had changed and what you had to do to address it. The main downside of this is that it feels like it would be natural to say something like "I expect the value to be 0" where now, you have to store that 0 into a variable or call a different routine (I never thought I'd say it, but this makes me wish we had overloading based on argument intents, possibly for the first time). The second is more subtle, assuming that people did pass in l-values... but would they really write code that relied upon the value being the same after the call? My bigger fear here points to a really important reason to unify with C++ for me: If you were coming from C++ assuming we behaved the same, and were checking to see whether or not the CAS succeeded by looking at whether or not Also interesting is that looking at the Wikipedia entry for compareAndSwap(), its behavior matches our current behavior, suggesting that introducing a second routine named compareAndSwap() which had the current behavior would make sense (though I suspect we could find additional examples for either name with different interfaces without searching very hard...). So I think I'm leaning heavily towards unifying with C++ and making the current behavior available as
Option 3 is obviously the safest, but also could be annoying if what you really wanted was the C++ routine (since you'd change your code now to avoid the warning then change it back to get the C++ behavior in the next release...). So it's also the slowest. Although I suppose we could have a config param to disable the warning so that you could stay in the compareExchange() world if that's where you wanted to end up... Option 2 feels safe, but noisy and would rely on code that uses the routine to throw the config param for all compiles until the next release.
I think [edit: I've scratched out my previous open-ness to option 1 based on Louis's example in the next comment] |
I can say that I have definitely written code where I assume that the 'expected' operand has not changed. Since you asked for a practical one, I can show you at least one which the C++'s tendency to update the 'expected' operand has bit me before, and that's the K42 MCS Spinlock. In particular, this part...
proc acquire (list : List)
var node = new ListNode();
node.next = nil
predecessor = list.tail.exchange(node);
if predecessor != nil { // queue was non-empty
node.locked.write(true);
predecessor.next = node;
node.waitFor(false); // wait for lock
// I now have the lock
successor = node.next;
if successor = nil {
// ??? Need to think about how L->next translates in Chapel...
if list.tail.compareExchange(node, ...) {
// somebody got into the timing window
do {
successor = node.next;
} while successor = nil; // wait for successor
// ???
else
// ??? The important part here is the fact that I know for a fact that I have done something similar in the past, but worst part is I don't know where so a change like this would be like a ticking time bomb (okay, that is a bit of hyperbole, but still it'd be an unexpected problem). Again, I prefer Also as to how often I pass in r-values... very often, |
Thanks for the detailed follow-up. I think your example (passing an existing field in as the expected value and not wanting it to change) has talked me out of option 1 as being too dangerous and for behavior (b) being more likely than I had hoped. I'm still not convinced that leaving compareExchange() as-is is as safe as you claim, though. Safe for existing Chapel programs sure, but not necessarily safe for future ones and users. For someone coming to Chapel from C++ who assumes the interface is the same and ignores the return value. I.e., if I wrote the following code I could incorrectly conclude that the action had succeeded when it had not, right? var x: atomic int = ...;
var value1 = 42;
var value2 = 33;
x.compareExchangeStrong(value1, value2);
if (value1 == 42) then
// Yay! compareExchange succeeded! Now sure, this is my fault as a user for not reading the docs more carefully / checking the return value, but given that it appears that we modeled our routines after C/C++ (and we did), it would be reasonable to assume that they were all the same once I'd verified that a few routines matched as expected and adjusted to the camelCase naming. I also hear your preference for Chapel's current behavior for compareExchange() over the proposed / C++ approach, but that seems more like an argument with C++'s choice than with Chapel's (assuming I'm correct that we only diverged because we didn't yet have the ability to express a |
I'll just say that I'm sticking with my original stated preferences, but I'll also say that I suppose it wouldn't be too much effort to find-and-replace |
I think I hadn't picked up on the fact that the C/C++ version was modifying one of the arguments. I don't know if it was already standardized that way, but I'm guessing that it probably was and it was an oversight on my part. |
I'd also like to propose Option 2.5: Start with option 3:
But mark the That way:
Option 3 by itself would also not bother me much, since one might argue this is a library stability vs language stability issue. |
I'd be fine with either option 2.5 or 3 To me, compareAndSwap and compareExchange are interchangeable terms. I think I'd always have to look up or compile an example to remember which one does what. (CAS is the more natural textbook term to me, but CMPXCHG is the intel instruction.) (I don't have any better naming suggestions, just noting that the names don't give me any indication of which one updates |
I agree that the names don't distinguish from one another very well, but I also think that either name in isolation doesn't make it all that clear what it does with that first argument, so maybe it doesn't matter all that much and it's just a learning curve (or chance to read the documentation or see which one complains about passing in a literal). If I wanted to work harder at distinguishing them, I might call the current behavior We could make both options use new names like |
I'm currently leaning towards option 3. My preference is to deprecate Option 2.5 is also reasonable, but I'm not sure it adds a ton of value and users would have to modify their code next release to remove the Once everything settles for either proposal 2.5 or 3, I think we'd have: proc compareAndSwap(expected: T, desired: T, param order:memoryOrder = memoryOrder.seqCst): bool
/* Matches C/C++ compare_exchange_strong */
proc compareExchange(ref expected: T, desired: T, param order:memoryOrder = memoryOrder.seqCst): bool
proc compareExchange(ref expected: T, desired: T, param success:memoryOrder, param failure:memoryOrder): bool
/* Matches C/C++ compare_exchange_weak */
proc compareExchangeWeak(ref expected: T, desired: T, param order:memoryOrder = memoryOrder.seqCst): bool
proc compareExchangeWeak(ref expected: T, desired: T, param success:memoryOrder, param failure:memoryOrder): bool (note that I'm proposing we don't have "strong" in the name for |
And on the off chance he's still following us, I'd love to get @dmk42's input here now that the proposal is more fleshed out. |
I thought option 3 meant no switch for this release but deprecating for now. |
Hi. I still see these discussions when I'm mentioned. I'm less concerned about the mechanism of migration than with the fact that the migration eventually happens. The machine instructions do change the expected value, so passing that behavior through to the user will save clock cycles by not having to reload the new expected value again in user code. I am happy with the direction in which this conversation is converging. |
I think that matches what I said. When I said |
Here's my current branch that would follow option 3 (deprecate |
@LouisJenkinsCS I made the minimum required changes to |
That's not a bad idea, I like the idea of making |
Do you want to introduce functions with the new behavior this release, so people can start developing against them, with some namespace work to allow opting in? I can imagine either giving them new names ( use Atomics_future only compareExchange; Then in that future release, they can just drop those |
That's more in line with Michael's option 2.5 (#13836 (comment)). My current plan is to go with option 3 though (deprecate this release, change next release.) Adding the prototype functionality now might be nice, but there is a lot of implementation work for the ~1 day left before feature freeze. Delaying also gives us time to make sure we're happy with what versions of the |
Ah, sorry. With only 1 day left before feature freeze, I didn't have time to read all the comments. :-P |
I like the 2.5 concept and the potential value of letting people work with the new interface, but then it occurred to me that nobody has ever asked us for the by-ref interface (that I can recall), so making them wait another release isn't likely to make them unhappy (i.e., if they've been happy with the current interface, they will probably continue to be happy with the compareAndSwap()). Then I didn't feel as bad about going with 3. (Elliot also reminded me of the number of runtime implementations of the atomics interface that would need to be fleshed out and tested). |
Deprecate compareExchange in favor of compareAndSwap [reviewed by @mppf] This deprecates `compareExchangeWeak()`, `compareExchangeStrong()`, and `compareExchange()` in favor of `compareAndSwap()`. This is being done so that next release we can repurpose the `compareExchange*()` interface to match the C/C++ interface like the rest of our atomic API. Part of #13836
Implement atomic compareExchange that matches C/C++ [reviewed by @gbtitus, @dmk42, and @benharsh] Implement `compareExchange`, which is like `compareAndSwap`, but `expected` is updated on failure. There is also a `compareExchangeWeak` variant that is allowed to spuriously fail, but can offer better performance on some architectures, and is recommended when you'd already use compareExchange in a loop. The new compareExchange version takes `expected` by ref and updates it on failure. This matches the C/C++ API. The implementation is pretty straightforward. One tricky aspect is that we have to ensure we localize `expected` when performing operations. For processor atomics, it needs to be localized on the target locale, and for network atomics it needs to be localized on the initiating locale. The runtime implementations for `cstdlib` and `locks` are pretty simple. The `intrinsics` version is more complicated since we have to implement compareExchange with a compareAndSwap primitive that only returns the old value. This implementation for this was inspired by: https://herbsutter.com/2014/02/19/reader-qa-is-stdatomic_compare_exchange_-implementable/ The ugni and ofi "NIC" network atomics implementations also only have a compareAndSwap that returns the old value. This updates our processor atomic fallbacks to have similar behavior, and at the runtime call site we take care of updating `expected` and setting success/failure. Closes Cray/chapel-private#727 Part of #13836
We now have proc compareAndSwap(expected: T, desired: T, param order:memoryOrder = memoryOrder.seqCst): bool
/* Matches C/C++ compare_exchange_strong */
proc compareExchange(ref expected: T, desired: T, param order:memoryOrder = memoryOrder.seqCst): bool
proc compareExchange(ref expected: T, desired: T, param success:memoryOrder, param failure:memoryOrder): bool
/* Matches C/C++ compare_exchange_weak */
proc compareExchangeWeak(ref expected: T, desired: T, param order:memoryOrder = memoryOrder.seqCst): bool
proc compareExchangeWeak(ref expected: T, desired: T, param success:memoryOrder, param failure:memoryOrder): bool |
Unlike the rest of our atomic API, the interface for compareExchange does not match C/C++. Ignoring memory orders, the Chapel interface is:
and C/C++ is:
Where the difference is that C/C++ take
expected
by ref and on failure the expected value is updated to be the value of the atomic. Should we update our implementation to match? Note that if we do I don't think it's possible to deprecate the old style and implement the new one, I think this would be a breaking change. (Assuming we don't change names, which would also be unfortunate.)The advantage of updating
expected
by ref is that the NIC/processor already has to load the old value to do the comparison so ifexpected
is passed by ref it can get updated for free. If it's not passed by ref the user needs to do another atomicread()
, which is not free. The downside to passingexpected
by ref is that it's a breaking change (and the behavior may be a little subtle, but it matches what C/C++ does)The code below compares what a spinlock and an atomic-fetch-multiply look like today vs. with
expected
passed by ref and updatedSpinlock:
This is a pretty weak example since you'd use testAndSet/clear in reality but it does showcase that when you don't want to update the
expected
argument, you have to add more code to clear the update (however clearing it is cheap computationally)Atomic multiply:
This is also a little weak, but it is much more compelling if you think about classes being atomically exchanged for lock-free data structures (it's just that we don't currently support atomic ops on classes):
I do not have any strong preferences here. I like matching the C/C++ interface like we do for everything else, but it took me a while to get used
expected
getting updated, and this is an incompatible breaking change.Here's some related information about what Rust did. Rust used to have a compare_and_swap, but they added a compare_exchange that is more similar to the C/C++ variants (and I believe they intended to deprecate compare_and_swap, but they never did that). However, instead of taking
expected
by ref, Rust returns aResult indicating whether the new value was written and containing the previous value
(for us I guess this would be a tuple that contains(performedExchange, origValue)
?)Options that I can think of in no particular order (note that I don't like many of these, and there are likely other options I'm not thinking of):
expected
by ref and update on failure (breaking change)compareExchangeUpdate()
?)compareExchange(expected, ...) ...
compareExchange(ref expected, ..., param updateExpected)...
(success, oldVal)
The text was updated successfully, but these errors were encountered: