I don't understand how do you think it could change or break a volatile or lock-free algorithm?
It's a transparent optimisation - it doesn't change observable behaviour. Just like how register renaming, top-of-stack-caching, and so on and so on don't change observable behaviour.
The only difference it would make is timing.
I can't write such a program. Can you?
How would you ensure that another thread was scheduled between these instructions in order to observe some intermediate state? If I just told you the OS would never schedule between these instructions again is that breaking any contract or something you can observe somehow? How would you tell?
If we can't such a program, then the logic isn't needed.
In one thread, do a loop of:
loop:
movq [rsi], 10
addq [rsi], 5
cmpq [rsi], 15
jeq loop
(Or however you write that with correct assembler).In another thread with the same value in rsi, do:
loop:
movq [rsi], 100
jmp loop
Ensure these threads run on different cores. You would expect the loop in the first thread to eventually terminate. The probability distribution of the time it takes to terminate would probably be informative.Would it terminate on this and chip? How would the distribution be different?
The store to the address at rsi will actually go in to the processor's store buffer and the subsequent add is allowed to retrieve the value from the store buffer _before_ it's drained to main memory.
Under TSO, AMD's memory mirroring implementation here is totally fine.
It could potentially change observable behaviour if you could turn it on or off but this is irrelevant given you wil see changes in observable behaviour for that same snippet of code depending on which microarchitecture of AMD or Intel processor you choose to run it on.
It's worth having a read on the details for different memory models and how they relate to implements. This seems like a good guide: https://www.cs.utexas.edu/~bornholt/post/memory-models.html
Why would you expect it to eventually terminate? There is nothing in the architecture that should lead you to expect that.
The architecture does not guarantee that instructions from the first loop will be interleaved with the second in any particular way, no matter what cores they're running on.
> The probability distribution of the time it takes to terminate would probably be informative.
But again... the architecture does not guarantee the set of or probability of distribution of interleavings. You cannot write a sound program using this information.
while (flag1 == true && turn == 1) { /* wait */ }
the loop terminates by another thread writing to either variable.If the variables were mirrored to registers, and that mirroring was not invalidated by another thread's write, then the loop would never terminate.
Of course Zen2 did not break Peterson locks, so the GP's question is cogent and has the answer "yes, there is such logic."
It's just a guess, but that's probably what happens in order for the memory mirroring not to break the cache-coherency. So "yes, there is such logic", but it already existed in the processor to avoid the same problem when data is in the cache. And so, there would be no additional logic to handle this.
Notice that your code has a backward jump between the write and the read and the example in the article (and the code of the other person I replied to) does not.
There is no logic needed to interrupt the renaming such as a signal from another core holding the cache, the rename just naturally retires beyond a certain point.
Read Morris Marden's 2014 patent on it for more details.
https://patentimages.storage.googleapis.com/16/3f/47/ea08705...
The patent is a great resource, thanks for that. But I think it supports my point when it suggests that memory renaming may persist indefinitely:
Alternatively, reclamation of the original physical register could be delayed to facilitate later memory renaming.
and later:
...the MRT may always retain physical registers of N past store operations...the physical register is reclaimed to the free list when it exits the MRT, for example, when it is no longer one of the N most recent store operations.
So it's possible. I don't think we know from Agner's work whether AMD went this route or not.
This optimization has a latency of 0-1 cycles.
If you write memory first to address A and then to B. Another core never can see the B change without also seeing the A change at any moment in time.
But in this case there is only a single memory location. So there is no ordering that could be violated in the first place.
It's like the ABA problem - how do you differentiate between just never happening to see a B out of chance, and B being elided instead? You can't.
As far as volatile: this isn't changing anything to do with memory operations. It's just caching a computed address in a special place so the CPU doesn't have to decode and recompute it for the very next instruction.
I think volatile in general is pretty useless, and it'd be better to replace it with atomic style functions:
x = volatile_read(&v);
y = use(x);
volatile_write(&y);
Which is both clearer about what's going on, and doesn't cause optimization barriers to happen everywhere, just around the specific loads and stores you care about.If you're paying attention to the standards community, what I'm wishing for was mostly sketched out in ISO/IEC TR 18037, section 6. (http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1169.pdf)
People try to use it for thread coordination. It's always been wrong.
Yes, he is wrong.
He has a point, though.
https://en.cppreference.com/w/cpp/language/cv
> Every access (read or write operation, member function call, etc.) made through a glvalue expression of volatile-qualified type is treated as a visible side-effect for the purposes of optimization (that is, within a single thread of execution, volatile accesses cannot be optimized out or reordered with another visible side effect that is sequenced-before or sequenced-after the volatile access. This makes volatile objects suitable for communication with a signal handler, but not with another thread of execution, see std::memory_order). Any attempt to refer to a volatile object through a non-volatile glvalue (e.g. through a reference or pointer to non-volatile type) results in undefined behavior.
In fact, Standards notwithstanding, real compilers routinely optimize away volatile operations if they deduce nothing can see the object -- e.g., it is on the stack, and its address has not been taken. And they will happily re-order reads from and stores to other objects around a volatile operation. Furthermore, caches don't know squat about volatile, so they will eagerly re-order the volatile ops, besides.
So, no, he did not have a point. He was just wrong, wrong, wrong. But it never caused him any personal distress.
But that is what he insisted.