【问题标题】:Stack pointer comparison anomaly when optimization enabled启用优化时堆栈指针比较异常
【发布时间】:2018-07-18 09:58:12
【问题描述】:

这让我头疼!我在函数调用期间对堆栈进行了一些实验,我遇到了这个小异常。编译器是 GCC 4.9.2 和 -O1、-O2 或 -O3 优化标志。禁用优化 (-O0) 时不存在该问题。无论是在C++98、C++11还是C++14下编译,结果都是一样的。

在下面的示例中,我假设 CDECL 调用约定用于对 result() 的调用,因此 args 应该从右到左压入堆栈。

#include <iostream>

void result(int a, int b) {
    int* pA = &a;
    int* pB = &b;

    // pA should be equal to pB + sizeof(int) bytes, why does this report "false" 
    // when optimizations are on?
    std::cout << (pA == pB + 1 ? "true" : "false") << '\n';

    // this appears to be true if we dump the pointer addresses.
    std::cout << pA << ", " << pB + 1 << '\n';

    // and still holds water when getting values through indirection.
    std::cout << "a: " << *(pB + 1) << " b: " << *pB << '\n';
}

int main(int argc, char** argv)
{
    result(10, 20);
    return 0;
}

启用优化的示例输出:

false // why?
0x704eca3c088c, 0x704eca3c088c // clearly they're equal
a: 10 b: 20 // and point to the correct arguments on the stack

禁用优化的示例输出:

true
0x7e5bd5f34fdc, 0x7e5bd5f34fdc
a: 10 b: 20

当可以直观地比较它们的相等性并发现它们的相等性时,编译器优化会导致堆栈指针比较失败怎么办,优化与否?

编辑:我相信这个问题与建议的副本 (Is it unspecified behavior to compare pointers to different arrays for equality?) 不同,因为堆栈传统上是作为连续数组实现的(显然有例外 - 一个恰当的例子),并且建议的副本给出了两个不同数组的示例.

【问题讨论】:

  • 不同对象之间的指针比较未定义。允许优化器假设未定义的行为永远不会发生。
  • pA 应该等于 pB + sizeof(int) bytes 为什么?为什么应该这样?如果编译器认为合适,则不必将这些值存储在堆栈帧中。
  • 这是我的一个假设。我现在明白这是未定义的行为。显然它是未定义的,除非它们是指向同一对象的成员或同一数组的元素的指针。
  • @Barmar 事实上,它保证false 指向不同对象的指针,除了来自一个对象的一个​​过去指针和另一个未指定的对象的开始指针的特殊情况
  • 不知道为什么这被否决了,这对我来说似乎是个好问题并且写得很好

标签: c++ optimization


【解决方案1】:

来自 C++17 (N4659) [expr.eq]/2(等式运算符):

比较指针定义如下:

  • 如果一个指针表示一个完整对象的地址,而另一个指针表示另一个完整对象的最后一个元素之后的地址,则比较结果未指定。
  • [...]

这意味着&amp;a&amp;b+1 的比较未指定。 (它不是未定义的,正如其他答案和 cmets 中所声称的那样;实际上,指针的相等比较在 C++ 中从来没有未定义的行为。

因此,您的代码可能会观察到truefalse 作为比较结果;优化器可以随心所欲地插入任一结果,而不是在运行时实际进行任何比较。


历史记录:此更改来自 CWG Defect 1652,针对 C++11 提交并授予缺陷状态,这意味着它追溯适用。最初的 C++11 文本说指针应该比较相等“如果它们表示相同的地址”,但这已经改变了,因为它阻碍了优化,并且因为在不能做运行时地址的 constexpr 中允许相等比较检查清楚。

我相信 g++ 总是在无视 C++11 明显要求不执行它的情况下执行此优化。

【讨论】:

  • 感谢出色的引用和解释。它在规范中的布局非常清楚,“缺陷”状态的追溯性解释了为什么这种行为至少早在 C++98 就存在。我选择这个作为答案,因为规范再清楚不过了。干杯!
【解决方案2】:

这个结果与堆栈的布局方式无关,而与无关指针之间的比较结果未指定这一事实有关。

让我们看看 GCC 为您的 result 函数的前一点生成的程序集:

.LC0:
  .string "false"
.LC1:
  .string ", "
.LC2:
  .string "a: "
.LC3:
  .string " b: "
result(int, int):
  push rbx
  mov edx, 5
  sub rsp, 32
  mov DWORD PTR [rsp+12], edi
  mov DWORD PTR [rsp+8], esi
  mov edi, OFFSET FLAT:std::cout
  mov esi, OFFSET FLAT:.LC0
  call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)

如您所见,根本没有分支。指向字符串"false" 的指针被加载到esi 并无条件地用作std::__ostream_insert 的参数。由于语言没有指定指针比较的行为,编译器只是假设true 的情况永远不会发生并完全删除它。

【讨论】:

  • 令人着迷。所以是编译器防止未定义的行为,而不是允许结果因运行而异?编译器优化的复杂性总是让我大吃一惊。
  • @HunterMueller 行为会因运行而异,更多的是忽略检查会更快,因为语言没有指定应该发生什么。
猜你喜欢
  • 1970-01-01
  • 2014-12-18
  • 1970-01-01
  • 2016-12-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-04
相关资源
最近更新 更多