在开发和调试阶段禁用优化真的是一个好习惯吗?


15

我已经阅读了C语言中的《编程16位PIC单片机》,书中有这样的肯定:

但是,在项目的开发和调试阶段,最好禁用所有优化,因为它们可能会修改所分析代码的结构,并使单步执行和断点放置成为问题。

我承认我有些困惑。我不知道作者是因为C30评估期而这么说还是真的是一个好习惯。

我想知道您是否实际使用此做法,为什么?

Answers:


16

这在整个软件工程中都是很标准的-当您优化代码时,只要您不知道操作上的任何区别,编译器就可以按照自己的意愿重新安排很多事情。因此,例如,如果您在循环的每次迭代中初始化变量,而从不在循环内更改变量,则允许优化器将该初始化移出循环,以免浪费时间。

它也可能会意识到您计算了一个数字,然后您在覆盖之前不执行任何操作。在这种情况下,它可能会消除无用的计算。

优化的问题在于,您需要在一些代码上放置一个断点,优化器已将其移走或消除了。在这种情况下,调试器将无法执行您想要的操作(通常,它会将断点放置在附近)。因此,为使生成的代码更类似于您编写的代码,请在调试过程中关闭优化-这可确保确实存在要中断的代码。

您需要注意这一点,但是,根据您的代码,优化可能会破坏事情!通常,由运行正常的优化器破坏的代码实际上只是无法解决问题的错误代码,因此您通常想弄清楚为什么优化器会破坏它。


5
与该论证相反的是,优化器可能会使事情变得更小和/或更快,如果您的代码受时间或大小的限制,那么您可能会通过禁用优化来破坏某些东西,并浪费时间调试没有解决问题的程序。真的存在。当然,调试器可能会使您的代码变慢并且变大。
凯文·维米尔

在哪里可以了解更多信息?
Daniel Grillo 2010年

我尚未使用C30编译器,但对于C18编译器,有一份应用笔记/该编译器手册涵盖了它支持的优化。
标记

@O Engenheiro:检查您的编译器文档以了解它支持哪些优化。优化取决于编译器,库和目标体系结构而千差万别。
迈克尔·科恩

同样,不是针对C30编译器,而是gcc发布了可以应用的各种优化的长列表。如果您要保留完整的特定控件结构,也可以使用此列表进行细粒度的优化。列表在这里:gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
Kevin Vermeer 2010年

7

我已将这个问题发送给杰克·甘斯勒,这就是他的回答:

丹尼尔

我更喜欢使用发布的代码中的所有优化功能进行调试。美国宇航局说:“测试你的飞行,测试你的飞行。” 换句话说,不要进行测试,然后更改代码!

但是,有时必须关闭优化,以便调试器可以运行。我试图在我正在处理的模块中将其关闭。出于这个原因,我相信保持文件较小,说几百行代码左右。

祝一切顺利,杰克


我认为此回应中的两个段落之间存在未说明的区别。测试是指应该证明软件,软件和/或硬件正常工作的过程。调试是一个过程,在该过程中,代码逐个指令逐步执行,以查看其为何仍无法运行。
凯文·维米尔

有选择的感觉真好。因此,无论是否进行优化,测试都可以涵盖更多的品种/排列。覆盖范围越广,测试效果越好

@reemrevnivek,在调试时,您是否也在测试?
Daniel Grillo 2010年

@O Engenheiro-不。我只能在测试失败的情况下进行调试。
凯文·维米尔

6

取决于,并且所有工具(不仅是C30)都是如此。

优化通常以各种方式删除和/或重组代码。您的switch语句可能使用if / else构造重新实现,或者在某些情况下可能一起被删除。y = x * 16可能会替换为一系列左移,等等。尽管最后一种优化通常仍可逐步执行,但其主要是对获得ya的控制语句的重组。

这可能导致无法通过C代码逐步调试程序,因为您在C中定义的结构不再存在,它们已由编译器替换或重新排序为编译器认为会更快或更小的空间。这也可能导致无法从C列表中设置断点,因为断点的指令可能不再存在。例如,您可以尝试在if语句中设置一个断点,但是编译器可能已删除了该if。您可以尝试在while或for循环内设置断点,但编译器决定展开该循环,使其不再存在。

因此,如果您可以在不进行优化的情况下进行调试,则通常会更容易。您应该始终使用优化进行重新测试。这是您发现错过重要事项volatile及其造成间歇性故障(或其他怪异现象)的唯一方法。

对于嵌入式开发,无论如何您都必须谨慎进行优化。特别是在对时间要求严格的代码部分中,例如一些中断。在这些情况下,您应该对汇编中的关键位进行编码,或者使用编译器指令来确保未对这些部分进行优化,因此您知道它们具有固定的执行时间或固定的最坏情况运行时间。

另一个难题可能是将代码适合于uC,您可能需要优化代码密度以使您的代码适合于芯片。这就是为什么通常一个好主意是从一个系列中最大的ROM容量uC开始,然后在代码被锁定后选择一个较小的ROM进行制造,这是一个好主意。


5

通常,我将使用计划发布的任何设置进行调试。如果要发布优化的代码,则可以使用优化的代码进行调试。如果要发布未优化的代码,则将使用未优化的代码进行调试。我这样做有两个原因。首先,优化器可以产生足够大的时序差异,从而导致最终产品的行为与未优化的代码不同。其次,尽管大多数都不错,但是编译器供应商确实会犯错误,并且优化的代码可能会与未优化的代码产生不同的结果。因此,无论我打算发布什么设置,我都希望获得尽可能多的测试时间。

话虽如此,优化器会使调试变得困难,如先前的答案所述。如果发现很难调试的特定代码部分,我将暂时关闭优化器,进行调试以使代码正常工作,然后重新打开优化器并再次进行测试。


1
但是,如果打开优化,单步执行代码几乎是不可能的。关闭优化进行调试,然后使用发布代码运行单元测试。
Rocketmagnet

3

我的正常策略是使用最终的优化程序进行开发(如果合适,最大尺寸或速度),但是如果我需要调试其他跟踪信息,则暂时关闭优化程序。这降低了由于更改优化级别而出现错误的风险。

典型的故障模式是,由于您没有在必要时将变量声明为易失性,因此不断增加的优化会导致以前看不见的错误浮出水面-这对于告诉编译器哪些事情不应该“优化”至关重要。


2

使用将要发布的任何形式,调试器和用于调试的编译会隐藏很多(很多)在编译发布之前看不到的错误。到那时,要找到这些错误要比进行调试要困难得多。到现在已有20多年了,我再也没有使用过gdb或其他类似调试器的东西,不需要监视变量或单步执行。每天数百到数千行。因此有可能,不要导致其他想法。

为调试而进行编译,然后为发行而进行后续编译,可能会花费两倍甚至两倍以上的精力。如果您陷入困境,并且必须使用调试器之类的工具,则需要进行编译以使调试器解决特定的问题,然后再恢复为正常操作。

其他问题也同样存在,例如优化器使代码更快,因此对于嵌入式而言,尤其是您使用编译器选项进行时序更改,并且可能会影响程序的功能,因此在整个阶段再次使用可交付的编译选项。编译器也是程序,有错误,优化器会犯错误,有些人对此不抱任何信念。如果是这种情况,那么不进行优化就可以编译,没错,只要一直这样做就可以了。我更喜欢的路径是进行编译以进行优化,然后,如果我怀疑编译器问题禁用了优化(如果解决了该问题),则该优化通常会来回进行,有时会检查汇编器输出以找出原因。


1
+1只是为了详细说明您的好答案:经常以“调试”模式进行编译会在未分配空间的变量周围填充堆栈/堆,以减轻较小的笔误和格式化字符串错误。如果在发行版中进行编译,您通常会遇到良好的运行时崩溃。
Morten Jensen

2

我总是使用-O0(gcc选项关闭优化功能)开发代码。当我感觉到我想开始让事情更趋于发布时,我将从-Os(针对大小进行优化)开始,因为通常可以在缓存中保留的代码越多越好,即使它不是超级超级优化的。

我发现gdb与-O0代码一起使用时效果更好,如果您必须进入程序集,则遵循起来要容易得多。在-O0和-Os之间切换还可以查看编译器对代码的作用。有时这是很有趣的教育,并且还可以发现编译器错误...那些使您烦恼的讨厌的事情,试图找出代码的问题所在!

如果确实需要,我将开始在-fdata-sections和-fcode-sections中添加--gc-sections,这使链接器可以删除实际上不使用的全部功能和数据段。您可以修补很多小东西,以尝试进一步缩小或加快速度,但总的来说,这些是我最终使用的唯一技巧,而任何较小或更快的东西我都会处理-组装。


2

是的,由于以下三个原因,在调试期间禁用优化一直是最佳实践,这已经有一段时间了:

  • (a)如果您要使用高级调试器对程序进行单步调试,则混乱程度会略微降低。
  • (a)(已过时)如果您要使用汇编语言调试器对程序进行单步调试,则不会造成太多混乱。(但是,当您使用高级调试器时,为什么还要为此烦恼呢?)
  • (b)(已过时)您可能只会运行一次此特定可执行文件,然后进行一些更改并重新编译。当编译器“优化”这个特定的可执行文件时,这将浪费一个人的时间,而这将节省不到10分钟的运行时间。(这与现代PC不再相关,后者可以在不到2秒的时间内完全优化地编译典型的微控制器可执行文件)。

许多人朝着这个方向走得更远,并且 在断言之前打开了


单步执行汇编代码对于诊断源代码实际指定的内容与实际情况有所不同的情况非常有用(例如“ longvar1&=〜0x40000000; longvar2&=〜0x80000000;”)或编译器生成错误代码的情况。我已经使用机器代码调试器找到了一些问题,我真的认为我不可能以其他任何方式找到问题。
超级猫

2

简单:优化是费时的,如果以后在开发中必须更改那段代码,则可能没有用。因此,它们很可能浪费时间和金钱。
它们对于完成的模块很有用。代码中最有可能不需要更改的部分。


2

在断点的情况下,这当然是有道理的……因为编译器可以删除实际上不影响内存的许多语句。

考虑类似:

int i =0;

for (int j=0; j < 10; j++)
{
 i+=j;
}
return 0;

可能会被完全优化(因为i从未读取过)。从断点的角度来看,它基本上跳过了所有代码,甚至根本不存在。...我想这就是为什么在睡眠类型函数中您经常会看到类似以下内容的原因:

for (int j=delay; j != 0; j--)
{
    asm( " nop " );
    asm( " nop " );
}
return 0;

1

如果您使用调试器,那么我将禁用优化并启用调试。

就个人而言,我发现PIC调试器所带来的问题多于其所能解决的问题。
我只对USART使用printf()来调试用C18编写的程序。


1

反对在编译中启用优化的大多数论点归结为:

  1. 调试麻烦(JTAG连接,断点等)
  2. 错误的软件时序
  3. sh * t停止正常工作

恕我直言,前两个是合法的,第三个不是很多。这通常意味着您有一些错误的代码,或者依赖于对语言/实现的不安全利用,或者作者可能只是对Undefined Undefined Behaviour的忠实粉丝。

“嵌入式学术界”博客对未定义的行为有一两句话要说,这篇文章是关于编译器如何利用它的:http : //blog.regehr.org/archives/761


另一个可能的原因是,打开优化后,编译器可能会很烦人。
超级猫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.