Such languages typically do provide some form of "no promises, here there be dragons" in the form of unsafe blocks or functions. Restricting this region of unsafety is a huge benefit for programmers and compilers. Programmers because relatively few pieces of code need to be carefully reviewed. Compilers because most of the program contains little or no UB.
As a typical example, in C you can alias and type pun. Therefore to avoid all UB the compiler would need to be extremely careful in any function containing a pointer. You could return a reference to a local through a separate opaque function call, or receive multiple pointers all aliasing the same memory. To completely avoid all UB means inserting checks after every potentially mutable machine instruction, or emitting duplicate function bodies that take different paths when the pointers alias; that's assuming the compiler even has enough type information to answer the question!
With typical C#, Rust, Swift, etc you just can't cause those kinds of problems without deliberate subterfuge and use of Unsafe types or blocks.
Or to go even further in the past, NEWP, still alive on Unisys ClearPath mainframes.
Lest we forget, any language with a C FFI capability must have some notion of UB, because the FFI effectively includes the C code in its own semantics (unless fully sandboxed, which may be too expensive to be done).
In systems like Unisys ClearPath, you can even configure the system such that only admins can execute applications with unsafe blocks.
In C any line of code, if care is not taken to use the correct compiler flags, can be a possible source of UB.
What do you mean?
ANSI C11 has 200 documented cases of UB, and each compiler might have additional cases, are you sure you can know all of them by heart while looking at a random line of C code?
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.
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.
By the way, the magic multiplication number can be worked out, because it's just fixed point math. We take the 32:32 fixed point representation of 1 and divide by 17 to get an approximation of 1/17 in 32:32 fixed: 0x100000000 / 17 = 0xF0F0F0F. That's our magic number for dividing 32 bits by 17 by doing a 64 bit multiplication. For instance 90/17 is 90 * 0xF0F0F0F = 0x54B4B4B46. The integer part of this 32:32 fixed point value is in the upper 32 bits which is 5.
There are some subtleties there, plus considerations of whether we want signed or unsigned division. Better let the compiler deal with it. Arithmetic reductions are safe optimizations. It's hard to imagine what you could do wrong so that an arithmetic reduction breaks your code, given that it produces the same result and doesn't interact with some some memory aliasing where the compiler isn't informed about what you're doing.
And by the way, given this code:
#include <stdio.h>
struct node {
struct node *next, *prev;
};
void ins_after_a(struct node *prev, struct node *node)
{
node->prev = prev;
node->next = prev->next;
prev->next->prev = node;
prev->next = node;
}
void ins_after_b(struct node *prev, struct node *node)
{
struct node *next = prev->next;
node->prev = prev;
node->next = next;
prev->next = node;
next->prev = node;
}
gcc 7.2.0 on Ubuntu 17 generates better code for the cleaner, streamlined second one with the local variable, for exactly the reason I gave. ins_after_a yields 6 movq instructions; in_after_b yields 5.Better source that is easier to reason about; better machine code: all round win.
ins_after_a:
.LFB23:
.cfi_startproc
movq (%rdi), %rax
movq %rdi, 8(%rsi)
movq %rax, (%rsi)
movq (%rdi), %rax <-- wasteful re-load of (%rdi) due to aliasing suspicion
movq %rsi, 8(%rax)
movq %rsi, (%rdi)
ret
.cfi_endproc
.LFE23:
.size ins_after_a, .-ins_after_a
.p2align 4,,15
.globl ins_after_b
.type ins_after_b, @function
ins_after_b:
.LFB24:
.cfi_startproc
movq (%rdi), %rax
movq %rdi, 8(%rsi)
movq %rax, (%rsi)
movq %rsi, (%rdi)
movq %rsi, 8(%rax)
ret
.cfi_endproc
The language could be specified that way (accesses to structs are memory loads) for all I care and that could be helpful.In this case, you could avoid the wasteful re-load by marking the function arguments as `restrict`. One of the reasons Rust's memory model is interesting is that the language statically prevents mutable aliasing in most cases, so "restrict" comes for free… well, kind of. (It's actually rather difficult to nail down the precise guarantees, especially when your compiler's backend was originally designed for C.)
That's what restrict does: it makes some behaviors undefined and otherwise doesn't change the semantics of the program. It's completely against the grain of moving to a safer language.
Why would I do that, if I can instead beautify the source and machine code while sticking to what was available in C90.
I think restrict is mainly intended as a way of competing against Fortran. If you're processing arrays referenced by pointers, and can get the compiler to believe that they do not overlap, then the compiler can unroll the loops and rearrange the accesses and calculations in the unrolled body. Like it can load four elements from a source arrays, do four calculations, and then store four elements into a target, rather than interleaving. That can be done by vectorized instructions.
Here is where our technique falls short: if we write the loop body with our load-calculate-store style, it cannot be unrolled and vectorized. We have pinned down an exact behavior for all possible cases of aliasing, like self-overlap with a displacement. Vectorizing unrolls do not preserve that behavior.
Manual unrolling isn't attractive because it's a guessing game. A good amount of unrolling on one machine may give the instruction cache indigestion on another machine.
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).
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).
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), 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).
I.e. you mean that the compiler front end has to choose an order without knowing which order will be good for LLVM, and there is no information to say "I don't actually need this specific order; pick another one that is better".
Compilers for languages with strict evaluation order can still perturb evaluation order when it is safe to do so: like when expressions do not have side effects, or have effects that don't mutually interfere and are not external. They can thus avoid or eliminate the temporaries.
C is still being specified like it's 1982 and compilers have to run in a few kilobytes of RAM, in a single pass, and go straight from source to target machine code.