为什么优化会破坏这个函数?

50

我们最近在大学里参加了一场关于几种语言中编程特性的讲座。

演讲者写下了以下函数:

inline u64 Swap_64(u64 x)
{
    u64 tmp;
    (*(u32*)&tmp)       = Swap_32(*(((u32*)&x)+1));
    (*(((u32*)&tmp)+1)) = Swap_32(*(u32*) &x);

    return tmp;
}

我完全理解这也会影响代码的可读性,但他的主要观点是,该代码部分在生产代码中运行得很好,直到他们启用了高优化级别。然后,代码就什么都不做了。

他说,变量tmp的所有赋值都将被编译器优化掉。但为什么会发生这种情况呢?

我知道有时需要声明变量为volatile,以便编译器即使认为它们永远不会被读或写,也不会触及它们,但我不知道为什么这里会出现这种情况。


42
这段代码表现出未定义行为。编译器在法律上被允许做任何事情(参见“鼻妖”)。“优化为无操作”是未定义行为的一种可能表现方式。 - Igor Tandetnik
5
寻找严格别名。 - Jarod42
1
有些编译器可以生成汇编输出(例如gcc -S)。我很想看看它在每种情况下生成了什么。 - jarmod
我刚刚尝试了 gcc (Ubuntu/Linaro 4.7.2-5ubuntu1) 4.7.2,并且在所有优化级别下都得到了预期的结果。(当然这并不能证明什么,但我试图找到一个它会失败的优化级别) - leemes
@leemes 很抱歉,我不知道使用了哪个编译器设置,因为这只是一个文本形式的示例,没有样本项目。 - guitarflow
显示剩余7条评论
3个回答

48
这段代码违反了严格别名规则,这使得通过不同类型的指针访问对象非法,尽管可以通过 *char ** 访问。编译器可以假设不同类型的指针不指向相同的内存并进行相应的优化。这也意味着代码会引发未定义行为,可能会导致任何结果。
这个主题最好的参考资料之一是理解严格别名,我们可以看到第一个例子与 OP 的代码类似:
uint32_t swap_words( uint32_t arg )
{
  uint16_t* const sp = (uint16_t*)&arg;
  uint16_t        hi = sp[0];
  uint16_t        lo = sp[1];

  sp[1] = hi;
  sp[0] = lo;

 return (arg);
} 

本文解释了这段代码违反了严格别名规则,因为sparg的别名,但它们具有不同的类型,并且指出虽然它将编译,但在swap_words返回后arg可能不会改变。尽管使用简单的测试,我无法复制上面的代码或OPs代码中的结果,但这并不意味着什么,因为这是未定义行为,因此不可预测。

文章继续讨论许多不同的情况,并提供了几个工作解决方案,包括通过联合进行类型转换,这在C991中是定义良好的,在C++中可能未定义,但在实践中得到大多数主要编译器的支持,例如这里是gcc关于类型转换的参考。以前的帖子C和C ++中联合的目的详细介绍了这些内容。尽管有许多关于此主题的线程,但这似乎做得最好。

那个解决方案的代码如下:

typedef union
{
  uint32_t u32;
  uint16_t u16[2];
} U32;

uint32_t swap_words( uint32_t arg )
{
  U32      in;
  uint16_t lo;
  uint16_t hi;

  in.u32    = arg;
  hi        = in.u16[0];
  lo        = in.u16[1];
  in.u16[0] = lo;
  in.u16[1] = hi;

  return (in.u32);
}

参考C99 draft standard关于strict aliasing的相关部分,其中第6.5表达式7段说:

一个对象的存储值只能通过具有以下类型之一的lvalue表达式访问:76)

— 与对象的有效类型兼容的类型,

— 与对象的有效类型兼容的类型的限定版本,

— 对应于对象有效类型的带符号或无符号类型,

— 对应于对象有效类型的带符号或无符号类型的限定版本,

— 包括上述类型之一在其成员中(包括子聚合或包含联合的成员,递归地),的聚合或联合类型,或

— 字符类型。

注脚76则说:

这个列表的目的是指定对象在哪些情况下可能或不可能具有别名。相关章节来自于C ++草案标准,即第 3.10 Lvalues and rvalues10段。该文章Type-punning and strict-aliasing提供了更温和但不太完整的介绍,并且 C99 revisitedC99和别名进行了深入分析,阅读难度较大。这篇答案Accessing inactive union member - undefined?通过一个C ++ 联合体详细说明了类型戏弄的模糊细节,阅读难度也很大。

脚注:

  1. 引用 Pascal Cuoq 的 comment[...]C99 最初措辞不当,似乎使通过联合类型转换成为未定义行为。实际上,通过联合类型转换是在 C89 中合法的,在 C11 中也是合法的,并且在 C99 中始终合法,尽管直到 2004 年委员会才修复了措辞错误,并发布了 TC3。open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm

2
不,它明确地不是未定义行为。相反,它会给出一个未指定的值,这个值可以是任何值(包括陷阱值,如果存在的话),但是它仍然具有明确定义的行为。因此,通过联合类型进行类型转换的结果不能做任何事情,但它可能给出任何值(包括看起来什么都没发生)。但对于不能有陷阱值的类型(如无符号整数),它不会崩溃。 - Chris Dodd
2
是的,但你引用了C99规范,而不是C++规范。此外,我相信即使在C++中,C规则也适用于标准布局联合。虽然非标准布局联合在C++11中是可能的,但它们并不是非常有用的。 - Chris Dodd
3
我认为,除非u32的对齐要求比u64更粗糙(这将非常奇怪),否则将u64*强制转换为u32*是必需的,这样如果将其转换为unsigned char*,将产生与直接将u64*强制转换为unsigned char *类型相同的结果。这并不意味着所有指针类型具有相同的位表示,但它比你所暗示的更严格地定义了指针的含义(尽管在实际使用指针时也存在一定的限制)。 - supercat
@supercat 在语言规范中没有这样的要求。具有定义值的转换基本上是指_到另一个指针类型然后返回_(但不仅限于_到另一个指针类型_)和_到char *_,以及(可以说)_到其[对象]开头的子对象_。但是,如果您通过另一种指针类型转到char *,则未指定结果指针指向什么(至少在C中)。 - davmac
@davmac:给定 typedef union { u64 ll; u32 ww[2] unsigned char bb[8];} u3264; u64 *p;(u32*)p 是否允许产生与 ((u3264*)p)->ww; 不同的结果?是否有任何实现可以有用地这样做?如果第一个问题的答案是肯定的,但第二个问题的答案是否定的,那么不要求前者的行为像后者的行为会有什么优势? - supercat
显示剩余20条评论

47
在C++中,除了char*之外,如果指针参数指向基本不同类型(“严格别名规则”),则假定它们不会别名。这允许进行一些优化。
这里,u64 tmp作为u64永远不会被修改。 u32*的内容被修改,但可能与'u64 tmp'无关,因此对于u64 tmp来说可能被视为nop

那么以类似的方式颠倒8个char*的顺序是定义良好的行为吗? - leemes
2
@leemes:是的,因为charunsigned char“别名”了所有其他类型(C++11 3.10p10)。 - Michael Foukarakis
谢谢你的回答!我会了解严格别名! - guitarflow

10

g++(Ubuntu / Linaro 4.8.1-10ubuntu9)4.8.1:

> g++ -Wall -std=c++11 -O0 -o sample sample.cpp

> g++ -Wall -std=c++11 -O3 -o sample sample.cpp
sample.cpp: In functionuint64_t Swap_64(uint64_t)’:
sample.cpp:10:19: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
     (*(uint32_t*)&tmp)       = Swap_32(*(((uint32_t*)&x)+1));
                   ^
sample.cpp:11:54: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
     (*(((uint32_t*)&tmp)+1)) = Swap_32(*(uint32_t*) &x);
                                                      ^

在任何优化级别下,Clang 3.4都不会发出警告,这很奇怪...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接