为什么优化会破坏此功能?

gui*_*low 49 c c++ optimization endianness strict-aliasing

我们最近在大学里开了一个关于多种语言编程特色的讲座.

讲师写下了以下功能:

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

    return tmp;
}
Run Code Online (Sandbox Code Playgroud)

虽然我完全理解这在可读性方面也是非常差的风格,但他的主要观点是这部分代码在生产代码中运行良好,直到它们实现了高优化级别.然后,代码将什么都不做.

他说,变量的所有赋值tmp都将由编译器优化.但为什么会这样呢?

我知道有些情况下变量需要声明为volatile,这样编译器就不会触及它们,即使他认为它们永远不会被读或写,但我不知道为什么会发生这种情况.

Jar*_*d42 47

在C++中,如果指针参数char*指向基本上不同的类型("严格别名"规则),则假定它们不是别名(除外).这允许一些优化.

在这里,u64 tmp永远不会被修改为u64.
的内容u32*被修改,而可以是无关的" u64 tmp"这样可以被看作nop用于u64 tmp.

  • @leemes:是的,因为 `char` 和 `unsigned char` 是所有其他类型的“别名”(C++11 3.10p10)。 (2认同)

Sha*_*our 41

此代码违反了严格的别名规则,这使得通过不同类型的指针访问对象是非法的,尽管允许通过*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);
} 
Run Code Online (Sandbox Code Playgroud)

文章解释说这段代码违反了严格的别名规则,因为sp它是别名,arg但是它们有不同的类型,并说尽管它会编译,但它很可能argswap_words返回后没有变化.虽然通过简单的测试,我无法使用上面的代码和OP代码重现该结果,但这并不意味着什么,因为这是未定义的行为,因此无法预测.

本文接着讨论了许多不同的案例,并提出了几种工作解决方案,包括通过union进行类型惩罚,这在C99 1中有明确定义,可能在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);
}
Run Code Online (Sandbox Code Playgroud)

作为参考,关于严格混叠C99标准草案的相关部分是表达式7段,其中说:6.5

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

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

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

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

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

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

- 角色类型.

脚注76说:

此列表的目的是指定对象可能或可能没有别名的情况.

C++草案标准的相关部分是3.10 Lvalues和rvalues10

文章Type-punning和strict-aliasing给出了一个更温和但不太完整的主题介绍,重新访问的C99给出了对C99和别名的深入分析,而不是轻松阅读.这个访问非活动联盟成员的答案 - 未定义?通过C++中的联合来讨论类型惩罚的泥泞细节,也不是轻松阅读.


脚注:

  1. Pascal Cuoq 引用评论:[...] C99最初是笨拙的措辞,似乎通过工会进行类型惩罚未定义.实际上,虽然工会的类型惩罚在C89中是合法的,在C11中是合法的,并且它在C99中一直是合法的,尽管委员会在2004年之前修正了不正确的措辞以及随后的TC3发布.open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm

  • 不,它明确不是未定义的行为。相反,它给出了一个未指定的值,该值可能是任何值(包括陷阱值,如果存在),但除此之外具有明确定义的行为。因此,通过联合进行类型双关的结果不能做任何事情,但它可能会给出任何价值(包括看起来什么都不做)。但是对于不能有陷阱值的类型(例如无符号整数),它不会崩溃。 (2认同)
  • 是的,但是您引用了 C99 规范,而不是 C++ 规范。此外,我相信 C 规则即使在 C++ 中也适用于*标准布局* 联合。虽然在 C++11 中可以使用非标准布局联合,但它们并不是非常有用。 (2认同)
  • @davmac:我相信除非 `u32` 的对齐要求比 `u64` 更粗(这很奇怪),将 `u64*` 转换为 `u32*` 需要产生一个 `u32*`,如果转换为`unsigned char*` 将产生与将 `u64*` 直接转换为类型 `unsigned char*` 相同的结果。这并不意味着所有指针类型都具有相同的按位表示,而是比您暗示的更严格地定义指针的含义(尽管实际上如何*使用*指针是有限制的)。 (2认同)

pep*_*ico 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 function ‘uint64_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);
                                                      ^
Run Code Online (Sandbox Code Playgroud)

Clang 3.4没有在任何优化级别发出警告,这很奇怪 ......