是否错误地指定了严格别名规则?

时间:2016-08-05 21:42:59

标签: c language-lawyer strict-aliasing

作为previously established,形式为

的联合
union some_union {
    type_a member_a;
    type_b member_b;
    ...
};

n 成员在重叠存储中包含 n + 1个对象:联合本身的一个对象和每个联合成员的一个对象。很明显,您可以按任何顺序自由地读取和写入任何工会成员,即使读取的工会成员不是最后写入的工会成员。严格别名规则永远不会被违反,因为您访问存储的左值具有正确的有效类型。

这是脚注95的further supported,它解释了类型双关语是否是联盟的预期用途。

严格别名规则启用的优化的典型示例是此函数:

int strict_aliasing_example(int *i, float *f)
{
    *i = 1;
    *f = 1.0;
    return (*i);
}

编译器可以优化为

int strict_aliasing_example(int *i, float *f)
{
    *i = 1;
    *f = 1.0;
    return (1);
}

因为它可以安全地假设写入*f不会影响*i的值。

然而,当我们将两个指针传递给同一个联盟的成员时会发生什么?考虑这个例子,假设一个典型的平台,其中float是IEEE 754单精度浮点数,int是32位二进制补码整数:

int breaking_example(void)
{
    union {
        int i;
        float f;
    } fi;

    return (strict_aliasing_example(&fi.i, &fi.f));
}

如前所述,fi.ifi.f指的是重叠的内存区域。阅读和写作是无条件的合法(一旦工会初始化,写作只是合法的)。在我看来,所有主要编译器执行的先前讨论的优化产生了错误的代码,因为不同类型的两个指针合法地指向相同的位置。

我不知道我对严格别名规则的解释是否正确。由于前面提到的拐角情况,严格混叠的优化设计似乎不合理。

请告诉我为什么我错了。

研究期间出现related question

在添加您自己的答案及其评论之前,请先阅读所有答案及其评论,以确保您的答案添加了新的参数。

8 个答案:

答案 0 :(得分:12)

根据§6.5.2.3中的工会成员定义:

  

3后缀表达式后跟.运算符,标识符指定结构或联合对象的成员。 ...

     

4后缀表达式后跟->运算符,标识符指定结构或联合对象的成员。 ...

另见§6.2.3¶1:

  
      
  • 结构或工会的成员;每个结构或联合为其成员都有一个单独的名称空间(通过.->运算符用于访问该成员的表达式的类型消除歧义;
  •   

很明显,脚注95指的是工会成员在范围内使用.->运算符访问工会成员。

由于对包含联合的字节的赋值和访问不是通过联合成员而是通过指针进行的,因此您的程序不会调用联合成员的别名规则(包括脚注95所阐明的那些)。

此外,由于*f = 1.0之后的对象的有效类型为float,因此违反了正常的别名规则,但其存储的值由int类型的左值访问(参见§6.5) ¶7)。

注意:所有引用都引用this C11标准草案。

答案 1 :(得分:5)

C11标准(§6.5.2.3.9例3)有以下例子:

  

以下不是有效的片段(因为联合类型不是   在函数f)中可见:

 struct t1 { int m; };
 struct t2 { int m; };
 int f(struct t1 *p1, struct t2 *p2)
 {
       if (p1->m < 0)
               p2->m = -p2->m;
       return p1->m;
 }
 int g()
 {
       union {
               struct t1 s1;
               struct t2 s2;
       } u;
       /* ... */
       return f(&u.s1, &u.s2);
 }

但我无法对此发现更多澄清。

答案 2 :(得分:5)

严格别名规则禁止通过两个没有兼容类型的指针访问同一个对象,除非一个是指向字符类型的指针:

  

7对象的存储值只能由具有以下类型之一的左值表达式访问:88)

     
      
  • 与对象的有效类型兼容的类型
  •   
  • 与对象的有效类型兼容的类型的限定版本,
  •   
  • 与对象的有效类型对应的有符号或无符号类型
  •   
  • 与对象的有效类型的限定版本对应的有符号或无符号类型的类型,
  •   
  • 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),或
  •   
  • 字符类型。
  •   

在您的示例中,*f = 1.0;正在修改fi.i,但类型不兼容。

我认为错误在于认为联合包含 n 对象,其中 n 是成员数。在程序执行期间,§6.7.2.1¶16

中,union在任何时候只包含一个活动对象
  

最多一个成员的值可以随时存储在一个union对象中。

支持这种解释,即联合不同时包含其所有成员对象,可以在§6.5.2.3中找到:

  

如果联合对象当前包含其中一个结构

最后,2006年defect report 236提出了一个几乎相同的问题。

  

示例2

// optimization opportunities if "qi" does not alias "qd"
void f(int *qi, double *qd) {
    int i = *qi + 2;
    *qd = 3.1;       // hoist this assignment to top of function???
    *qd *= i;
    return;
}  

main() {
    union tag {
        int mi;
        double md;
    } u;
    u.mi = 7;
    f(&u.mi, &u.md);
}
     

委员会认为示例2违反了6.5中的别名规则   第7段:

     

“包含上述之一的聚合或联合类型   其成员之间的类型(包括,递归地,成员   subaggregate或contains union)。“

     

为了不违反规则,示例中的函数f应为   写作:

union tag {
    int mi;
    double md;
} u;

void f(int *qi, double *qd) {
    int i = *qi + 2;
    u.md = 3.1;   // union type must be used when changing effective type
    *qd *= i;
    return;
}

答案 3 :(得分:4)

本质上,严格别名规则描述了允许编译器假定(或者,相反地,不允许假设)两个不同类型的指针不指向内存中相同位置的情况。

在此基础上,允许您在strict_aliasing_example()中描述的优化,因为允许编译器假定fi指向不同的地址。

breaking_example()导致传递给strict_aliasing_example()的两个指针指向同一个地址。这打破了允许strict_aliasing_example()允许的假设,因此导致该函数表现出不确定的行为。

因此,您描述的编译器行为是有效的。事实是breaking_example()导致传递给strict_aliasing_example()的指针指向导致未定义行为的同一地址 - 换句话说,breaking_example()打破了允许编译器生成的假设在strict_aliasing_example()内。

答案 4 :(得分:3)

让我们暂时退出标准,并考虑编译器实际可行的内容。

假设strict_aliasing_example()中定义了strict_aliasing_example.cbreaking_example()中定义了breaking_example.c。假设这两个文件都是单独编译然后链接在一起的,如下所示:

gcc -c -o strict_aliasing_example.o strict_aliasing_example.c
gcc -c -o breaking_example.o breaking_example.c
gcc -o breaking_example strict_aliasing_example.o breaking_example.o

当然,我们必须向breaking_example.c添加一个函数原型,如下所示:

int strict_aliasing_example(int *i, float *f);

现在考虑gcc的前两次调用是完全独立的,除了函数原型之外不能共享信息。编译器在生成i的代码时,不可能知道jstrict_aliasing_example()将指向同一联盟的成员。链接或类型系统中没有任何内容可以指定这些指针在某种程度上是特殊的,因为它们来自一个联合。

这支持其他答案提到的结论:从标准的角度来看,与解除引用任意指针相比,通过.->访问联合服从不同的别名规则。

答案 5 :(得分:3)

在C89标准之前,绝大多数实现将写入解除引用的行为定义为特定类型的指针,因为以为该类型定义的方式设置底层存储的位,并定义了读取的行为。取消引用特定类型的指针,以按照为该类型定义的方式读取底层存储的位。虽然这些能力对于所有实现都不是有用的,但是存在许多实现,其中热循环的性能可以通过例如大大提高来实现。使用32位加载和存储一次操作四个字节的组。此外,在许多这样的实现中,支持这样的行为并没有花费任何成本。

C89标准的作者声明他们的目标之一是避免无可挽回地破坏现有代码,并且有两种基本方式可以解释规则与此一致:

  1. C89规则可能只适用于类似于基本原理中给出的情况(通过该类型直接访问具有声明类型的对象并通过指针间接访问),以及编译器没有理由期望左值相关。跟踪每个变量是否当前缓存在寄存器中非常简单,并且能够在访问其他类型的指针时将这些变量保存在寄存器中是一种简单而有用的优化,并且不会妨碍对使用更常见的代码的支持类型惩罚模式(让编译器将float*解释为int*强制转换,因为需要刷新任何寄存器缓存的float值是简单而直接的;这样的强制转换很少见到这种方法不太可能对业绩产生不利影响。)

  2. 鉴于标准对于为给定平台提供高质量实现的内容通常是不可知的,规则可以被解释为允许实现破坏使用类型双关的代码,这两种方式都是有用的而且很明显,没有暗示优质的实施不应该试图避免这样做。

  3. 如果标准定义了允许就地类型双关语的实用方法,这种方式与其他方法相比没有任何明显劣势,那么除了定义方式之外的方法可能被合理地视为已弃用。如果不存在标准定义的方法,那么为了获得良好性能而需要类型惩罚的平台的质量实现应该努力有效地支持这些平台上的常见模式,无论标准是否要求它们这样做。

    不幸的是,标准要求的不明确导致了一些人认为不存在替代品的弃用结构的情况。存在涉及两个基本类型的完整联合类型定义被解释为指示通过一种类型的指针的任何访问应该被视为对另一种类型的可能访问将使得可以调整依赖于就地类型的程序。在没有未定义的行为的情况下这样做 - 这是在本标准下任何其他实际方法无法实现的。不幸的是,这样的解释也会限制99%无害的情况下的许多优化,从而使解释标准的编译器无法以尽可能高效的方式运行现有代码。

    关于规则是否正确指定,这取决于它应该是什么意思。可能有多种合理的解释,但将它们结合起来会产生一些相当不合理的结果。

    PS - 关于指针比较和memcpy的规则的唯一解释,如果不给出术语&#34;对象&#34;与别名规则中的含义不同的含义表明,没有分配的区域可用于保存多于一种对象。虽然某些类型的代码可能能够遵守这样的限制,但是如果没有过多的malloc / free调用,程序就无法使用自己的内存管理逻辑来回收存储。标准的作者可能打算说实现不是 required 让程序员创建一个大区域并将其分割成较小的混合类型块本身,但这并不意味着它们预期的通用实现将无法实现。

答案 6 :(得分:2)

这是注释95及其上下文:

  

后缀表达式后跟。运算符和标识符指定结构或联合对象的成员。该值是指定成员的值,(95),如果第一个表达式是左值,则该值为左值。如果第一个表达式具有限定类型,则结果具有指定成员类型的限定版本。

     

(95)如果用于读取union对象内容的成员与上次用于在对象中存储值的成员不同,则对象表示的相应部分为该值被重新解释为新类型中的对象表示,如6.2.6中所述(有时称为“类型双关”的过程)。这可能是陷阱表示。

注释95明确适用于通过工会成员进行的访问。您的代码不会这样做。通过指向2个不同类型的指针访问两个重叠对象,其中没有一个是字符类型,并且没有一个是与类型双关语相关的后缀表达式。

这不是一个明确的答案......

答案 7 :(得分:1)

标准不允许使用成员类型的左值访问结构或联合的存储值。由于您的示例使用类型不是union的类型的lvalues访问union的存储值,也没有任何包含该union的类型,因此仅在此基础上行为将是Undefined。

有一件事变得棘手的是,在严格阅读标准的情况下,即使是如此简单的事情

int main(void)
{
  struct { int x; } foo;
  foo.x = 1;
  return 0;
}

也违反N1570 6.5p7,因为foo.xint类型的左值,它用于访问类型为struct foo的对象的存储值,并键入int }不满足该部分的任何条件。

标准甚至可以远程有用的唯一方法是,如果一个人认识到在涉及从其他左值导出的左值的情况下需要N1570 6.5p7的例外。如果标准描述了编译器可能或必须认识到这种推导的情况,并指定N1570 6.5p7仅适用于在函数或循环的特定执行中使用多种类型访问存储的情况,那么这将消除很多复杂性,包括对“有效类型”概念的任何需求。

不幸的是,一些编译器甚至在一些明显的情况下也忽略了左值和指针的推导:

s1 *p1 = &unionArr[i].v1;
p1->x ++;

如果涉及p1的其他操作分离了p1的创建和使用,但编译器无法识别unionArr[i].v1unionArr[i]之间的关联可能是合理的,但gcc和clang都没有即使在使用指针立即的指针跟随取得工会成员地址的操作的简单情况下,也可以始终如一地识别这种关联。

同样,由于标准不要求编译器识别派生左值的任何用法,除非它们是字符类型,gcc和clang的行为不会使它们不符合。另一方面,他们合规的唯一原因是因为标准中的一个缺陷是如此离谱,以至于没有人读标准说它实际上做了什么。