<p>This question is tangentially related to: <a href="https://langdev.stackexchange.com/q/2350/38">Why is type reinterpretation considered highly problematic in many programming languages?</a></p>
<p>Regardless how 'problematic' type reinterpretation is, why do some languages such as C allow doing so through some avenues (<code>memcpy</code>, <code>union</code>) but not others such as pointer casts? A search reveals that the answer lies in 'optimization' but it remains unclear to me how to leaving the behavior constructs such as <code>int x; float y = *(float *)&amp;x;</code> undefined permits optimizations that would not be possible if the behavior were defined the same way as if one used a <code>union</code> or <code>memcpy()</code>.</p>

<types><c><undefined-behavior><type-conversion>
What optimizations does the strict aliasing rule facilitate?
<p>The issue is discussed explicitly in the C99 Rationale at <a href="https://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf" rel="noreferrer">https://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf</a> starting on page 59, line 33, and continuing through the top of line 61.  The basic idea is that given a function like [from the Rationale]:</p>
<pre><code>int a;
void f( double * b )
{
  a = 1;
  *b = 2.0;
  g(a);
}
</code></pre>
<p>it's likely that a compiler's customers wouldn't mind if the compiler were to replace the call <code>g(a)</code> with a call to <code>g(1)</code>.  There are some contrived scenarios where that substitution would be (as noted in the Rationale) &quot;incorrect&quot;, but the view of the Committee was that if a compiler's customers wouldn't demand that a compiler accommodate such possibilities, the Committee shouldn't either.  Note that because compilers would always be allowed to access such constructs &quot;in a documented manner characteristic of the environment&quot;, any compilers whose customers would care about such constructs remained free to process them in the &quot;correct&quot; manner.  Such code may not be portable, but as observed on page 13, in discussing conformance, &quot;A <em>strictly conforming program</em> is another term for a maximally portable program.  The goal is to give the programmer a <em>fighting chance</em> to make powerful C programs that are also highly portable, without seeming to demean perfectly useful C programs that happen not to be portable, thus the adverb <em>strictly</em>.&quot;</p>
<p>Note that the example shows a relatively non-controversial and safe kind of optimization: saying that if storage is accessed as type T and later read again as type T, and if no intervening actions have any visible relationship to anything having to do with type T, a compiler may replace the load implied by the second operation with code that re-uses the previous value read or written.  This optimization is simple but effective and relatively safe, and will be compatible even with most code for which clang and gcc would require <code>-fno-strict-aliasing</code>, provided only that a compiler makes a good faith effort to notice when intervening actions have a visible relationship to the type involved.</p>
<p>Note that because a function like:</p>
<pre><code>unsigned get_float_bits(float *fp)
{
  return *(unsigned*)fp;
}
</code></pre>
<p>would be non-portable, the Standard makes no attempt to exercise jurisdiction over how compilers treat it.  It was obvious to just about everyone when the Standard was written that <em>anyone wanting to sell compilers to anyone who would find such constructs useful</em> should process such constructs &quot;in a documented manner characteristic of the environment&quot; with or without a mandate, and thus there was no need for the Standard to recognize such constructs.</p>
<p>Note that the authors of the Standard get unfairly maligned for the Standard's failure to forbid nonsensical compiler behavior, as though they intended to invite such nonsense.  The Standard was a compromise between people who wanted to recognize non-portable constructs as legitimate, and those who wanted to forbid anything that wasn't portable; the solution was to have the Standard <em>waive jurisdiction</em> over such constructs.  If waiver of jurisdiction is recognized as implying nothing about how implementations process constructs that are intended to perform tasks in non-portable fashion, the Standard's categorization of constructs that are useful but non-portable as Undefined Behavior is entirely appropriate.</p>

CC BY-SA 4.0
2023-10-04T20:22:37.223Z
2023-10-05T15:02:18.840Z
2023-10-05T15:02:18.840Z
supercat
<h3>Aliasing</h3>
<p>As mentioned by @rpjohnst's <a href="https://langdev.stackexchange.com/a/3000/874">answer</a>, the definite absence of aliasing is the optimization enabler. That is, a number of optimizations are only sound -- ie, do not alter the semantics -- if the pointers they operate on can be proven NOT to alias.</p>
<p><em>How</em> the alias analysis which allows deducing this absence of aliasing is conducted has no impact on the optimizations being enabled, only on which pointers can be proven not to alias.</p>
<h3>Alias Analysis</h3>
<p>Proving the absence of aliasing, however, is hard. It's related to the Escape Analysis problem faced by the compilers of GC languages, and can in fact be arbitrarily hard.</p>
<p>Since the optimizations enabled by proving the absence of aliasing are so important, it therefore makes sense to &quot;simplify&quot; alias analysis by introducing rules in the language:</p>
<ul>
<li>Fortran requires function parameters not to alias.</li>
<li>C and C++ have the strict aliasing rule.</li>
<li>C (since C99) has the <code>restrict</code> keyword, and many C++ compilers have the <code>__restrict</code> extension.</li>
<li>Rust has <code>&amp;T</code> vs <code>&amp;mut T</code>.</li>
</ul>
<h3>Why not Strict Aliasing?</h3>
<p>Strict Aliasing is one of the early set of rules decided on to simplify alias analysis, and implemented in C and C++. It unfortunately suffers from a number of issues which later sets of rules have attempted to improve on.</p>
<p>Strict Aliasing, while conceptually simple and intuitive, is plagued by exceptions and accidental collisions:</p>
<ul>
<li><strong>Accidental Collisions</strong>: as demonstrated in @rpjohnst's <a href="https://langdev.stackexchange.com/a/3000/874">answer</a> a function taking two parameters -- a length <code>int*</code> and an array <code>T*</code> -- will not be as well optimized when it turns out that <code>T = int</code>, because suddenly Strict Aliasing cannot exclude the possibility of aliasing. This is made even worse by the fact that <code>int32_t</code> is just a typedef of <code>int</code> on most platforms, so that even types that are not spelled the same may &quot;accidentally&quot; be the same and foil Strict Aliasing.</li>
<li><strong>Exceptions</strong>: exceptions are carved into the rule for <code>char*</code> and <code>unsigned char*</code>, allowing those to aliasing everything. It's a useful exception... but also a <em>very</em> unfortunate one seeing as <code>char*</code> is a widely used pointer type whenever one uses strings or raw memory.</li>
</ul>
<p>C99 introduced the <code>restrict</code> qualifier for much more fine-grained annotations, which do not fall apart by accident.</p>

CC BY-SA 4.0
2023-10-05T10:04:23.427Z
2023-10-05T10:04:23.427Z
Matthieu M.
<p>The main thing the optimizer wants to know here is whether writes through one pointer might affect reads through other pointers or names.</p>
<p>When it knows for sure that two pointers are unrelated in this way (they do not ever <em>alias</em>) it can reorder those unrelated reads and writes for purposes like common subexpression elimination, loop-invariant code motion, or caching intermediate results in registers.</p>
<p>On the other hand, when pointers <em>might</em> alias, the optimizer becomes much more restricted in what it can do. It might be forced to re-load the same values every iteration of a loop, or re-compute the same values several times. If you've ever heard the folklore that Fortran is still faster than C, this sort of missed optimization is why people say that.</p>
<p>Keep in mind that the most important cases here involve pointers that are used across multiple functions. When analyzing uses of pointers that come from parameters or global variables, the compiler has much less information about data and control flow, for multiple reasons:</p>
<ul>
<li>The function may called in many different places at many different times. Combining all this information would be complex, slow, and also (due to <a href="https://en.wikipedia.org/wiki/Rice%27s_theorem" rel="noreferrer">Rice's theorem</a>) necessarily incomplete or imprecise in many cases.</li>
<li>Even with a solution to the above problem, the full set of call sites is often unknown or even unknowable- consider a library being optimized before the application that uses it.</li>
</ul>
<p>So how does strict aliasing come into play here? It lets the compiler <em>assume</em> a lot of non-aliasing facts about these kinds of pointers. For example, because the program is never allowed to pass a function <code>void f(int *x, float *y) { ... }</code> aliasing <code>x</code> and <code>y</code> pointers, the optimizer doesn't have to do any (complicated, slow, imprecise) alias analysis of callers before processing its body. Consider this simple prefix scan:</p>
<pre class="lang-c prettyprint-override"><code>void scan(int *length, float *array) {
    float sum = 0.0f;
    for (int i = 0; i &lt; *length; i++) {
        sum += array[i];
        array[i] = sum;
    }
}
</code></pre>
<p>Clang 17 on x86-64 with -O1 generates this assembly, which loads <code>*length</code> once before the loop, saves in it <code>eax</code>, and then each iteration simply reuses <code>eax</code>. It can only do this because the program is not allowed to pass in a <code>length</code> that aliases with <code>array</code>- if that were allowed, writing to <code>array[i]</code> might change <code>*length</code> and thus the number of loop iterations:</p>
<pre><code>scan(int*, float*):
        mov     eax, dword ptr [rdi]          # save `*length` in eax
        test    eax, eax
        jle     .LBB0_3
        xorps   xmm0, xmm0
        xor     ecx, ecx
.LBB0_2:
        addss   xmm0, dword ptr [rsi + 4*rcx]
        movss   dword ptr [rsi + 4*rcx], xmm0 # write to `array[i]`
        inc     rcx
        cmp     rax, rcx                      # reuse eax
        jne     .LBB0_2
.LBB0_3:
        ret
</code></pre>
<p>If we change <code>array</code> and <code>sum</code> to <code>int</code>, Clang no longer performs this optimization, and <code>*length</code> is re-loaded every iteration:</p>
<pre><code>scan(int*, int*):
        cmp     dword ptr [rdi], 0
        jle     .LBB1_3
        xor     eax, eax
        xor     ecx, ecx
.LBB1_2:
        add     ecx, dword ptr [rsi + 4*rax]
        mov     dword ptr [rsi + 4*rax], ecx # write to `array[i]`
        inc     rax
        movsxd  rdx, dword ptr [rdi]         # reload `*length` into rdx
        cmp     rax, rdx
        jl      .LBB1_2
.LBB1_3:
        ret
</code></pre>
<p>Language designers can certainly make different choices here, but the approach taken by strict aliasing has some nice properties:</p>
<ul>
<li>The rule is relatively simple to explain to programmers: &quot;don't access an object through incompatible pointer types.&quot; Carving out more exceptions makes it more difficult to tell if a program is following the rules.</li>
<li>The rule is completely independent of compiler implementation details. Changes to make the optimizer smarter don't affect the definition of the language, nor do programmers <em>have</em> to know anything about how the optimizer works just to write correct programs.</li>
<li>As discussed in <a href="https://langdev.stackexchange.com/q/2350/38">the question you linked</a>, the rule is fairly close to something programmers want to follow anyway. In the rare cases they need to break it, there are options that stick out as &quot;weird&quot; to the reader of the code.</li>
</ul>
<p>A couple of approaches taken by other languages:</p>
<ul>
<li>Fortran defines its (non-)aliasing rules not in terms of types but in terms of parameters, so it can also optimize (its equivalent of) the <code>int</code> prefix sum example above.</li>
<li>Rust distinguishes &quot;shared&quot; pointers, which can alias but must point to an immutable object, from &quot;unique&quot; pointers, which can point to a mutable object but must not alias. This lets it propagate more detailed non-aliasing information through the program, and enforce it at compile time.</li>
</ul>

CC BY-SA 4.0
2023-10-04T19:29:22.763Z
2023-10-05T16:19:43.770Z
2023-10-05T16:19:43.770Z
rpjohnst
CC BY-SA 4.0
2023-10-04T20:34:29.803Z
It's kind of strange that the optimization is dependent on the pointers being to different types. Isn't this what the `restrict` qualifier handles more generally?
Barmar
CC BY-SA 4.0
2023-10-04T20:35:42.363Z
Yes- `restrict` was added to cover the cases that the strict aliasing rules don't. But `restrict` requires the programmer to do a lot more work, and be a lot more vigilant, so compiler writers are unlikely to give up on strict aliasing anytime soon.
rpjohnst
CC BY-SA 4.0
2023-10-07T18:50:18.557Z
@Barmar: `restrict` isn't strictly *more* general, it's usable in different subset of possible (non)-aliasing cases.  e.g. if `int *a` and `int *b` never point to the same thing as each other, but could be pointing to the same object as `int *c`, you can't use `restrict` unless they're separated by scope, e.g. make `restrict` copies of the pointers for the purpose of a loop.  But if some of those pointers had different types, the different one would be known not to alias across types, even if it might alias with other pointers of the same type.  e.g. `foo(int *a, int *b, float *x, float *y)`.
Peter Cordes
CC BY-SA 4.0
2023-10-07T18:51:53.467Z
(With `-fno-strict-aliasing`, it would be legal if some of those float and int pointers were pointing into the same union, and the compiler couldn't re-order writes and reads on the assumption that the storage was disjoint.)
Peter Cordes
CC BY-SA 4.0
2023-10-09T18:23:04.227Z
@PeterCordes: A compiler could usefully allow most forms of type punning and still make such an assumption, while processing the code for `foo`, if nothing *within the text of that function* hinted at any means via which an `int*` and a `float*` might come to identify the same storage.  It seems dubious, however, that a compiler given `float *x` could *usefully* assume an expression like `*(unsigned*)x += 0x00800000;` will only be invoked if `x` actually holds the address of an object that's never accessed as a `float`, despite the type of `x` being `float*`.
supercat
CC BY-SA 4.0
2023-10-09T18:36:28.837Z
@supercat: Yeah, that's why modern GCC at least makes some effort to act that way (unlike when `-fstrict-aliasing` first became the default), even though not required to.  I don't know how reliable that is, and it's not guaranteed portable, so I recommend people never intentionally code that way, but at least it probably avoids breaking a lot of simple cases.  IIRC, GCC3 when strict-aliasing first became the default, would act totally naively even for `float x` and `*(unsigned *)&x` where it can even see the underlying object, not just a pointer.
Peter Cordes
CC BY-SA 4.0
2023-10-09T18:37:46.950Z
@supercat: But anyway, it might be hard to formalize your suggestion in a language standard, but I have to agree it seems like good quality-of-implementation behaviour.
Peter Cordes
CC BY-SA 4.0
2023-10-09T18:45:07.563Z
@PeterCordes: Well designed aliasing analysis will sometimes be able to prove that things are disjoint, sometimes be able to prove that they alias, and sometimes be unable to prove either.  The problem is that clang and gcc are designed to apply all optimizations they can't prove to be unsound, rather than only those they can prove to be sound.  It's too bad people didn't long ago respond to questions of whether the Standard would allow a compiler to make a broken "optimization" wasn't "It would allow a garbage quality implementation to do so.  Why--do you want to write one?"
supercat
CC BY-SA 4.0
2024-09-30T00:49:25.763Z
No, an arbitrary function `void f(int *x, float *y) { ... }` may have aliased `x` and `y` pointers. It depends on what the function does. For instance, you cannot deduce anything with `int f(int *x, float *y) { return x == (int *) y; }`. However, if you have accesses `*x` and `*y`, then the effective types at `x` and `y` will not match, so that the pointers are necessarily different.
vinc17
# What optimizations does the strict aliasing rule facilitate?
Posted by: username_0 on 2023-10-04
This question is tangentially related to: [Why is type reinterpretation considered highly problematic in many programming languages?](https://langdev.stackexchange.com/q/2350/38)

Regardless how 'problematic' type reinterpretation is, why do some languages such as C allow doing so through some avenues (`memcpy`, `union`) but not others such as pointer casts? A search reveals that the answer lies in 'optimization' but it remains unclear to me how to leaving the behavior constructs such as `int x; float y = *(float *)&x` undefined permits optimizations that would not be possible if the behavior were defined the same way as if one used a `union` or `memcpy()`.
**Tags:** types c undefined-behavior type-conversion
---
## 3 answers
### Answer by username_3 on 2023-10-04. Score: 29
The main thing the optimizer wants to know here is whether writes through one pointer might affect reads through other pointers or names.
When it knows for sure that two pointers are unrelated in this way (they do not ever alias) it can reorder those unrelated reads and writes for purposes like common subexpression elimination, loop-invariant code motion, or caching intermediate results in registers.
On the other hand, when pointers might alias, the optimizer becomes much more restricted in what it can do. It might be forced to re-load the same values every iteration of a loop, or re-compute the same values several times. If you've ever heard the folklore that Fortran is still faster than C, this sort of missed optimization is why people say that.
Keep in mind that the most important cases here involve pointers that are used across multiple functions. When analyzing uses of pointers that come from parameters or global variables, the compiler has much less information about data and control flow, for multiple reasons:

The function may called in many different places at many different times. Combining all this information would be complex, slow, and also (due to Rice's theorem) necessarily incomplete or imprecise in many cases.
Even with a solution to the above problem, the full set of call sites is often unknown or even unknowable- consider a library being optimized before the application that uses it.

So how does strict aliasing come into play here? It lets the compiler assume a lot of non-aliasing facts about these kinds of pointers. For example, because the program is never allowed to pass a function void f(int *x, float *y) { ... } aliasing x and y pointers, the optimizer doesn't have to do any (complicated, slow, imprecise) alias analysis of callers before processing its body. Consider this simple prefix scan:
void scan(int *length, float *array) {
    float sum = 0.0f;
    for (int i = 0; i < *length; i++) {
        sum += array[i];
        array[i] = sum;
    }
}

Clang 17 on x86-64 with -O1 generates this assembly, which loads *length once before the loop, saves in it eax, and then each iteration simply reuses eax. It can only do this because the program is not allowed to pass in a length that aliases with array- if that were allowed, writing to array[i] might change *length and thus the number of loop iterations:
scan(int*, float*):
        mov     eax, dword ptr [rdi]          # save `*length` in eax
        test    eax, eax
        jle     .LBB0_3
        xorps   xmm0, xmm0
        xor     ecx, ecx
.LBB0_2:
        addss   xmm0, dword ptr [rsi + 4*rcx]
        movss   dword ptr [rsi + 4*rcx], xmm0 # write to `array[i]`
        inc     rcx
        cmp     rax, rcx                      # reuse eax
        jne     .LBB0_2
.LBB0_3:
        ret

If we change array and sum to int, Clang no longer performs this optimization, and *length is re-loaded every iteration:
scan(int*, int*):
        cmp     dword ptr [rdi], 0
        jle     .LBB1_3
        xor     eax, eax
        xor     ecx, ecx
.LBB1_2:
        add     ecx, dword ptr [rsi + 4*rax]
        mov     dword ptr [rsi + 4*rax], ecx # write to `array[i]`
        inc     rax
        movsxd  rdx, dword ptr [rdi]         # reload `*length` into rdx
        cmp     rax, rdx
        jl      .LBB1_2
.LBB1_3:
        ret

Language designers can certainly make different choices here, but the approach taken by strict aliasing has some nice properties:

The rule is relatively simple to explain to programmers: "don't access an object through incompatible pointer types." Carving out more exceptions makes it more difficult to tell if a program is following the rules.
The rule is completely independent of compiler implementation details. Changes to make the optimizer smarter don't affect the definition of the language, nor do programmers have to know anything about how the optimizer works just to write correct programs.
As discussed in the question you linked, the rule is fairly close to something programmers want to follow anyway. In the rare cases they need to break it, there are options that stick out as "weird" to the reader of the code.

A couple of approaches taken by other languages:

Fortran defines its (non-)aliasing rules not in terms of types but in terms of parameters, so it can also optimize (its equivalent of) the int prefix sum example above.
Rust distinguishes "shared" pointers, which can alias but must point to an immutable object, from "unique" pointers, which can point to a mutable object but must not alias. This lets it propagate more detailed non-aliasing information through the program, and enforce it at compile time.
> **Comments:**
> **username_4** (2023-10-04): It's kind of strange that the optimization is dependent on the pointers being to different types. Isn't this what the `restrict` qualifier handles more generally?  
> **username_3** (2023-10-04): Yes- `restrict` was added to cover the cases that the strict aliasing rules don't. But `restrict` requires the programmer to do a lot more work, and be a lot more vigilant, so compiler writers are unlikely to give up on strict aliasing anytime soon.  
> **username_5** (2023-10-07): @Barmar: `restrict` isn't strictly *more* general, it's usable in different subset of possible (non)-aliasing cases.  e.g. if `int *a` and `int *b` never point to the same thing as each other, but could be pointing to the same object as `int *c`, you can't use `restrict` unless they're separated by scope, e.g. make `restrict` copies of the pointers for the purpose of a loop.  But if some of those pointers had different types, the different one would be known not to alias across types, even if it might alias with other pointers of the same type.  e.g. `foo(int *a, int *b, float *x, float *y)`.  
> **username_5** (2023-10-07): (With `-fno-strict-aliasing`, it would be legal if some of those float and int pointers were pointing into the same union, and the compiler couldn't re-order writes and reads on the assumption that the storage was disjoint.)  
> **username_1** (2023-10-09): @PeterCordes: A compiler could usefully allow most forms of type punning and still make such an assumption, while processing the code for `foo`, if nothing *within the text of that function* hinted at any means via which an `int*` and a `float*` might come to identify the same storage.  It seems dubious, however, that a compiler given `float *x` could *usefully* assume an expression like `*(unsigned*)x += 0x00800000;` will only be invoked if `x` actually holds the address of an object that's never accessed as a `float`, despite the type of `x` being `float*`.  
> **username_5** (2023-10-09): @supercat: Yeah, that's why modern GCC at least makes some effort to act that way (unlike when `-fstrict-aliasing` first became the default), even though not required to.  I don't know how reliable that is, and it's not guaranteed portable, so I recommend people never intentionally code that way, but at least it probably avoids breaking a lot of simple cases.  IIRC, GCC3 when strict-aliasing first became the default, would act totally naively even for `float x` and `*(unsigned *)&x` where it can even see the underlying object, not just a pointer.  
> **username_5** (2023-10-09): @supercat: But anyway, it might be hard to formalize your suggestion in a language standard, but I have to agree it seems like good quality-of-implementation behaviour.  
> **username_1** (2023-10-09): @PeterCordes: Well designed aliasing analysis will sometimes be able to prove that things are disjoint, sometimes be able to prove that they alias, and sometimes be unable to prove either.  The problem is that clang and gcc are designed to apply all optimizations they can't prove to be unsound, rather than only those they can prove to be sound.  It's too bad people didn't long ago respond to questions of whether the Standard would allow a compiler to make a broken "optimization" wasn't "It would allow a garbage quality implementation to do so.  Why--do you want to write one?"  
> **username_6** (2024-09-30): No, an arbitrary function `void f(int *x, float *y) { ... }` may have aliased `x` and `y` pointers. It depends on what the function does. For instance, you cannot deduce anything with `int f(int *x, float *y) { return x == (int *) y; }`. However, if you have accesses `*x` and `*y`, then the effective types at `x` and `y` will not match, so that the pointers are necessarily different.  
---
### Answer by username_2 on 2023-10-05. Score: 8
### Aliasing

As mentioned by @rpjohnst's [answer](https://langdev.stackexchange.com/a/3000/874), the definite absence of aliasing is the optimization enabler. That is, a number of optimizations are only sound -- ie, do not alter the semantics -- if the pointers they operate on can be proven NOT to alias.

*How* the alias analysis which allows deducing this absence of aliasing is conducted has no impact on the optimizations being enabled, only on which pointers can be proven not to alias.

### Alias Analysis

Proving the absence of aliasing, however, is hard. It's related to the Escape Analysis problem faced by the compilers of GC languages, and can in fact be arbitrarily hard.

Since the optimizations enabled by proving the absence of aliasing are so important, it therefore makes sense to "simplify" alias analysis by introducing rules in the language:

* Fortran requires function parameters not to alias.
* C and C++ have the strict aliasing rule.
* C (since C99) has the `restrict` keyword, and many C++ compilers have the `__restrict` extension.
* Rust has `&T` vs `&mut T`.

### Why not Strict Aliasing?

Strict Aliasing is one of the early set of rules decided on to simplify alias analysis, and implemented in C and C++. It unfortunately suffers from a number of issues which later sets of rules have attempted to improve on.

Strict Aliasing, while conceptually simple and intuitive, is plagued by exceptions and accidental collisions:

* **Accidental Collisions**: as demonstrated in @rpjohnst's [answer](https://langdev.stackexchange.com/a/3000/874) a function taking two parameters -- a length `int*` and an array `T*` -- will not be as well optimized when it turns out that `T = int`, because suddenly Strict Aliasing cannot exclude the possibility of aliasing. This is made even worse by the fact that `int32_t` is just a typedef of `int` on most platforms, so that even types that are not spelled the same may "accidentally" be the same and foil Strict Aliasing.
* **Exceptions**: exceptions are carved into the rule for `char*` and `unsigned char*`, allowing those to aliasing everything. It's a useful exception... but also a *very* unfortunate one seeing as `char*` is a widely used pointer type whenever one uses strings or raw memory.

C99 introduced the `restrict` qualifier for much more fine-grained annotations, which do not fall apart by accident.
---
### Answer by username_1 on 2023-10-04. Score: 6
The issue is discussed explicitly in the C99 Rationale at https://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf starting on page 59, line 33, and continuing through the top of line 61.  The basic idea is that given a function like [from the Rationale]:
int a;
void f( double * b )
{
  a = 1;
  *b = 2.0;
  g(a);
}

it's likely that a compiler's customers wouldn't mind if the compiler were to replace the call g(a) with a call to g(1).  There are some contrived scenarios where that substitution would be (as noted in the Rationale) "incorrect", but the view of the Committee was that if a compiler's customers wouldn't demand that a compiler accommodate such possibilities, the Committee shouldn't either.  Note that because compilers would always be allowed to access such constructs "in a documented manner characteristic of the environment", any compilers whose customers would care about such constructs remained free to process them in the "correct" manner.  Such code may not be portable, but as observed on page 13, in discussing conformance, "A strictly conforming program is another term for a maximally portable program.  The goal is to give the programmer a fighting chance to make powerful C programs that are also highly portable, without seeming to demean perfectly useful C programs that happen not to be portable, thus the adverb strictly."
Note that the example shows a relatively non-controversial and safe kind of optimization: saying that if storage is accessed as type T and later read again as type T, and if no intervening actions have any visible relationship to anything having to do with type T, a compiler may replace the load implied by the second operation with code that re-uses the previous value read or written.  This optimization is simple but effective and relatively safe, and will be compatible even with most code for which clang and gcc would require -fno-strict-aliasing, provided only that a compiler makes a good faith effort to notice when intervening actions have a visible relationship to the type involved.
Note that because a function like:
unsigned get_float_bits(float *fp)
{
  return *(unsigned*)fp;
}

would be non-portable, the Standard makes no attempt to exercise jurisdiction over how compilers treat it.  It was obvious to just about everyone when the Standard was written that anyone wanting to sell compilers to anyone who would find such constructs useful should process such constructs "in a documented manner characteristic of the environment" with or without a mandate, and thus there was no need for the Standard to recognize such constructs.
Note that the authors of the Standard get unfairly maligned for the Standard's failure to forbid nonsensical compiler behavior, as though they intended to invite such nonsense.  The Standard was a compromise between people who wanted to recognize non-portable constructs as legitimate, and those who wanted to forbid anything that wasn't portable; the solution was to have the Standard waive jurisdiction over such constructs.  If waiver of jurisdiction is recognized as implying nothing about how implementations process constructs that are intended to perform tasks in non-portable fashion, the Standard's categorization of constructs that are useful but non-portable as Undefined Behavior is entirely appropriate.
---
