Back in 89, many of those unspecified behavior were understood as implementation/hardware dependent, not undefined. Aliasing was the norm, `restrict` was actually a keyword.
Modern C is neither safe nor low-level.
But definitely by the time C99 came out, it is clear that optimize-assuming-UB-doesn't-happen was an endorsed viewpoint of the committee [1]. C99 also added restrict to the language (not C89 as you suggest), and restrict was the first standardized feature that was a pure UB-optimization hint [2].
It is important to remember that there isn't just one catch-all category of implementation-varying behavior. There is a difference between unspecified behavior, implementation-defined behavior, and undefined behavior. Undefined behavior has been understood, from its inception, as behavior that doesn't constrain the compiler, and often describes behavior that can't be meaningfully constrained (especially with regards to potentially-trapping operations).
[1] The C99 rationale gives an example of an optimization that compilers can perform that relies on assuming UB can't happen--reassociation of integer addition, on one's complement machines.
[2] The register keyword is I believe even in K&R C and would also be qualified as a compiler hint feature, but I note that it prohibits taking the address of the variable entirely, so it doesn't rely on UB. Whereas restrict has to rely on "if these two variables alias, it's UB" to allow the compiler to optimize assuming nonaliasing.
Was it? I thought it’s more recent. https://en.wikipedia.org/wiki/Restrict seems to agree (“In the C programming language, restrict is a keyword, introduced by the C99 standard,[1] that can be used in pointer declarations”), as does https://en.cppreference.com/w/c/language/restrict (“restrict type qualifier (since C99)”)
Was there an older usage?
http://blog.llvm.org/2011/05/what-every-c-programmer-should-...
The TL;DR is that compilers compile code based on assumptions that UB won't be invoked. This sometimes produces extremely surprising results which have nothing to do with the hardware/OS.
Unspecified behavior is more what you're thinking of, though in that case the standard still provides a list of possibilities that compiler authors have to pick from, they just don't have to document it or always make the same choice for every program.
There's no allowed subset of behavior where compiler authors are free to pick whatever they want and document it (but must do so). IMO there should be, most "Undefined Behavior" could be specified and documented, even where that choice would be "the compiler assumes such situations are unreachable and optimizes based on that assumption" like much of current UB. At least it'd be explicit!
The standard definitely does not require implementations to pick from a list of possible behaviors. All the standard requires is that the implementation document the behavior.
For example, the behavior on integer demotion is implementation-defined and there's no list of possible behaviors:
> When an integer is demoted to a signed integer with smaller size, or an unsigned integer is converted to its corresponding signed integer, if the value cannot be represented the result is implementation-defined.
> Unspecified behavior is more what you're thinking of, though in that case the standard still provides a list of possibilities that compiler authors have to pick from
That contradicts the standard's definition of unspecified behavior. For example, from the C89 draft (emphasis added) [0]:
> Unspecified behavior --- behavior, for a correct program construct and correct data, for which the Standard imposes no requirements.
For implementation-defined behavior, the language standard lays out a menu of options and your implementation is required to pick one and document it. IMHO, many things in the C standard are undefined that ought to be implementation-defined. But unaligned pointer accesses would be hard to handle that way; at best you could make the compiler explicitly document whether or not it supports them on a given architecture.
I’m alright with folks sticking to aligned pointer operations, largely for performance reasons. On some platforms, unaligned operations are really expensive.
Another is that you want to guarantee objects are stored aligned in memory because that gives you some free bits in pointers you can hide stuff in. (This has less hardware support than it should.)
My point here is that you can’t have “everything works as it does in the native assembly language” and “portable assembly” at the same time because if you rely on implementation defined or undefined behaviour then it’s not portable any more