【问题标题】:How to ensure local variables are not removed during optimization如何确保优化期间不删除局部变量
【发布时间】:2015-09-19 19:05:32
【问题描述】:

背景

在工作中,我经常使用优化代码的核心转储进行事后调试。

对于某些难以重现的、不可重现的故障,我希望获得额外的信息。在这些情况下添加额外的跟踪是不可行的,因为绝大多数调用都是成功的,并且每分钟会添加数百万个“不必要的”跟踪,这将快速滚动日志文件。捕获和跟踪并不总是可行的,一些错误可能会破坏环境,导致跟踪失败。

由于我们的核心转储包括调用堆栈内存,我认为我可以使用调用堆栈内存上的一个区域进行“跟踪”。

问题

感谢优化编译器这样的代码不起作用

void process (int i)
{
   int save_me = i;
   // Do something else
}

这个想法是通过分配给局部变量来将输入变量存储在堆栈上。这通常在调试模式下运行良好,但在优化构建中,编译器认为该语句没有副作用并将其删除。

alloca 似乎可以工作,除非我们针对一些不支持 alloca 的平台,而且我不确定它与 C++ 的配合如何。

我做了一些实验,即使在优化的构建中,以下代码似乎也能够使状态“粘”在堆栈上:

#include <cstdint>
#include <stdexcept>
#include <istream>
#include <sstream>

struct saved_state
{
  saved_state ()
    : head  (0xAABBCCDD)
    , tail  (0xEEFF0000)
  {
    std::fill (state, state + 16, 0);
  }

  void push (std::int32_t input) volatile
  {
    for (auto i = 15U; i > 0U; --i)
    {
      state[i] = state[i - 1];
    }
    state[0] = input;
  }

  volatile std::uint32_t  head      ;
  volatile std::int32_t   state [16];
  volatile std::uint32_t  tail      ;
};

void invoke (std::int32_t i)
{
  if (i > 10)
  {
    throw std::runtime_error ("Busted");
  }
}

void process (std::istream & input)
{
  saved_state volatile ss;

  while (!input.eof ())
  {
    std::int32_t i;
    if (input >> i)
    {
      ss.push (i);
      invoke (i);
    }
  }
}

int main()
{
  std::istringstream input ("1\n2\n30\n");
  process (input);
  return 0;
}

问题

我可以期望代码执行我希望它执行的操作吗?它似乎适用于我们当前的一组编译器(clang 和 gcc),但我可以期待它继续工作吗?

有没有更好的方法来实现我想做的事情?

我所说的更好是指更简单、更健壮或符合标准。

【问题讨论】:

  • volatile 表示对象的加载/存储不能被优化掉,使用它通常是保持变量活动的一种公认方式。
  • 你的程序是如何被杀死的?在它们被杀死之前运行一个可以输出所需信息的处理程序是否可行?对我来说似乎更简单,更健壮。例如,如果它们因调用std::terminate 而死,您可以安装std::terminate_handler
  • 故障通常表现为某种低级异常,触发平台提供的故障处理程序,该处理程序收集了大量信息。我不确定是否有办法连接到该故障处理程序并以可靠的方式注入信息。不过我会调查的。谢谢。
  • 您可以指示编译器不要优化代码(至少在 Visual Studio 中)。但是,您将不会从优化中受益。恕我直言,您应该接受它,而是学习如何调试优化的代码。您不希望每次出现错误时都修改代码并将部分volatile 构建发送给客户以重现错误并随后进行分析。有时它甚至会变得无法重现,更不用说很难重建客户使用的确切版本。

标签: c++ debugging postmortem-debugging


【解决方案1】:

从您的问题看来,您知道在代码的特定功能/区域中存在罕见/难以调试的问题?我假设这是因为您正在谈论手动检测,并且我猜您不打算在任何地方都这样做,以预测可能出现的问题。

如果这是您的情况,那么我认为您可能需要考虑仅针对该函数/代码区域禁用优化。在 Visual Studio 中,您可以使用 #pragma 来执行此操作,我想对于 clang / gcc 也存在类似的情况。最坏的情况是,您可以将相关函数提取到一个单独的文件中,然后只编译该文件而不进行优化。

对于那些只出现在优化构建中的问题,这可能对您没有帮助,但是当您遇到那些棘手的 Heisenbugs 时,任何类型的添加跟踪都可能会隐藏问题或降低问题的频率。在那种情况下,你唯一真正的办法就是真正擅长破译反汇编......

也就是说,volatile 确实告诉编译器不允许优化读取和写入,因此您的方法应该是健壮的,并且对于某些类型的错误可能是有用的工具。

【讨论】:

  • 嗨,是的,这是真的。我有时会为需要额外仪器的困难错误而苦苦挣扎。我一直在尝试禁用 GCC 的优化,但我很难让它变得健壮(不得不强制非内联并添加空 asm)。除了感觉相当难看之外,我不想使用编译器细节,除非我必须这样做。
  • @FuleSnabel 如果您不想在代码中使用特定于编译器的#pragmas,那么将相关函数放在单独的目标文件中并只编译该文件而不进行优化可能是可行的方法。这样一来,您主要包含构建系统的丑陋之处,而不是将其放入您的代码中(尽管您最终会更改源文件的组织)。
【解决方案2】:

优化的编译可能难以调试:

你可以试试这样的:

在你的例子中:

void process (int i)
{
   int save_me = i;
   // Do something else
}

(预初始化的)形式参数和自动变量都在同一个堆栈上,仅相隔几个字节。如果在“做其他事情”期间发生崩溃,则优化器已经完成了它不再使用的堆栈项的操作。

我比较幸运的是:

void process (int i)
{
   // Do something else

   if (bool_that_compiler_can_not_predetermine_is_always_false)
   {
       std::cerr << "error:  int i is " << i << std::endl;
   }
}

由于编译器无法确定 cerr 行永远不会被执行,它会生成代码,并将形参保留在作用域内。

当然,除了 cerr 之外,您还可以选择其他操作。也许是一个日志条目?也许更小的东西。关键是,在丢弃 i 的值(或者,如果您仍然需要,save_me)直到“进程”结束后,您的核心转储中不会发生故障。

优化器也可以重新排序代码,但是 if 子句在流程结束时的位置(我认为)会强制 do-something-else 的所有部分在该子句之前完成。


我有时会使用时间戳来创建不能为真子句。 (因为 ::time(0) 非常有效)。

如果你有一个main,argc很容易使用,即(0 == argc),或者(argc > 100), 并且多余的参数很容易被忽略。

【讨论】:

    猜你喜欢
    • 2016-04-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-02
    • 1970-01-01
    相关资源
    最近更新 更多