【问题标题】:GCC optimization issues and behaviorGCC 优化问题和行为
【发布时间】:2017-11-05 21:27:18
【问题描述】:

我在 Eclipse 上运行 gcc 时遇到了优化问题。 我有一个程序在 -O0 优化级别下运行良好,但我需要打开其他优化级别,因为我的软件使用了太多闪存空间。 但是当我使用 -O1 优化级别(例如)时,我的代码不再工作,我需要使用 UART 外设管理功能进行如下所述的奇怪修改。

使用 -O0 运行良好的代码

void function(...)
{
   // Send char one by one on UART peripheral
   UART_put_char(...)

   // Wait end of transmission on UART
   while(end_of_transmission_flag == flase);
}

void UART_TxCompleteCallback()
{
   // Change flag state
   end_of_transmission_flag = true;
}

对 -O1 进行修改的代码

void function(...)
{
   // Send char one by one on UART peripheral
   UART_put_char(...)

   // Wait end of transmission on UART
   while(end_of_transmission_flag == flase)
   {
      Dummy = Dummy == 0 ? 1 : 0;
   }
}


void UART_TxCompleteCallback()
{
   // Change flag state
   end_of_transmission_flag = true;
}

我想知道为什么 gcc 强迫我做这种修改,我是否需要改变我对代码设计的“思维方式”。或者有这种行为的解释吗?

感谢您的建议。

【问题讨论】:

  • end_of_transmission_flag 是否声明为volatile
  • ^ 它也应该被声明为原子的(或者,在 C11 之前,您必须依赖编译器扩展来保证它以原子方式读取和写入)
  • @M.M Atomic 无关紧要,除非这里涉及线程。 Volatile 对于硬件驱动程序来说已经足够了。
  • @ZanLynx 不,不是,至少一般情况下不是。例如,在 AVR 上,任何 16 位分配都是非原子的,需要通过在分配期间禁用中断来保护 ISR 不被“修改到分配的一半”。

标签: c gcc optimization embedded


【解决方案1】:

正如 cmets 中所说,您应该将 end_of_transmission_flag 声明为 volatile 以确保编译器在优化期间不会删除循环。通过声明变量volatile,编译器知道该变量将在其他地方更新。

但要调试您的问题,您不应一次更改所有带有“-O1”的文件,而应尝试将一些文件移动到-O1 并将一些文件保留在-O0

您也可以在本地禁用 GCC 优化:

#pragma GCC push_options
#pragma GCC optimize ("O0")

// your code

#pragma GCC pop_options

请注意,在嵌入式中访问硬件寄存器时必须遵守操作顺序。编译优化有时/经常改变顺序。将volatile 与寄存器地址一起使用可确保操作的顺序。

【讨论】:

  • “将 volatile 与寄存器地址一起使用可确保操作的顺序。” 我不会走那么远。它只保证内存将被显式读取,而不是重用一些以前读取的值。 stackoverflow.com/questions/1787450/…
  • 你是对的。正如您提到的链接内存/指令障碍是订购要求的答案(在 ARM 架构的情况下)
  • 那么 -Os 优化呢?似乎即使是 volatile 也不足以保留该变量。
  • 为什么你认为volatile会和-Os一起消失?
【解决方案2】:

启用优化后,编译器会尝试尽可能多地保存冗余代码。这一行:

while(end_of_transmission_flag == false);

不断检查从未修改过的变量(从编译器的角度来看)。此外,该行什么也不做(再次从编译器的角度来看)。实际上,这行一些事情:它一直等到标志改变状态。但是编译器不够聪明,无法理解这一点。所以你必须通过将标志变量声明为volatile来帮助它。这样做,您警告编译器变量可以在后台更改状态(即,在中断中)。编译器现在知道它不能依赖变量 似乎 永远不会被修改的事实——它不能“缓存”它的值来生成更快的代码。

请注意,即便如此,整行仍然什么都不做(循环内没有要执行的代码);但通常情况下,在这种情况下,编译器会理解无论如何都必须执行循环。

你的把戏“Dummy = Dummy == 0 ? 1 : 0;”似乎对编译器有一些影响,我无法向自己解释。但是,正如您已经想到的那样,这似乎真的很奇怪。该技巧解决了不执行任何操作的语句的问题,迫使编译器在循环内生成代码。但这只是问题的一半,无论如何,一个非常聪明的编译器可能会想“我为什么要编译代码来为一个从未使用过的变量赋值?当这种情况发生时,编译器经常用类似“警告:分配给 xxxxx 的值从未使用”之类的警告。

最后,将 end_of_transmission_flag 声明为 volatile

只是一个想法:许多编程语言生来就不能很好地应对硬件(端口 I/O、中断、DMA 等),C 也不例外:它迫使程序员使用奇怪的声明、编译指示、编译旗帜等。另一方面,像“while(end_of_transmission_flag == false);”这样的行有点模棱两可;真正的行应该是类似“wait_until_true(end_of_transmission_flag);”的内容。这样的事情可以由语言或环境提供(通过宏、库、硬件支持或其他方式)。

【讨论】:

  • while (X == false); 表示“等到真(X)”
  • 酷。我认为任何 C 程序员确实会这样阅读它而不会跳过一个节拍,但我并不真正遵循你的抱怨,即该语言应该为相同的效果提供一些替代语法
  • @M.M 好吧,我不会在多进程环境中写while (X == false);:有信号量、消息、信号等等。在许多情况下,该语言似乎只是将系统调用放在一起的语法粘合剂。这是不好的。如果我想等待一个变量改变状态,我会有语言支持!事实上,有现代语言有它。本页中的问题很好地说明了当语言/编译器没有针对问题领域量身定制时出现的问题。
  • 在最新的 C 标准 (2011) 中有语言支持同步
  • 是的,它又长又复杂。 C11 在 IMO 中具有足够的表现力,如果您编写的代码根据语言规范定义良好,那么行为是不可预测的
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-08-26
  • 2016-02-23
  • 1970-01-01
  • 1970-01-01
  • 2012-12-26
  • 1970-01-01
  • 2016-01-25
相关资源
最近更新 更多