14 ms·
Three proposals for the C standard WG14
- mpweiher 8y agoYes! Please return the standard and the compilers to sanity.
- geofft 8y ago> 6. EXAMPLE Code that checks to see if a pointer value is null cannot be omitted during translation solely on the basis that the pointer is dereferenced before the check and that earlier dereference would produce undefined behavior if the pointer value was null. Here's the thing I don't quite understand: I gather that this is advocating for something like GCC's -fno-delete-null-pointer-checks to be part of the spec, but, how is -fno-delete-null-pointer-checks actually helpful? I think it's because if you write code like this: void foo(struct foo *param) { int n = param->length, i; if (param == NULL) { return; } for (i = 0; i < n; i++) { do_something(param->data[i]); } } with -fdelete-null-pointer-checks the compiler might optimize it in two successive steps to delete the check (because param->length was already dereferenced) and then move the dereference down to the beginning of the for loop, and with -fno-delete-null-pointer-checks the compiler might skip the first optimization and keep the second. But for -fno-delete-null-pointer-checks, you're relying on the second optimization, moving the dereference later. Otherwise you'd just have segfaulted already (or, in a kernel context, the read might have succeeded from a userspace page mapped at the null page). Right? It seems reasonable for the compiler (or some other tool) to produce a hard error here and prevent this code from compiling, saying, hey, this is clearly doing something unwanted. It doesn't seem reasonable to expect the compiler to do certain optimizations to make your code safe, without which your code would have a security vulnerability, and also get mad at the compiler for doing more optimizations that return it to the original state of things. > The limitation of lvalue access by types has never made much sense and required a undefensible exception for character pointers. My understanding of type-based alias analysis is that it's required for code like this to optimize the way you'd want: struct string { size_t len; char *data; } void copy_string(struct string *dest, struct string *src) { size_t i; if (dest->len < src->len) return; for (i = 0; i < src->len; i++) { dest->data[i] = src->data[i]; } } Otherwise the compiler would have to worry that dest->data would overlap with src->len and re-read it every time! You can do this without type-based alias analysis in a language that gave you different tools for managing variables, but doing so backwards-compatibly in C seems difficult, and this proposed rule does not seem to help.
- deleted 8y ago[deleted]
- iainmerrick 8y agoWell, what benefit does -fdelete-null-pointer-checks bring? Why would I ever want to remove that check? The more conservative version either segfaults or (quite likely) accidentally does the right thing. That situation is not great but it’s better than nothing. The fancy optimization just makes a not-great situation much worse. I agree, it would be best if the compiler rejected this as clearly incorrect code. I think that needs a change in the spec relating to undefined behavior, though (and more importantly a change in direction from compiler vendors). Edit to add: your second example, type aliasing, is a good one too. I think that one could also be usefully addressed by a compiler that flags it as a likely error. Although you’d need some way to annotate the code in the event that the weird aliasing behavior is actually correct and expected (I dunno, make “len” volatile or something?)
- geofft 8y ago> Well, what benefit does -fdelete-null-pointer-checks bring? Why would I ever want to remove that check? The more conservative version either segfaults or (quite likely) accidentally does the right thing. That situation is not great but it’s better than nothing. My concern is largely about how to phrase "The compiler may not delete this dead code" in a way that's meaningful. For instance, I assume the intent of the proposal is not to forbid optimizing this code in the obvious way, right? int n = param->length, i; if (param == NULL) { return; } if (param == NULL) { return; } for (i = 0; i < n; i++) { do_something(param->data[i]); } What about this? int n = param->length, i; for (i = 0; i < n; i++) { if (param == NULL) { return; } do_something(param->data[i]); } which is more likely to show up in the form of an inline function that checks for NULL - can the compiler refactor that check into a single one? > I think that one could also be usefully addressed by a compiler that flags it as a likely error. The compiler can't know when compiling that one function whether src->len and dest->data alias. It might be compiling that file as a standalone object file, with no other context about who's calling it. Should it emit a warning when compiling that code? (What's the warning, "I'd like to optimize this but I can't?") Alternatively, should it flag the function ... somehow ... so that when someone else calls it with aliasing arguments, that's an error? That isn't even a capability available to the C language as written: it's valid to call that function knowing nothing other than the struct definition and the function's prototype. You can imagine a language where variables carried information about whether they alias. (Actually you don't really have to imagine; the `restrict` keyword does a subset of this in C itself, and other languages have much more explicit ways of tracking this, such as Rust's shared vs. mutable references.) But it would be hard to retrofit such a thing onto existing C code. The fundamental problem with the C language working group is that they're obligated to keep working on C as it already exists.
- iainmerrick 8y agoThank you for doing this.
- obl 8y agoSpecifying behavior is a bit more complex than that. There is a wide range of "unspecified" and the "full UB" one is the easiest from the spec point of view. Using the shift example : - allowed to assume that the negative shift does not happen (what it says now) - the value of each dynamic negative shift is unspecified but it must have a well defined value, however the same shift operation in the source code could return a different output given the same inputs dynamically (eg in a different loop iteration or function call) - each source code shift is a well defined (unspecified on negative inputs) function - all shifts in the program are a well defined (unspecified) function So, for example, the last (least surprising) version means that you have to compile every shift to the target CPU's shift and that you also have to use the target's behavior when you constant fold a shift at compile time.
- eftychis 8y agoThe first proposal is extremely welcome. Still you need to expand on it I feel. But yes, I think it is becoming the dream and nightmare along with aliasing (...) of a lot of engineers. It has become ridiculously impossible to write serious code without hitting the UB land. The second although I think I get the reasoning, seems not well-defined to me. I am not talking about expanding into it with concrete steps. You need to define what kind of semantic invariance to require. You might just not want the compiler deleting sections it thinks do not affect the output; you might not want the compiler to alter nop segments for timing reasons (e.g. when writing a cryptographic primitive). There is a wide variety of different aspects of semantics and it is not clear what you propose. This have been a discussion topic among cryptographers. Or you are a kernel programmer and actually do not want checks removed or aliasing messed with etc. (One reason people compile their kernels with specific options -- see Linus' rants about UB if you want to pull some extra hair of your head.) Anyways -- I am afraid at times GCC might be too far gone to tone down undefined behavior. Anyone working on a compiler has any input if there are plans, in the (unlikely) case the next WG decides to go sane on undefined behavior, how their compiler is going to address that new standard?
- jabl 8y ago> Anyone working on a compiler has any input if there are plans, in the (unlikely) case the next WG decides to go sane on undefined behavior, how their compiler is going to address that new standard? Disclaimer: I have contributed on my own time to one of the GCC frontends, so I guess I know a little bit more about compilers and the compiler development than the average Joe, but I wouldn't call myself an expert nor am I in a position to do any decisions wrt this for GCC. With that out of the way: - You can see compiler development, particularly the optimizer stuff, as a game, where the standard limits what you can do, and the goal is to make SPEC CPU [1] run as fast as possible (your employer says which target CPU(s) you should focus on). - The standard is the "contract" between the user and the compiler writers. Compiler writers do take standards conformance pretty seriously. - If the standard changes, say to limit undefined behavior, compiler developers adapt to the new rules, and the game continues. No big drama here. - AFAICT GCC developers wouldn't complain that loudly about "losing" previously allowed optimizations. There is (again, AFAICT) sympathy towards the viewpoint of changing the standard towards improving robustness. - What you won't see is GCC, or any other compiler, doing it on their own without the standard backing them. That would just lose performance compared to the competition, and wouldn't help users write portable code anyway. [1] Yes, there are other benchmarks too, but SPEC CPU is the most influential one.
- raphlinus 8y agoThis sounds like Regehr's "Proposal for a Friendly Dialect of C" [1]. I believe that was a well thought out argument, but ultimately did not make headway [2]. [1] https://blog.regehr.org/archives/1180 https://blog.regehr.org/archives/1180 [2] https://blog.regehr.org/archives/1287 https://blog.regehr.org/archives/1287
- iainmerrick 8y agoI haven’t seen a good explanation of the arguments against this kind of thing. Who doesn’t want to fix the UB weirdness? Are there really many people worried it will completely break optimization? If so I’d love to hear about it. Even if you don’t agree with this proposal, or with Regehr’s, surely a reasonable response would be “okay, yeah, it would be good to do something along those lines, but not that; how about this?”
- pjmlp 8y agoCheck Chandler Carruth's talks about UB, he already has a couple of them.
- vyodaiken 8y agoThey exhibit a really badly mistaken idea of the role of the compiler.
- raphlinus 8y agoI don't think there are good arguments against it, and I agree it would be great to fix it. But I think it's very important to understand the reasons why Regehr's proposal failed. I also think it's incumbent on new proposals to argue why their effort is likely to succeed where Regehr's did not. If I were more cynical, I'd say that the C-standards community has simply ceded the ecosystem niche of a low-level language which has common-sense behavior, creating an opportunity for other languages such as Rust, D, Zig, etc. But I don't really believe that. Almost everything we do relies on C at the bottom, and it's pretty much the only viable way to publish an API that can be consumed by multiple languages.
- anderskaseorg 8y agoYour proposed statement “conforming implementations cannot transform code on the basis that some behavior left undefined by the standard is impossible” would have no meaning in the standard, even if it were written somewhere other than a non-normative note. The standard doesn’t describe transformations; it describes behaviors. You can’t just say that undefined behaviors can’t be “transformed”, because there was nothing to transform them _from_. If you want some behaviors to be possible and some behaviors to be impossible, then you need to define those behaviors. For C, that’s not something we can realistically do in every case. There’s no way to reasonably constrain the set of allowed behaviors after a program does some crazy pointer operation like * (int *) rand() = rand(), because any implementation of those constraints would more or less require an expensive run-time check at every pointer access. What you could meaningfully ask is for particular situations (like negative shifts) to be considered unspecified or implementation-defined rather than undefined. That way you don’t give up the ability to constrain the behavior of programs after those situations arise. There are already many unspecified behaviors in the standard, such as reading the value of padding bytes in structs. Such proposals could be considered on their merits, but they’d have to be discussed individually. There’s no way to remove all undefined behavior from C and still have something that would be recognizable as C.
- TooBrokeToBeg 8y ago> The standard doesn’t describe transformations; it describes behaviors. That doesn't seem to be an argument against, or even related to, what the proposal entails. All of these proposals are reductive attempts to curb the instability that UB introduces, for little value (see the linux kernel). > because there was nothing to transform them _from_ Yes there was. Anything that can result in UB. > For C, that’s not something we can realistically do in every case. That's why the proposal is not exhaustive, as described.
- geofft 8y agoThere aren't things that "can result in" UB, though. The C code itself, before any compilation, is already undefined by the C spec. For instance, the following code is not valid C code: int a[2], b; a[2] = 5; The compiler is free to do anything it wants, because the standard has not defined what it does. It could stop. Most likely it will just overwrite whatever memory is after a[1] with 5, because that's easiest. If you want the compiler to do something specific, you need to define it. In some cases the standard has language for otherwise-defined behavior that is carved out as undefined (e.g., strict aliasing), and it's easy to go "Never mind"; in some cases it does not (e.g., writing to a string literal). For these ones, the behavior needs to be defined. Simply saying "Don't optimize this" is just saying "I'm not going to define the behavior, but don't implement it this way," which isn't a particularly useful way to define a standard (e.g., for the null checks example, is the compiler permitted to leave the null check in, but insert a segfault right before it?).
- 7676576575 8y agowow.checkout this new forum http://mawazo.tk http://mawazo.tk
- muricula 8y ago^^ This looks like spam.
- zokier 8y agoWhat exactly is the point of this article? Despite its title it feels neither modest, nor like a good-faith attempt of a proposal. It more feels like a cheap snipe against the standards group, regurgitating the same tired points. Does anyone really believe that anything productive comes out of this?
- tom_ 8y agoNo, but perhaps if people keep regurgitating these same tired points, compiler developers will get bored of listening, and start to do something different. As for the modesty, it's presumably a reference to this: https://en.wikipedia.org/wiki/A_Modest_Proposal https://en.wikipedia.org/wiki/A_Modest_Proposal
- vyodaiken 8y agoWhat is outrageous about this argument, which has been going on for far too long, is how people who don't get C or don't use it, keep dismissing critiques from expert practitioners like DJB or Torvalds and from academics like Regehr as if those people are all to damn stupid to understand the issues.
- ericpauley 8y agoSince nobody has mentioned it: https://en.m.wikipedia.org/wiki/A_Modest_Proposal https://en.m.wikipedia.org/wiki/A_Modest_Proposal
- loeg 8y agoI think what the author actually wants from (1) is a transformation of common (or maybe all) UB to be more strongly specified as implementation-defined. That could be reasonable, with specific scope. I'm having trouble determining the proposed scope from the post, though. (2) seems like it is already specified; the trouble is in determining what "the semantics of the program" are. This relates to (1) although it doesn't seem like the author has a completely solid grasp of the legalities of the language.
- jcranmer 8y agoWhat the author wants, essentially, is to dictate that common disable-UB flags (e.g., -fwrapv, -fno-strict-aliasing) be folded into the C standard. Well, he probably knew that the C standards committee isn't going to accept that, so instead he's trying to "only" disable the bits of undefined behavior that don't screw over the user. Except he doesn't have a good enough grasp of the standard to actually effect proper wording: (1) essentially disables inlining as a side effect, at least if undefined behavior is involved. (2) either is 100% redundant or means that every implementation must strictly execute the C abstract machine with absolutely no optimization permitted. (3) means you can no longer access any type with a char. You also couldn't access the bits of a float by casting to an int.
- vyodaiken 8y agoit's bizarre to me that people make this argument, given the obvious fact that those flags do not prevent optimization or inlining.
- crooked-v 8y agoAs somebody primarily focused on other languages, I've always found it absolutely bizarre that in C-land it's considered even remotely acceptable for "bad stuff" to result in invisible-to-the-user transformation of code instead of actual error handling.
- palotasb 8y agoI guess you're focused on languages that don't have the following two constraints from C: (1) be as fast as possible (2) compile ahead-of-time. The "weird" behavior in case of UB in C/C++ is a direct result of these two constraints and the fact that the transformations you're referring to are only affect code that already has _programming bugs._ The standard already specifies that optimizations don't change semantics -- and really they don't -- for bug-free code. Adding runtime error checks to that code only helps buggy programs and pessimizes the performance of correct ones. This is a tradeoff C/C++ does not make.
- pjmlp 8y agoUsing seat belts is an inconvenience that only gets in the way of good drivers, designed to help drivers that aren't capable of handling traffic.
- int_19h 8y agoIt's probably an accurate comparison, given the amount of code out there that really doesn't need to be written in C, but nevertheless it is (and the majority of people writing it aren't capable of writing bug-free C).
- pjmlp 8y agoJust think about the memory corruption issues that occasionally pop up on Linux, in spite of their static analyzers and the review process of getting anything into kernel mainline.
- palotasb 8y ago
- YouAreGreat 8y ago> conforming implementations cannot transform code on the basis that some behavior left undefined by the standard is impossible You may have misunderstood how rogue optimizers obtain their license to miscompile. It is not by assuming that the "behavior left undefined is impossible". As boring and tautological as it sounds, it is by assuming that the operation with "behavior left undefined", when it actually happens, has undefined (and therefore arbitrary) behavior.