dzaima
Joined 7 years ago
Joined 7 years ago
Of course no doubt that (hopefully) C compilers used for compiling plan 9 things won't break it, but...that's then talking about plan9-C, not C99, at which point you're necessarily already in the bucket of "this library relies on extra guarantees that plan9 C makes, instead of it just being a regular C99 library".
> > so you'd need to know how Plan 9's compiler/language specifies atomics.
> It doesn't, as far as I can tell. Kencc is a very "dumb" compiler
...So it effectively quietly makes a bunch of quiet guarantees that C99 doesn't, on account of not making certain valid optimizations. Even if not explicit, that's still semantics outside of those C99 specifies, that code may be relying on.
Fact of the matter is, you cannot make a full atomics library that will necessarily work correctly on C99. As a simple example I came up with on the spot:
static int foo;
int main() {
some_preparation();
foo = 2;
fence_probably();
do_thing(&foo);
}
A fully-proper standards-compliant C99 compiler could notice that foo isn't used until the `do_thing` call, and as such reorder the `foo = 2;` to after the `fence_probably` call (yay for putting all operations on foo in the same place!).But, if atomics exist, `do_thing` could do a relaxed store of its given foo address to a global, and another thread could then do a relaxed load of that and dereference that to get foo's value. And if the compiler decides to reorder the store to `foo` to after the fence (as a C99 compiler can), the program now has a data race that it otherwise would not.
(hell, a C99 compiler could even insert `int tmp = something; something = 123; something = tmp;` for any variable or global or field `int something`, in any place in the code (as long as `something` is known to be mutable), which'd break basically every multithreaded thing; this effectively one of the things the "Threads Cannot Be Implemented As a Library" paper mentions)
That said, I was surprised that it's mandatory by the standard (and wasn't made optional later like VLAs); feels rather unlike C to require such precise control over something it doesn't even specify any behavior of - imagine if we had mandatory macros for integer overflow behavior and whatnot!
...That "that said" said, C11 does also contain:
> Each of the macros FE_DOWNWARD FE_TONEAREST FE_TOWARDZERO FE_UPWARD is defined if and only if the implementation supports getting and setting the represented rounding direction by means of the fegetround and fesetround functions.
so presumably one could just not define those and get away with not implementing (at least the rounding mode part of) fenv.h, properly allowing the `#define FLT_ROUNDS 1` hack? gcc+glibc does define those, though.
Well, considering that gcc doesn't support the FENV_ACCESS pragma, and as such all fesetround calls are always UB, it is valid for it to act as if the rounding mode never changes from the default :)
Perhaps not significant for writing code (though maybe someone does want the NaN bit pattern to be taken from a specific arg!), but is one aspect that can easily go missing in a higher-level assembler (or esp. disassembler).
vpor xmm2, xmm0, 0x80000000
vpcmpeqd xmm2, xmm1, xmm2
vcmpunordps xmm2, xmm2, xmm0
vminps xmm0, xmm1, xmm0
vblendvps xmm0, xmm0, xmm1, xmm2
yay for superoptimizersHit upon https://github.com/llvm/llvm-project/issues/217376 while playing around with proper minimumnum, failing to SMT-verify whatever version of LLVM I had; did find a funky working 6-instr (+ constant) version though:
vpandn ymm2, ymm1, ymm0
vpcmpeqd ymm2, ymm2, 0x80000000 # whether ymm0 is -0 and ymm1 is +0 (or other cases that magically don't cause issues)
vcmpltpd ymm3, ymm0, ymm1
vcmpunordpd ymm3, ymm3, ymm1 # regular NaN-is-larger ymm0<ymm1
vpor ymm2, ymm2, ymm3
vpblendvb ymm0, ymm1, ymm0, ymm2They've already gone through the pain, deciding on it on 2018[0] (and functional & non-experimental 3 years ago per TFA). What's left is just changing the default (and some stragglers to complete support). Changing the function now would push back changing the default by a couple additional years until the new git version gets widespread deployment (incl. on LTS distros and whatnot).
[0]: https://github.com/git/git/commit/0ed8d8da374f648764758f1303...
This leaves things in a weird state though, as modifying the branch from one of the worktrees will effectively change the ref the other is pointing to, but won't update the other's working tree, making it look like it's preparing to revert all the changes that the first worktree made.
(obligatory mention of jj, whose workspaces track what state they were snapshotted at, thus never losing what the true diff of a workspace should be, allowing a "jj workspace update-stale" to safely update the working copy with whatever the other workspaces changed (if you change the working copy and in parallel from another workspace change what that workspace points to, you'll get a saved commit of the working copy changes before being updated, which you'll need to squash/rebase wherever those changes were needed))
(also; if anything, the title is implying the exact opposite of "Rust compiler was able to optimize ...", "Replacing a Rust [...] with [...]" is clearly moving away from Rust-magic to something else)
In the case of a tagged object in Rust, depending on how well the compiler can wrangle through it, you might even be able to add a `.unpack()` method that returns a pretty enum from a packed value, that you can pattern-match on or whatever, and let the compiler remove all the code of unpacking unused cases.
(using that directly for the addition example would end up less efficient of course, but still most likely beneficial. It's after this when there's a potential true readability vs performance tradeoff)
https://codeberg.org/dzaima/