首页 文章

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

提问于
浏览
17

上下文

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

GCC和Clang都在a standard description full of dark corners中提取了最多的含义,并且在实践中对生成的代码的性能存在偏差,假设指向 struct thing 的第一个成员的 int 指针没有别名指向 int 的第一个成员的 int

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;
}

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

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

只要有人同意这种优化的推理,那么 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];
}

GCC优化上述函数 h

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

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

问题

但是,我怀疑以下示例,其中没有 struct

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

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

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

是否有任何标准读数证明这种非常严格的严格混叠方法是正确的?如果GCC的优化在这里是合理的,那么这些参数是否也适用于未经GCC优化的函数 fk 的优化?

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];
}

我愿意与GCC开发人员讨论这个问题,但我应该首先做出决定,而不是报告函数 g 的正确性错误或 fk 错过优化 .

2 回答

  • 5

    在:

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

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

    • (*p)(*q) 实际上需要"access"(因为术语在6.5p7中使用)到它们所指向的数组?如果他们不这样做,那么很容易认为表达式 (*p)[3](*q)[4] 本质上降级为指针算术和两个 int * 的解除引用,这两个可以清楚地别名 . (这不是一个完全不合理的立场; 6.5.2.1 Array Subscripting 说其中一个表达式应该有''指向完整对象类型的'',另一个表达式应该有整数类型,结果类型为''type'' - 所以根据通常的转换规则,数组左值必然降级为指针;唯一的问题是在转换发生之前是否访问了数组 .

    • 但是,为了捍卫 (*p)[3] 纯粹等同于 *((int *)p + 3) 的观点,我们必须证明 (*p)[3] 不需要评估 (*p) ,或者如果确实如此,则访问没有未定义的行为(或定义但不需要的行为) . 我不认为标准的准确措辞有任何理由允许 (*p) 未被评估;这意味着如果定义了 (*p)[3] 的行为,则表达式 (*p) 必须不具有未定义的行为 . 所以,问题实际上归结为 *p*q 是否已经定义了行为,如果它们引用了相同类型的部分重叠数组,实际上它们是否可能同时这样做 .

    对于 * 运算符的定义,标准说:

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

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

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

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

  • 5

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

    §2所有引用同一对象或函数的声明都应具有兼容类型;否则,行为未定义 . §3复合类型可以由两种兼容的类型构成;它是一种兼容两种类型并满足以下条件的类型:如果两种类型都是数组类型,则应用以下规则:如果一种类型是已知常量大小的数组,则复合类型是一个数组那个大小 . ...

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

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

相关问题