5

我已经完成了一些关于类似主题的查询以及一些与之相关的材料。但我的查询主要是为了了解以下代码的警告。我不想修复!我知道有两种方法,联合或使用memcpy

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

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

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

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

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

4

3 回答 3

3

语言标准试图在使用语言的程序员和希望使用广泛的优化来生成合理快速代码的编译器编写者之间有时相互竞争的利益之间取得平衡。将变量保存在寄存器中就是这样一种优化。对于在程序的某个部分中“活动”的变量,编译器会尝试将它们分配到寄存器中。存储在指针中的地址可以存储程序地址空间中的任何位置——这将使寄存器中的每个变量都无效。有时编译器可以分析程序并找出指针可以或不可以指向的位置,但 C(和 C++)语言标准认为这是一个过度的负担,对于“系统”类型的程序来说,这通常是一项不可能完成的任务。因此,语言标准通过指定某些构造导致“未定义的行为”来放松约束,因此编译器编写者可以假设它们不会发生并在该假设下生成更好的代码。如果是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: Linux 内核的 include/net/iw_handler.h 中的严格别名违规,它使用 GCC-fno-strict-aliasing来防止可能的重新排序。

2.6 类型双关指针取消引用

C 让程序员可以自由地将一种类型的指针转​​换为另一种类型。指针转换经常被滥用来重新解释具有不同类型的给定对象,这种技巧称为类型双关语。通过这样做,程序员期望两个不同类型的指针指向同一个内存位置(即别名)。但是,C 标准对别名有严格的规定。特别是,除了少数例外,两个不同类型的指针没有别名 [19, 6.5]。违反严格的别名会导致未定义的行为。图 7 显示了一个来自 Linux 内核的示例。该函数首先更新 iwe->len,然后使用 memcpy 将包含更新后的 iwe->len 的 iwe 的内容复制到缓冲区流。请注意,Linux 内核提供了自己优化的 memcpy 实现。在这种情况下,

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

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

答案

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

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

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

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

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

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

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

也许,有时程序的另一部分中非常小的变化可能会改变编译器对这段代码所做的事情,想一想如果函数是“内联”的,它可能会被扔到你代码的其他部分的混合中,看这个所以问题

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

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

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

请参阅您的编译器的文档 - 通常您会得到较慢的代码,如果您这样做(就像 Linux 内核在上面论文中的示例中选择的那样)然后将其限制为一个小的编译单元,只包含必要的函数.

结论

我了解您的问题是出于好奇,并试图更好地了解其工作原理(或可能不起作用)。您提到要求代码是可移植的,暗示然后要求程序是合规的并且不调用未定义的行为(请记住,如果您这样做,负担就在您身上)。在这种情况下,正如您在问题中指出的那样,一种解决方案是使用memcpy,因为事实证明这不仅使您的代码兼容并因此具有可移植性,而且还可以在当前 gcc 上以最有效的方式执行您想要的操作优化级别-O3,编译器将 转换为memcpy一条指令localvalDataPtrmovl %esi, (%rdi)操作说明。

于 2014-05-23T06:08:11.753 回答
3

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

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

现在,由于localvalhas typeuint32*(const float32*)((const void*)(&localval))has type float32,它们不能别名,所以编译器可以自由地重新排序最后两个语句。这显然会导致行为与您想要的不同。

正确的写法是:

memcpy(DataPtr, &localval, sizeof localval);
于 2014-05-23T05:20:11.910 回答
2

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

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

6.5#6:

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

6.5#7:

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

localval具有有效类型uint32,并且“以下类型”列表不包括float32,因此这违反了别名规则。

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

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

当然,您也可以选择调整您的代码,使其不违反严格混叠规则,但您没有提供足够的背景信息来继续此轨道。

于 2014-05-23T05:18:47.030 回答