I believe 6.5p7 in the C standard defines the so-called strict aliasing rule as follows.
An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
- a type compatible with the effective type of the object,
- a qualified version of a type compatible with the effective type of the object,
- a type that is the signed or unsigned type corresponding to the effective type of the object,
- a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
- an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
- a character type.
Here's a simple example that shows GCC's optimization based on its assumption to the rule.
int IF(int *i, float *f) {
*i = -1;
*f = 0;
return *i;
}
IF:
mov DWORD PTR [rdi], -1
mov eax, -1
mov DWORD PTR [rsi], 0x00000000
ret
The load for return *i
is omitted assuming that int
and float
cannot alias.
Then let's consider case 6, where it says an object could be accessed by a character type lvalue expression (char *
).
int IC(int *i, char *c) {
*i = -1;
*c = 0;
return *i;
}
IC:
mov DWORD PTR [rdi], -1
mov BYTE PTR [rsi], 0
mov eax, DWORD PTR [rdi]
ret
Now there is a load for return *i
because i
and c
could overlap according to the rules, and *c = 0
could change what's in *i
.
Then can we also modify a char
through an int *
? Should the compiler care that such thing might happen?
char CI(char *c, int *i) {
*c = -1;
*i = 0;
return *c;
}
CI: #GCC
mov BYTE PTR [rdi], -1
mov DWORD PTR [rsi], 0
movzx eax, BYTE PTR [rdi]
ret
CI: #Clang
mov byte ptr [rdi], -1
mov dword ptr [rsi], 0
mov al, byte ptr [rdi]
ret
Looking at the assembly output, both GCC and Clang seem to think a char
can be modified by access through int *
.
Maybe it's obvious that A
and B
overlapping means A
overlaps B
and B
overlaps A
. However, I found this detailed answer which emphasizes in boldface that,
Note that
may_alias
, like thechar*
aliasing rule, only goes one way: it is not guaranteed to be safe to useint32_t*
to read a__m256
. It might not even be safe to usefloat*
to read a__m256
. Just like it's not safe to dochar buf[1024]; int *p = (int*)buf;
.
Now I got really confused. The answer is also about GCC vector types, which has an may_alias
attribute so it can alias similarly as a char
.
At least, in the following example, GCC seems to think overlapping access can happen in both ways.
int IV(int *i, __m128i *v) {
*i = -1;
*v = _mm_setzero_si128();
return *i;
}
__m128i VI(int *i, __m128i *v) {
*v = _mm_set1_epi32(-1);
*i = 0;
return *v;
}
IV:
pxor xmm0, xmm0
mov DWORD PTR [rdi], -1
movaps XMMWORD PTR [rsi], xmm0
mov eax, DWORD PTR [rdi]
ret
VI:
pcmpeqd xmm0, xmm0
movaps XMMWORD PTR [rsi], xmm0
mov DWORD PTR [rdi], 0
movdqa xmm0, XMMWORD PTR [rsi]
ret
https://godbolt.org/z/ab5EMx3bb
But am I missing something? Is strict aliasing one-way?
Additionally, after reading the current answers and comments, I thought maybe this code is not allowed by the standard.
typedef struct {int i;} S;
S s;
int *p = (int *)&s;
*p = 1;
Note that (int *)&s
is different from &s.i
. My current interpretation is that an object of type S
is being accessed by an lvalue expression of type int
, and this case is not listed in 6.5p7.
int *
into an actual__m256i
object, like GCC AVX _m256i cast to int array leads to wrong values. But you're using a__m128i *
pointer to point at memory that's allowed to be a different underlying type. Note that what you quoted from my answer gave achar buf[1024]
example, a char-array object, nochar*
involved. (Accessing it may involvechar*
due to howbuff[i]
works as*(buff+i)
, so that may be safer, unlike __m128i) – Absorbefacientstruct {int i;} s = {1}; *(int *)&s = 0;
also possibly break? I knows
ands.i
must be in the same memory location if it is in memory, but the rules say it's only possible to access anint
throughstruct {int i;} *
, and not the other way. – Wizardly*(int *)&s = 0;
is definitely fine. Theint
member of a struct is anint
object, so that's allowed by rule (1) in your quote. – Absorbefacientint i = 1;
/*(struct s*)&i = {0};
I think that's what (5) is allowing, so unless there's a separate problem in the pointer casting, that may work. It may also work to point astruct{int i[2];};
at something declared asint arr[2]
, but that feels even weirder. – Absorbefacientint
member is anint
object, but that's different from astruct {int i;}
object. By*(int *)&s = 0;
, you are accessing astruct {int i;}
throughint *
. Not sure if that's okay. – Wizardlyint i
struct member is anint
object. That's what makes it safe to pass&s.i
to things that want anint*
, and why that doesn't need any casting. The struct object and int member fully overlap in this case. – Absorbefacient*(int *)&s = 0;
is the same astypedef struct {int i;} S; S s; int *p = (int *)&s; *p = 0
. See that(int *)&s
is different from&s.i
. It's kind of an artificial example, but I want a clear understanding of the rules. – Wizardlyint *p = (int *)&s; *p = 0;
, an object of typeS
is being accessed by an lvalue expression of typeint
. – Wizardlyint*
by casting astruct*
, it's not strict-aliasing. C defines enough about how addresses and memory works that there definitely is anint
object in there somewhere, at some address between&s
and((char*)&s) + sizeof(s) - sizeof(int)
. If your implementation doesn't put padding before the firstint
member, then it's correct. (I think padding might be allowed, but on implementations that choose not to do that, I'm pretty sure everything is well-defined behaviour even in pure ISO C.) – Absorbefacientoffsetof
is a thing and is implementable and usable. – Absorbefacient__m256i v = ...; int *p = (int *)&v;
wouldn't be any problem because&v
is clearly assigned top
. But as you know the compiler lets garbage to be loaded. – Wizardly__m256i v
doesn't have anint
member sub-object at that address, so point (1) of the strict-aliasing rule doesn't apply. Of course you have to respect strict-aliasing as well as other rules for deriving pointers. – Absorbefacientmay_alias
attribute for vector types imply that a vector object can exist in any contiguous array of any type? In the way that there are 4char
's existing in a 32-bitint
but anint
doesn't exist in an array of 4char
s? I think this is the point you were trying to explain? – Wizardlyint arr[256]
as also being composed ofchar
or__m256i
objects. Just that you can use pointers of that type to access the bytes of other objects. (The "object-representation"). Including astruct { char c; short s[7];}
including padding. The mental model you suggest could I guess work. It gets hairy when you consider a__attribute__((aligned(1),may_alias))
type (like you might use as an alternative tomemcpy
to do an unaligned aliasing-safe load or store ofuint32_t
to any offset of a char array). So there are overlapping objects... – Absorbefacient__attribute__((aligned(1), may_alias)) uint32_t
(u32
), there'd be au32
in0:3
,1:4
,2:5
, and so on. Anyway in my personal opinion, strict aliasing doesn't help much unless someone decides to write Java in C (GTK?). But such codebases won't be performance critical, and performance critical cases like OS or SIMD optimized computation often half-ignore aliasing. – Wizardly__m128i v; (int *)&v
and(long *)&v
breaks while(long long *)&v
works. So, if you're trying to access a vector oflong long
objects byint *
, that's clearly not allowed. However, it seems GCC treatschar
very specially thatchar ca[16]; (int *)&ca;
doesn't break in a similar use case, even with unaligned access. See the last two functions. – Wizardlylong long*
case is interesting, and maybe isn't a coincidence that a type matching the vector works as expected, including doing all loads first before either call. It might be interesting to test that withtypedef short v8si __attribute__((vector_size(16)));
and see ifshort*
is the only type that works as expected with it. – Absorbefacient