警告“取消引用类型惩罚指针会破坏严格别名规则”的后果

时间:2014-05-23 05:00:41

标签: c casting compiler-optimization strict-aliasing aliasing

我对类似主题和与之相关的一些材料进行了一些查询。 但我的查询主要是了解下面代码的警告。我不想修复!! 我了解有两种方法,联合或使用 memcpy

uint32 localval;
void * DataPtr;
localval = something;
(*(float32*)(DataPtr))= (*(const float32*)((const void*)(&localval)));

请注意以下重要事项
1.这里涉及的两种类型都是32位。 (或者我错了吗?)
2.两者都是局部变量。

编译器特定要点:
1.代码应该是独立于平台的,这是一个要求!!
我在GCC编译,它按预期工作。 (我可以将int重新解释为浮点数),这就是我忽略警告的原因。

我的问题
1.编译器在此别名情况下可以执行哪些优化? 2.因为两者都占据相同的大小(如果不是,请纠正我)这样的编译器优化的副作用是什么? 3.我可以安全地忽略警告或关闭别名吗? 4.如果编译器没有执行优化,我的程序在第一次编译后没有被破坏?我可以安全地假设每次编译器的行为方式都相同(不进行优化)吗? 5.别名是否也适用于void *类型转换?或者它是否仅适用于标准类型转换(int,float等...)? 6.如果禁用别名规则会有什么影响?

被修改
1.基于R和Matt McNabb的更正 2.添加了新的问题

3 个答案:

答案 0 :(得分:3)

你有一个不完整的例子(如所写的,它展示了UB,因为localval未初始化)所以让我完成它:

uint32 localval;
void * DataPtr;
DataPtr = something;
localval = 42;
(*(float32*)(DataPtr))= (*(const float32*)((const void*)(&localval)));

现在,由于localval的类型为uint32*(const float32*)((const void*)(&localval))的类型为float32,因此它们不能使用别名,因此编译器可以自由地重新排序最后两个语句彼此。这显然会导致行为与您想要的行为不同。

写这个的正确方法是:

memcpy(DataPtr, &localval, sizeof localval);

答案 1 :(得分:3)

语言标准试图在使用语言的程序员的有时竞争利益和想要使用广泛的优化集来生成合理快速代码的编译器编写者之间取得平衡。将变量保存在寄存器中就是这样一种优化。对于" live"在程序的一部分中,编译器尝试在寄存器中分配它们。存储在指针中的地址可以存储在程序的地址空间中的任何地方 - 这将使寄存器中的每个变量无效。有时编译器可以分析程序并找出指针指向或不指向的位置,但C(和C ++)语言标准认为这是一个不适当的负担,对于" system"程序类型通常是不可能的任务。因此,语言标准通过指定某些结构导致未定义的行为来放宽约束。因此,编译器编写者可以假设它们不会发生并在该假设下生成更好的代码。在strict aliasing的情况下,达成的妥协是如果使用一个指针类型存储到内存,则假定不同类型的变量不变,因此可以保存在寄存器中,或者存储和加载到这些其他类型可以针对指针存储重新排序。

本文中有许多这类优化的例子"未定义的行为:我的代码发生了什么?"

http://pdos.csail.mit.edu/papers/ub:apsys12.pdf

有一个例子违反了Linux内核中的严格别名规则,显然内核通过告诉编译器不要使用严格别名规则进行优化来避免这个问题。" Linux内核使用-fno-strict-aliasing 禁用基于严格别名的优化。"

struct iw_event {
    uint16_t len; /* Real length of this stuff */
    ...
};
static inline char * iwe_stream_add_event(
    char * stream, /* Stream of events */
    char * ends, /* End of stream */
    struct iw_event *iwe, /* Payload */
    int event_len ) /* Size of payload */
{
    /* Check if it's possible */
    if (likely((stream + event_len) < ends)) {
        iwe->len = event_len;
        memcpy(stream, (char *) iwe, event_len);
        stream += event_len;
    }
    return stream;
}

图7:严格的别名冲突,在include / net / iw_handler.h中 Linux内核,它使用GCC的-fno-strict-aliasing来防止可能的 重新排序。

2.6类型间隔指针解除引用

  

C使程序员可以自由地转换一种类型的指针   到另一个。指针演员经常被滥用以重新解释给定的   具有不同类型的对象,称为类型惩罚的技巧。通过   这样做,程序员期望两个不同的指针   类型指向相同的内存位置(即别名)。   但是,C标准对别名有严格的规定。在   特别是,只有少数例外,两个不同的指针   类型不是别名[19,6.5]。违反严格别名导致   未定义的行为。   图7显示了Linux内核的一个示例。该   函数首先更新iwe-&gt; len,然后复制内容   iwe,其包含更新的iwe-&gt; len,到缓冲流   使用memcpy。请注意,Linux内核提供了自己的优化memcpy实现。在这种情况下,当event_len   在32位系统上是常数8,代码扩展如下。

iwe->len = 8;
*(int *)stream = *(int *)((char *)iwe);
*((int *)stream + 1) = *((int *)((char *)iwe) + 1);
  

扩展代码首先将8写入iwe-&gt; len,即   输入uint16_t,然后读取指向相同的iwe   iwe-> len的内存位置,使用不同的int类型。根据严格的别名规则,GCC得出结论认为   并且写入不会发生在同一个内存位置,   因为他们使用不同的指针类型,并重新排序这两个   操作。因此,生成的代码复制了陈旧的iwe-&gt; len   值。 Linux内核使用-fno-strict-aliasing来禁用基于严格别名的优化。

<强>答案

  

1)编译器在此别名情况下可以执行哪些优化?

语言标准对严格符合程序的语义(行为)非常具体 - 编译器编写者或语言实现者的负担是正确的。一旦程序员越过该行并调用未定义的行为,那么标准清楚地表明,这将按预期工作的证据负担落在程序员身上,而不是编译器编写器上 - 在这种情况下,编译器足以警告未定义已经调用了行为,尽管它甚至没有义务这样做。有时令人讨厌的人会告诉你,在这一点上&#34;任何事情都可能发生&#34;通常接着是一些笑话/夸张。对于您的程序,编译器可以生成典型的平台代码&#34;并存储到localval something然后的值从localval加载并存储在DataPtr,就像您想要的那样,但要明白它是没有义务这样做。它将商店localval视为uint32类型的商店,它将(*(const float32*)((const void*)(&localval)))的加载解除引用作为float32类型的加载,并将这些加到不属于同一位置,因此localval可以在包含something的寄存器中,当它从为localval保留的堆栈上的未初始化位置加载时,如果它确定需要&localval #34;溢出&#34;注册回其保留的&#34;自动&#34;存储(堆栈)。在取消引用指针并从内存加载之前,它可能会或可能不会将localval存储到内存中。根据代码中的后续内容,可能会认为something未被使用且DataPtr的分配没有副作用,因此可能会认为分配是&#34;死代码& #34;甚至没有分配到寄存器。

  

2)因为两者都占据相同的大小(如果不是,请纠正我)这样的编译器优化的副作用是什么?

效果可能是未定义的值存储在void *指向的地址。

  

3)我可以安全地忽略警告或关闭别名吗?

这是特定于您正在使用的编译器 - 如果编译器记录了关闭严格别名优化的方法,那么是的,无论编译器有什么警告。

  

4)如果编译器没有执行优化,我的程序在第一次编译后没有被破坏?我可以安全地假设每次编译器的行为方式都相同(不进行优化)吗?

也许,有时程序另一部分的非常小的变化可能会改变编译器对此代码的作用,如果函数是&#34; inlined&#34;那么请考虑一下。它可能会与代码的其他部分组合在一起,请参阅this SO question

  

5)别名是否也适用于void *类型转换?或者它是否仅适用于标准类型转换(int,float等...)?

你不能取消引用const所以编译器只关心你的最终演员表的类型(如果你将non-const转换为memcpy,那么它会让你感到抱怨,反之亦然)。

  

6)如果禁用别名规则会有什么影响?

请参阅编译器的文档 - 一般来说,如果你这样做,你会得到更慢的代码(比如Linux内核在上面的论文中选择做的那样),然后将其限制为一个小的编译单元,只有必要的功能。

<强>结论

我理解你的问题是为了好奇并试图更好地理解这是如何工作的(或者可能不起作用)。你提到要求代码是可移植的,暗示然后要求程序是兼容的而不是调用未定义的行为(记住,如果你这样做,你就会负担)。在这种情况下,正如您在问题中所指出的,一种解决方案是使用-O3,因为事实证明这不仅使您的代码符合并因此可移植,它还以最有效的方式执行您的意图在具有优化级别memcpy的当前gcc上,编译器可以将localval转换为在DataPtr指向的地址处存储movl %esi, (%rdi)值的单个指令,请参阅{{1}} 3}} - 查找{{1}}指令。

答案 2 :(得分:2)

const没有任何区别。要检查类型是否相同,您可以将sizeof (uint32)sizeof (float32)进行比较。这两种类型也可能有不同的对齐要求。

除了那些东西;行为未定义以读取localval的内存,就好像它中存储了一个浮点数一样,这就是严格的别名规则所说的。

6.5#6:

  

访问其存储值的对象的有效类型是声明的类型   对象,如果有的话。

6.5#7:

  

对象的存储值只能由具有其中一个的左值表达式访问   以下类型

localval的有效类型为uint32,“以下类型”列表不包含float32,因此这违反了别名规则。

如果您在动态分配的内存中出现别名,那么它就不同了。没有“声明类型”,因此“有效类型”是最后存储在对象中的内容。您可以malloc(sizeof (uint32)),然后将float32存储在其中并将其读回。

总而言之,您似乎在问“我知道这是未定义的,但我可以依靠我的编译器成功地完成它吗?”要回答这个问题,你必须指定你的编译器是什么,以及你调用它的开关,至少。

当然,也可以选择调整代码,使其不违反严格别名规则,但是你没有提供足够的背景信息来继续这条赛道。