Pascal Cuoq Pascal Cuoq - 18 days ago 5
C Question

GCC and strict aliasing between arrays of a same type

Context



“Strict aliasing”, named after the GCC optimization, is an assumption by the compiler that a value in memory will not be accessed through an lvalue of a type (the “declared type”) very different from the type the value was written with (the “effective type”). This assumption allows code transformations that would be incorrect if the possibility had to be taken into account that writing to a pointer to
float
could modify a global variable of type
int
.

Both GCC and Clang, extracting the most meaning out of a standard description full of dark corners, and having a bias for performance of generated code in practice, assume that a pointer to the
int
first member of a
struct thing
does not alias a pointer to the
int
first member of a
struct object
:

struct thing { int a; };
struct object { int a; };

int e(struct thing *p, struct object *q) {
p->a = 1;
q->a = 2;
return p->a;
}


Both GCC and Clang infer that the function always return 1, that is, that
p
and
q
cannot be aliases for the same memory location:

e:
movl $1, (%rdi)
movl $1, %eax
movl $2, (%rsi)
ret


As long as one agrees with the reasoning for this optimization, it should be no surprise that
p->t[3]
and
q->t[2]
are also assumed to be disjoint lvalues in the following snippet (or rather, that the caller causes UB if they alias):

struct arr { int t[10]; };

int h(struct arr *p, struct arr *q) {
p->t[3] = 1;
q->t[2] = 2;
return p->t[3];
}


GCC optimizes the above function
h
:

h:
movl $1, 12(%rdi)
movl $1, %eax
movl $2, 8(%rsi)
ret


So far so good, as long as one sees
p->a
or
p->t[3]
as somehow accessing a whole
struct thing
(resp.
struct arr
), it is possible to argue that making the locations alias would break the rules laid out in 6.5:6-7. An argument that this is GCC's approach is this message, part of a long thread that also discussed the role of unions in strict aliasing rules.

Question



I have doubts, however, about the following example, in which there is no
struct
:

int g(int (*p)[10], int (*q)[10]) {
(*p)[3] = 1;
(*q)[4] = 2;
return (*p)[3];
}


GCC versions 4.4.7 through the current version 7 snapshot on Matt Godbolt's useful website optimize function
g
as if
(*p)[3]
and
(*q)[4]
could not alias (or rather, as if the program had invoked UB if they did):

g:
movl $1, 12(%rdi)
movl $1, %eax
movl $2, 16(%rsi)
ret


Is there any reading of the standard that justifies this very strict approach to strict aliasing? If GCC's optimization here can be justified, would the arguments apply as well to the optimization of functions
f
and
k
, which are not optimized by GCC?

int f(int (*p)[10], int (*q)[9]) {
(*p)[3] = 1;
(*q)[3] = 2;
return (*p)[3];
}

int k(int (*p)[10], int (*q)[9]) {
(*p)[3] = 1;
(*q)[2] = 2;
return (*p)[3];
}


I'm willing to take this up with the GCC devs, but I should first decide without I am reporting a correctness bug for function
g
or a missed optimization for
f
and
k
.

Answer

In:

int g(int (*p)[10], int (*q)[10]) {
  (*p)[3] = 1;
  (*q)[4] = 2;
  return (*p)[3];
}

*p and *q are lvalues of array type; If they may overlap, access to them is governed by section 6.5 paragraph 7 (the so called "strict aliasing rule"). However, since their type is the same, that does not present a problem for this code. The standard is however remarkably vague regarding a number of relevant concerns that would be required to give a comprehensive answer to this question, such as:

  • Do (*p) and (*q) actually necessitate "access" (as the term is used in 6.5p7) to the arrays to which they point? If they do not, the expressions (*p)[3] and (*q)[4] essentially degrade to pointer arithmetic and dereference of two int *s which can clearly alias. (This isn't an entirely unreasonable standpoint; 6.5.2.1 Array Subscripting says that One of the expressions shall have type ‘‘pointer to complete object type’’, the other expression shall have integer type, and the result has type ‘‘type’’ - so the array lvalue has necessarily degraded to a pointer as per usual conversion rules; the only question is whether the array was accessed before the conversion occurred).

  • If (*p) implies access: Does it require that an object of the appropriate type (i.e. int [10]) has already been established at the (precise) location addressed by p?

For the defintion of the * operator, the standard says:

if it points to an object, the result is an lvalue designating the object

  • does this mean that the pointer must point to the start of the object? Does the object have to have been established somehow before it can be accessed? If both are the case, *p and *q cannot overlap and so (*p)[3] and (*q)[4] cannot alias.

The problem is that there is no suitable guidance on these questions. In my view, a conservative approach should be taken: do not assume that this kind of aliasing is legal.

In particular, the "effective type" wording in 6.5 suggests a means by which an object of a particular type can be established. It seems like a good bet that this is intended to be definitive; that is, that you cannot establish an object other than by setting its effective type (including by means of it having a declared type), and that access by other types is restricted.

Comments