5 ms·
The languages that you describe must look nothing like C (other than, ironically, syntax), and must have no untyped direct memory access pointer feature at all,
by eddyb 8y ago
The languages that you describe must look nothing like C (other than, ironically, syntax), and must have no untyped direct memory access pointer feature at all, which usually means they rely on a GC instead for memory safety.
- zokier 8y ago> and must have no untyped direct memory access pointer feature at all Can you expand on why you feel this must be the case? I'm thinking for example deference of pointer could be defined to be equivalent of platform-specific memory load instruction. It wouldn't be memory safe and would segfault like C, but it still wouldn't bring up nasal demons like C UB.
- pcwalton 8y agoIf every source-language load must result in a memory load, then you at the very least killed scalar replacement of aggregates (a critical optimization), and, depending on how you define it, you might have killed register allocation too.
- eddyb 8y agoYou get register allocation back if you deny taking the address of variables declared with the `register` keyword - and we've gone full circle!
- kazinator 8y agoTaking the addresses of variables defined register is denied; that's the only modern meaning of the specifier.
- kazinator 8y agoGive me the rule "every source language load of a structure member, array or global variable, or anything loaded indirectly through a pointer results in an actual load" and I will still crank out fast code, by explicitly using local variables for caching the values coming from those places.
- pcwalton 8y agoYou'll also write unmaintainable code. To a large degree, optimizations exist to allow programmers to write maintainable code without sacrificing performance.
- kazinator 8y agoI will write absolutely clear code this way. I use that style anyway, quite often. Optimization can work backwards in this regard. For instance, let's consider CSE (common subexpression elimination). That allows some repetitive code to produce the same results as code that adheres to DRY. E.g. stupid way to insert into a circular list: node->prev = prev; node->next = prev->next; prev->next->prev = node; prev->next = node; The compiler has no idea whether node and prev are aliased or not. The assignment to node->next might be the same as prev->next, and so prev->next has to be reloaded even though it was just loaded in the previous line. smart way: get a local variable for prev->next! node *next = prev->next; node->prev = prev; node->next = next; next->prev = node; prev->next = node; Bonus: much more readable, lining up in neat columns. Just one arrow in each line. If this doesn't generate at most one load and four stores, the compiler is garbage. Also note that we can reorder these four assignments in any of the 4! permutations and they produce the same result (unless there is aliasing, which would be unintentional and wrong regardless of the order). Not the case in the original. For instance, it's important that prev->next is stored in node->next before the assignment to prev->next. You can shoot yourself in the foot with local variables though. Caching is susceptible to staleness. You have to know when it is legitimate to keep using the cached value and when it must be reloaded or disused. After our assignment prev->next = node, the next variable no longer represents the value of prev->next. In this case, since we are done, we don't care. If more code followed which still assumed that next is the original successor of prev (now the successor of node), that would be wrong.
- pcwalton 8y agoDo you also, for example, look up the magic multiplication number in Hacker's Delight every time you want to divide by a constant?
- kazinator 8y agoPointers are a red herring; misuses of pointer dereferences are the manageable form of UB that programmers understand well, in reference to memory models that are straightforward. It's all the nasty gratuitous UB that wrecks the C language: behavior that could be well-defined without changing the character of the C language, or making incorrect any existing programs that are correct. Like UB in the preprocessor. WTF? Processing a bunch of tokens at compile time should be totally safe. But no: if the ## token pasting operator glues together two tokens which do not look like one token, the behavior is undefined. It worked differently on different compilers 35 years ago and was coded as undefined. It being undefined gave the compiler writers no incentive to fix their implementations to some common behavior (like diagnosis of an invalid token paste).
- kazinator 8y agoWe can easily imagine a language that is exactly like C in every regard, but free of some gratuitous behaviors. All standard-conforming C programs work in this dialect, and a good many others also work and are portable. For instance, we could have a dialect of C in which this is required to print "0123": int i = 0; printf("%d%d%d%d\n", i++, i++, i++, i++); A behavior is gratuitously undefined if it is left that way for no good reason, such that programming language constructs can be undefined simply for having the wrong form. That is to say, the input values are well-defined (i has a good initial value, which we can increment four times), and the individual operations are defined also (i++ is fine by itself). But for no possible value of i is the above printf call correct. An example of undefined behavior which is not gratuitous is overflow on integer addition. Th expression i + j, where i and j are int, is not ipso facto undefined because of its form. Only for certain combinations of values of its operands is it undefined. We cannot simply banish that without banishing addition, and various ways of making overflow defined have drawbacks, like being expensive (e.g. target machine has no native support for the particular behavior, so extra instructions have to be generated) or super-expensive, with complicated representation and memory management (switching to bignums).
- eddyb 8y agoIt's interesting to see which things Rust has defined where C refused to, such as fixing a strict evaluation order (mostly post-order on the AST), requiring signed integers to be 2's complement or masking shift amounts (`x << n` being `x << (n % bitwidth)`). Where does C actually gain anything nowadays? Signed integer UB is mostly useful for optimizing misuses of `int` for unsigned values (https://news.ycombinator.com/item?id=17191295 https://news.ycombinator.com/item?id=17191295), and is evaluation order even relevant anymore? (I don't think clang can pass that information down to LLVM, at all) The only example I gave which has known drawbacks is the shift one, where modern platforms differ in the behavior, and LLVM will only optimize out the masking of the shift amount on the platforms that have that same behavior in their shift instructions (I think x86, but not ARM). However, even if you make all of these changes to C, there's still a lot of UB left, in the form of memory accesses, which is much harder to get rid of (see the other comments, some of which mention Rust as well).