49

我们最近在大学举办了一场关于多种语言编程专题的讲座。

讲师写下了以下函数:

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以便编译器不会触及它们,即使他认为它们从未被读取或写入,但我不知道为什么会发生这种情况。

4

3 回答 3

47

char*在 C++ 中,如果指针参数指向根本不同的类型(“严格别名”规则) ,则假定指针参数不具有别名(除了)。这允许一些优化。

在这里,u64 tmp永远不会被修改为u64.
的内容u32*被修改但可能与 ' u64 tmp' 无关,因此可能被视为nopfor u64 tmp

于 2014-01-04T15:26:43.880 回答
45

此代码违反了严格的别名规则,这使得通过不同类型的指针访问对象是非法的,尽管允许通过 *char ** 进行访问。允许编译器假设不同类型的指针不指向同一个内存并进行相应的优化。这也意味着代码调用了未定义的行为并且真的可以做任何事情。

该主题的最佳参考之一是Understanding Strict Aliasing,我们可以看到第一个示例与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);
} 

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

本文继续讨论了许多不同的情况,并提出了几种可行的解决方案,包括通过联合进行类型双关,这在C99 1中定义良好,在C++中可能未定义,但实际上大多数主要编译器都支持,例如这里是gcc 关于 type-punning 的参考上一篇 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 标准草案中关于严格别名的相关部分是6.5 Expressions7段,其中说:

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

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

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

— 与对象的有效类型相对应的有符号或无符号类型,

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

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

— 一种字符类型。

脚注76说:

此列表的目的是指定对象可能或可能不别名的情况。

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

文章Type-punning and strict-aliasing对该主题进行了较为温和但不太完整的介绍,而C99 revisited则对C99和 aliasing进行了深入分析,阅读量并不大。访问非活动工会成员的这个答案 - 未定义?通过C++中的 union 讨论了类型双关语的混乱细节,并且阅读起来也不轻松。


脚注:

  1. 引用Pascal Cuoq 的评论[...]C99 最初措辞笨拙,似乎使通过联合的类型双关语未定义。实际上,尽管工会的类型双关在 C89 中是合法的,在 C11 中是合法的,并且在 C99 中一直是合法的,尽管直到 2004 年委员会才修复了不正确的措辞,并随后发布了 TC3。open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
于 2014-01-06T18:00:52.187 回答
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);
                                                      ^

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

于 2014-01-04T15:59:41.663 回答