GCC和相同类型的数组之间的严格别名

Pas*_*uoq 17 c gcc strict-aliasing

上下文

以GCC优化命名的"严格别名"是编译器假设内存中的值不会通过类型的左值("声明的类型")访问,该值与写入的值的类型非常不同( "有效型").如果必须考虑写入指针float可以修改类型的全局变量,则该假设允许代码转换是不正确的int.

GCC和Clang都是从充满暗角的标准描述中提取出最多的含义,并且在实践中对生成代码的性能有偏见,假设指向a的int第一个成员的struct thing指针不会将指向第int一个成员的指针作为别名一个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;
}
Run Code Online (Sandbox Code Playgroud)

GCC和Clang的推断该函数总是返回1,也就是说,pq不能为同一个内存位置的别名:

e:
        movl    $1, (%rdi)
        movl    $1, %eax
        movl    $2, (%rsi)
        ret
Run Code Online (Sandbox Code Playgroud)

只要有人同意这种优化的推理,就不足为奇了,p->t[3]并且q->t[2]在下面的代码片段中也假设是不相交的左值(或者更确切地说,如果它们别名,调用者会导致UB):

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];
}
Run Code Online (Sandbox Code Playgroud)

GCC优化了上述功能h:

h:
        movl    $1, 12(%rdi)
        movl    $1, %eax
        movl    $2, 8(%rsi)
        ret
Run Code Online (Sandbox Code Playgroud)

到目前为止,只要有人看到p->ap->t[3]以某种方式访问​​整个struct thing(相应struct arr),就有可能认为制作位置别名会破坏6.5:6-7中规定的规则.这是GCC方法的一个论点是这条消息,它是一个长线程的一部分,它也讨论了联盟在严格别名规则中的作用.

但是,我怀疑下面的例子,其中没有struct:

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

GCC版本4.4.7通过Matt Godbolt的有用网站上的当前版本7快照优化函数g,好像(*p)[3](*q)[4]不能别名(或者更确切地说,好像程序已调用UB,如果他们这样做):

g:
        movl    $1, 12(%rdi)
        movl    $1, %eax
        movl    $2, 16(%rsi)
        ret
Run Code Online (Sandbox Code Playgroud)

是否有任何标准读数证明这种非常严格的严格混叠方法是正确的?如果这里GCC的优化可以是合理的,会的论点同样适用于功能的优化fk,这是不是由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];
}
Run Code Online (Sandbox Code Playgroud)

我愿意与海湾合作委员会的开发者利用这个,但我应该先决定没有我的报告功能的正确性错误g或遗漏的优化fk.

Ser*_*sta 5

恕我直言,该标准不允许确定大小的数组同时重叠(*).n1570草案在6.2.7兼容类型和复合类型(强调我的)中说:

§2所有涉及同一对象或功能的声明均应具有兼容类型; 否则,行为未定义.

§3复合类型可以由两种兼容的类型构成; 它是一种兼容两种类型并满足以下条件的类型:

  • 如果两种类型都是数组类型,则应用以下规则:
    • 如果一个类型是已知常量大小数组,则复合类型是该大小的数组.
      ...

由于对象的存储值只能通过具有兼容类型的左值表达式(6.5表达式§7的简化读取)访问,因此不能对不同大小的数组进行别名,也不能使具有相同大小的数组重叠.因此,在函数g中,p和q应该指向相同的数组或非重叠的数组,这允许优化.

对于函数f和k,我的理解是,根据标准允许优化,但开发人员尚未实现.我们必须记住,只要其中一个参数是一个简单的指针,就可以指向另一个数组的任何元素,并且不会发生任何优化.所以我认为缺乏优化只是UB着名规则的一个例子:任何事情都可能发生,包括预期的结果.

  • 您可以说数组类型的左值不能访问数组。当在涉及访问的任何上下文中使用时,这样的左值都会衰减到指针。例如`(* p)[3] = 1;`使用类型为`int`的左值访问`int`,它不访问整个数组。 (2认同)

dav*_*mac 5

在:

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

*p并且*q是数组类型的左值; 如果它们可能重叠,则对它们的访问受第6.5节第7段(所谓的"严格别名规则")的约束.但是,由于它们的类型相同,因此不会对此代码造成问题.然而,对于对该问题给出全面答案所需的一些相关问题,该标准非常模糊,例如:

  • 不要(*p)(*q)实际必要对"访问"(如该术语6.5p7使用)向它们指向的数组?如果他们不这样做,这是很有诱惑力采取认为,表情(*p)[3](*q)[4]基本降解为指针两个算术和反引用int *S的可以清楚的别名.(这不是一个完全不合理的观点; 6.5.2.1 Array Subscripting表示其中一个表达式应具有类型''指向完整对象类型'的指针,另一个表达式应具有整数类型,结果具有类型''类型'' - 所以数组左值必须按照通常的转换规则降级为指针;唯一的问题是在转换发生之前是否访问了数组.

  • 但是,为了保护(*p)[3]纯粹等同于的视图*((int *)p + 3),我们必须证明(*p)[3]不需要评估(*p),或者如果确实如此,则访问不具有未定义的行为(或定义但不需要的行为).我不认为标准的准确措辞有任何理由允许(*p)不对其进行评估; 这意味着(*p)如果(*p)[3]定义了行为,则表达式不得具有未定义的行为.所以,真正的问题归结为是否*p*q他们是否指的是同一类型的部分重叠的阵列,而事实上是否有可能即它们可以同时做到这一点已经定义的行为.

对于*运营商的定义,标准说:

如果它指向一个对象,则结果是指定该对象的左值

  • 这是否意味着指针必须指向对象的开头?(这似乎意味着这就是意思).在访问对象之前是否必须以某种方式建立对象(并确定对象是否会解除任何重叠对象)?如果两者都是这种情况,*p并且*q不能重叠 - 因为建立任何一个对象都会使另一个对象无效 - 因此(*p)[3]并且(*q)[4]不能使用别名.

问题是对这些问题没有适当的指导.在我看来,应采取保守的方法:不要认为这种混叠是合法的.

特别是,6.5中的"有效类型"措辞提出了一种可以建立特定类型的对象的方法.这似乎是一个很好的选择,这是明确的; 也就是说,您不能通过设置其有效类型(包括通过具有声明类型的方式)来建立对象,并且限制其他类型的访问; 此外,建立一个对象取消建立任何现有的重叠对象(要清楚,这是外推,而不是实际的措辞).因此,如果(*p)[3]并且(*q)[4]可以别名,则指向pq不指向对象,因此其中之一*p*q具有未定义的行为.