5 ms·
I love the WTF potential of this one: Another interesting optimization is to use potentially shared memory locations (on the stack, heap and globals) as scratc
by qb45 10y ago
I love the WTF potential of this one:
Another interesting optimization is to use potentially shared memory locations (on the stack, heap and globals) as scratch storage, if the compiler can prove that they are not accessed in other threads concurrently. [..] For example the following transformation could occur:
// Some code, but no synchronization.
*p = 1; // Can be on stack, heap or global.
Becomes:
// ...
*p = RAX; // Spill temporary value.
// ...
RAX = *p; // Restore temporary value.
// ...
*p = 1;
Since we write to p and there is no synchronization operations, other threads do not read/write p without exercising undefined behavior. We can therefore use it as scratch storage.
- mike_hock 10y agoI see no WTF here. Concurrent reads from * p have to be allowed to produce any possible value whatsoever, since if the variable is larger than a system word, the compiler may have to break up the write into multiple instructions (and some architectures don't even have instruction-level isolation). The language doesn't give you any stricter concurrency guarantees just because a variable is sizeof(void* ). And that's good. It makes the language cleaner. No bullshit special cases that require you to know implementation details. You want atomic behavior, use std::atomic (or a lock), period.
- xorblurb 10y agoThis prevents debugging, probably does not really optimize (except 1% in useless microbenchmarks), and increases compiler complexity and the risk of bugs in it. A similar thing has already happen in the past: compiler started to introduce speculative writes on code pathes where the write did not occur in the source code, and this broke multithreaded programs -- IIRC there was no threading model at that time (the current one forbid to do that kind of thing, and it is probably impossible to get a sound threading model that does not forbid that) but instead of saying that they were technically allowed to do that compiler authors recognized that because it made multithreaded programs impossible to write, that they had to fix their shit. In the C standard, crashing is not a specified behavior, so when an obscure architecture crashes on a specific construct, the standard has to technically say that this is undefined behavior. That compiler authors now interpret most of undefined behavior as a license to be stupid on ALL architecture instead using their brain and allowing non-portable programs to exist is not them being smart optimizing all our code-bases for free but just a proof they do not understand a part of what they are doing, including the standard they pretend to know so much. So the position of "all the standard, nothing but the standard" is utter bullshit. Another example: the current published atomic specification is subtly unsound. Do compiler authors do the right thing instead of trying to implement the broken spec, while waiting for the spec to be fixed? On that point yes -- at least for now. So when, on other subjects, they introduce new bugs in compiled code compared to what all previously known compilers produced - and without proving this really improves performance like crazy to justify taking that kind of risk - they are just being dangerous asses. -O2 has shown to be able to be fast enough without (reasonable) code breaking behavior in the past. In most case you wants to keep the same behavior as -O0 built programs. An optimisation that does not preserve behaviors reasonably expected by skilled programmers and requires that every programmer knows the standard by heart at any point of their work under penalty of crashes and security holes is not an optimisation but a piece of crap. This is blurry but the C / C++ standards are blurry and their usage even more so -- and more importantly they do not forbid to stay safe and sane to begin with -- so trying to do formal logic instead of doing engineering by thinking about the whole picture and history is completely stupid.
- mike_hock 10y agoIn this case, the programmer cannot reasonably expect that a concurrent read produces a meaningful result. There are good reasons to use clean, abstract formal logic over brutish "engineering" that results in a myriad of unmaintainable "if x, y, and z, do this, but not if also a or b, then we do that." That said, I agree that compilers should not needlessly optimize based on undefined behavior in cases where a sensible architecture-specific behavior can be defined (but turning a "read can produce any value" into a "read can produce high 32 bits from old write and low 32 bits from new write" isn't even worth specifying).
- amluto 10y ago> Another example: the current published atomic specification is subtly unsound. Can you explain or point us somewhere?
- gpderetta 10y ago'the current published atomic specification is subtly unsound' Are you talking about memory_order_consume? Although I believe that correctness proofs have so far excluded memory_order_consume, I do not think the current spec has been shown to be unsound; it is just generally regarded as hard to implement and hard to use correctly. In practice most compilers (conformingly) just lower it to memory_order_acquire; even Linus has been considering doing away with all the dependent load trickery in the kernel and just convert all of them to explicit load acquires. x86 and SPARC are TSO machines and have zero cost acquires, modern POWERs can also be run in a similar sane TSO mode and ARM64 has cheap explicit load acquire and store release (which I believe were added explicitly to allow simple mapping of the C and C++ memory model). Even Itanic has (I assume cheap) load acquire and store release operations. Other insane architectures that actually require dependant loads for performance can go and have anatomically impossible sex with themselves. edit: spelling
- jfbastien 10y agoThe latest on consume: http://wg21.link/p0190r0 http://wg21.link/p0190r0 The spec is pretty sound, but unimplemented because that's not how compilers work right now for the generality that consume wants. I think the above proposal is likely to work because it's more restrictive towards how consume works, but we need implementation experience. Linux uses consume extensively for RCU, and I don't think it'll go away soon because of the benefits it provides to Power and ARM. It works through a gentleperson's agreement between Linux's RCU implementation in C and GCC.
- qb45 10y ago> I see no WTF here. Not WTF in itself, but has WTF potential. The way I read it is: If you forget to put synchronization around accesses to global state, in addition to the usual problems, we will also overwrite your globals with random garbage. Which sounds great when you think about debugging affected code. > sizeof(void* ) You think that's equivalent to machine word? On 16-bit x86, for example, pointers can be either 16 or 32 bit ;)
- mike_hock 10y ago> You think that's equivalent to machine word? On 16-bit x86, for example, pointers can be either 16 or 32 bit ;) The 16bit x86 memory model is absolutely atrocious. And IDGAF what you call a "machine word" on x86, it's a 64 bit architecture grafted onto a historically grown 32 bit architecture grafted onto a historically grown 16 bit architecture grafted onto an 8 bit architecture. And when Intel tried to do a clean reboot for the 64-bit part, the market chose the grafted-on abomination instead. Also, you can have 128 bit atomic loads if you use an xmm register. But that's for the compiler to figure out when you declare an std::atomic of a 16-byte large type, not for you to assume on a plain variable, which was my point.