警告"解除引用类型 - 惩罚指针会破坏严格别名规则"的结果

Dar*_*ght 5 c casting strict-aliasing compiler-optimization pointer-aliasing

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

uint32 localval;
void * DataPtr;
localval = something;
(*(float32*)(DataPtr))= (*(const float32*)((const void*)(&localval)));
Run Code Online (Sandbox Code Playgroud)

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

编译器具体要点:
1.代码应该是平台无关的,这是一个要求!!
我在GCC编译,它按预期工作.(我可以将int重新解释为浮点数),这就是为什么我忽略了警告.

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

编辑
1.基于R和Matt McNabb的更正
2.增加了一个新问题

amd*_*mdn 4

语言标准试图在使用该语言的程序员和希望使用广泛的优化来生成相当快的代码的编译器编写者之间有时相互竞争的利益之间取得平衡。将变量保存在寄存器中就是这样的优化之一。对于程序中“活动”的变量,编译器会尝试将它们分配到寄存器中。存储在指针中的地址可以存储在程序地址空间中的任何位置 - 这将使寄存器中的每个变量无效。有时,编译器可以分析程序并找出指针可以或不可以指向的位置,但 C(和 C++)语言标准认为这是不应有的负担,并且对于“系统”类型的程序来说通常是一项不可能完成的任务。因此,语言标准通过指定某些构造导致“未定义的行为”来放宽约束,以便编译器编写者可以假设它们不会发生并在该假设下生成更好的代码。如果是strict aliasing,如果使用一种指针类型存储到内存,则假定不同类型的变量保持不变,因此可以保留在寄存器中,或者可以重新排序对这些其他类型的存储和加载关于指针存储。

\n\n

本文“Unde\xef\xac\x81ned Behaviour: What Happened to My Code?”中有很多此类优化的示例。

\n\n

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

\n\n

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

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

图 7: Linux 内核的 include/net/iw_handler.h 中的严格别名违规,该内核使用 GCC\xe2\x80\x99s-fno-strict-aliasing来防止可能的重新排序。

\n\n

2.6 类型双关指针解引用

\n\n
\n

C 使程序员可以自由地将一种类型的指针强制转换为另一种类型。指针强制转换经常被滥用来用不同的类型重新解释给定的对象,这种技巧称为类型双关。通过这样做,程序员期望两个不同类型的指针指向相同的内存位置(即别名)。但是,C 标准对于别名有严格的规则。特别是,除了少数例外,不同类型的两个指针不会别名 [19, 6.5]。违反严格别名会导致\n unde\xef\xac\x81ned 行为。\n 图 7 显示了 Linux 内核的示例。函数\xef\xac\x81rst 更新iwe->len,然后使用memcpy 将包含更新后的iwe->len 的iwe 内容复制到缓冲流。请注意,Linux 内核提供了自己优化的 memcpy 实现。在本例中,当 event_len\n 在 32 位系统上为常量 8 时,代码扩展如下。

\n
\n\n
iwe->len = 8;\n*(int *)stream = *(int *)((char *)iwe);\n*((int *)stream + 1) = *((int *)((char *)iwe) + 1);\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n

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

\n
\n\n

答案

\n\n
\n

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

\n
\n\n

语言标准对于严格遵守程序的语义(行为)非常具体 - 编译器编写者或语言实现者有责任使其正确。一旦程序员越界并调用未定义的行为,那么标准就很清楚,证明这将按预期工作的责任落在程序员身上,而不是编译器编写者身上 - 在这种情况下,编译器已经足够好地警告未定义的行为行为已被援引,尽管它甚至没有义务这样做。有时,令人烦恼的是,人们会告诉你,此时“任何事情都可能发生”,通常会伴随着一些笑话/夸张。对于您的程序,编译器可以生成“平台典型”的代码并存储到 的localvalsomething,然后加载localval并存储在 处DataPtr,就像您预期的那样,但请理解它没有义务这样做。它将存储视为localval对某种类型的存储uint32,并将加载的取消引用视为(*(const float32*)((const void*)(&localval)))来自类型的加载float32,并得出结论,这些不是到同一位置,因此localval可以在包含something从某个类型加载时包含的寄存器中堆栈上未初始化的位置,如果localval它决定需要将寄存器“​​溢出”回其保留的“自动”存储(堆栈),则保留该位置。localval在取消引用指针并从内存加载之前,它可能会也可能不会存储到内存中。根据代码中接下来的内容,它可能会决定localval不使用 ,并且 的分配something没有副作用,因此它可能会决定分配是“死代码”,甚至不对寄存器进行分配。

\n\n
\n

2)由于两者占用相同的大小(如果不是,请纠正我)这种编译器优化可能会产生什么副作用?

\n
\n\n

结果可能是未定义的值存储在 指向的地址处DataPtr

\n\n
\n

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

\n
\n\n

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

\n\n
\n

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

\n
\n\n

也许,有时程序的另一部分的非常小的变化可能会改变编译器对此代码的操作,想一下如果该函数是“内联”的,它可能会被扔到代码的其他部分的混合中,请参阅此那么问题

\n\n
\n

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

\n
\n\n

您不能取消引用 a void *,因此编译器只关心最终转换的类型(在 C++ 中,如果您将 a 转换const为,它会抱怨non-const,反之亦然)。

\n\n
\n

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

\n
\n\n

请参阅编译器的文档 - 一般来说,如果您这样做(就像 Linux 内核在上面论文的示例中选择做的那样),那么您会得到更慢的代码,然后将其限制为一个小的编译单元,仅包含以下函数这是必要的。

\n\n

结论

\n\n

我知道您的问题是出于好奇,并试图更好地理解这是如何工作的(或可能不起作用)。您提到要求代码可移植,这意味着程序必须兼容并且不调用未定义的行为(请记住,如果您这样做,负担就在您身上)。在这种情况下,正如您在问题中指出的,一种解决方案是使用memcpy,因为事实证明,这不仅使您的代码兼容并因此可移植,它还可以在当前 gcc 上以最有效的方式实现您的意图优化级别-O3编译器将 转换memcpy为单个指令,将 的值存储localval在 指向的地址处DataPtr,请在此处查看它在 coliru 中- 查找该movl %esi, (%rdi)指令。

\n