我怎么知道编译器是否破坏了我的代码?如果是编译器,我该怎么办?


14

偶尔进行某些级别的优化时,C ++代码将无法工作。可能是编译器进行了优化而破坏了代码,也可能是包含未定义行为的代码,这些行为使编译器可以做任何感觉。

假设我有一些仅在使用更高的优化级别进行编译时会中断的代码。我怎么知道是代码还是编译器,如果是编译器怎么办?


43
最有可能是你。
littleadv

9
@littleadv,甚至gcc和msvc的最新版本都充满了错误,因此我不确定。
SK-logic

3
您已启用所有警告吗?

@ThorbjørnRavn Andersen:是的,我确实启用了它们。
sharptooth 2011年

3
FWIW:1)我尽量不要做任何棘手的事情,以免诱使编译器搞砸,2)优化标志(对于速度)至关重要的唯一地方是代码中程序计数器花费大量时间的地方。除非您编写紧密的cpu循环,否则在许多应用程序中,PC基本上将所有时间都花在库或I / O中。在这种应用中,/ O开关根本无法帮助您。
Mike Dunlavey

Answers:


19

我可以肯定地说,在大多数情况下,损坏的是您的代码而不是编译器。即使是在非常特殊的情况下(当它是编译器时),您也可能会以一种不寻常的方式使用一些晦涩的语言功能,而这些特性并未为特定的编译器做好准备;换句话说,您很可能会将代码更改为更惯用的语言,从而避免了编译器的薄弱环节。

无论如何,如果可以证明发现了编译器错误(基于语言规范),请向编译器开发人员报告该错误,以便他们可以在某个时间对其进行修复。


@ SK-logic,很公平,我没有任何统计数据来支持它。它基于我自己的经验,我承认我很少扩展语言和/或编译器的限制-其他人可能会更频繁地这样做。
彼得Török

(1)@ SK-Logic:刚发现一个C ++编译器错误,相同的代码,在一个编译器上尝试并运行,在另一个编译器上尝试却失败了。
umlcat 2011年

8
@umlcat:很可能是您的代码,具体取决于未指定的行为;在一个编译器上,它符合您的期望,在另一个编译器上,它不符合您的期望。那并不意味着它坏了。
哈维尔

@Ritch Melton,您使用过LTO吗?
SK-logic

1
在谈论游戏机时,我同意Crashworks的观点。在那种特定情况下发现深奥的编译器错误一点也不罕见。但是,如果您使用的是频繁使用的编译器作为普通PC的目标,那么您就不太可能碰到以前没人见过的编译器错误。
Trevor Powell,2012年

14

和其他错误一样,请执行一个受控实验。缩小可疑区域的范围,关闭其他所有功能的优化,然后开始更改应用于该代码块的优化。一旦获得100%的可重复性,就开始更改代码,引入可能破坏某些优化的内容(例如,引入可能的指针别名,插入可能产生副作用的外部调用等)。在调试器中查看汇编代码也可能会有所帮助。


可能有什么帮助?如果是编译器错误-那又如何?
littleadv

2
@littleadv,如果它是编译器错误,则可以尝试对其进行修复(或仅对其进行详细详细地报告),或者,如果将来注定继续使用此功能,则可以找到避免该问题的方法。版本的编译器有一段时间。如果它与您自己的代码有关,这是无数C ++边界问题之一,则这种检查还有助于修复错误并避免将来发生此类错误。
SK-logic

因此,正如我在回答中所说的-除了报告外,治疗方法没有太大区别,无论是谁的过错。
littleadv

3
@littleadv,如果不了解问题的性质,您可能会一次又一次地面对它。通常,您可以自行修复编译器。而且,是的,不幸的是,在C ++编译器中发现错误并不是完全不可能。
SK-logic

10

检查产生的汇编代码,并查看其是否满足您的源代码的要求。请记住,以某种非显而易见的方式,确实是您的代码有过错的可能性很高。


1
这确实是这个问题的唯一答案。在这种情况下,编译器的工作是将您从C ++转换为汇编语言。您认为它是编译器...请检查编译器的工作情况。就这么简单。
old_timer 2012年

7

在30多年的编程过程中,我发现的真正的编译器(代码生成)错误的数量仍然仅为〜10。在同一时期内发现并修复的我自己的(以及其他人的)错误的数量可能是> 10,000。我的“经验法则”是,任何给定的错误是由编译器引起的,概率小于0.001。


1
你真幸运 我的平均值是每个月大约有1个真正的错误漏洞,而次要的边界问题则经常出现。而且,您使用的优化级别越高,编译器错误机会就越高。如果您尝试使用-O3和LTO,那么很幸运没有立即找到其中的几个。在这里,我仅数发行版中的错误-作为编译器开发人员,我在工作中面临更多此类问题,但这并不重要。我只知道破坏编译器是多么容易。
SK-logic

2
25年了,我也看到了很多。编译器每年都在恶化。
old_timer 2012年

5

我开始写评论,然后决定它的时间太长,太过分了。

我认为是您的代码被破坏了。万一您发现了编译器中的错误,您应该向编译器开发人员报告该错误,但这就是区别的地方。

解决方案是识别有问题的构造,然后对其进行重构,以使其执行相同的逻辑不同。无论是在您身边还是在编译器中,这最有可能解决问题。


5
  1. 重新阅读您的代码。确保您没有在ASSERT或其他调试(或更一般的配置)语句中产生副作用。还要记住,在调试构建中,内存的初始化方式有所不同-告诉指针值,您可以在此处进行检查:调试-内存分配表示形式。在Visual Studio中运行时,几乎总是使用调试堆(即使在发布模式下也是如此),除非您使用环境变量明确指定了这不是您想要的。
  2. 检查您的构建。在实际编译器以外的其他地方遇到复杂构建的问题很常见-依赖关系常常是罪魁祸首。我知道“您尝试过完全重建”几乎像“您尝试过重新安装Windows”一样令人发指,但它确实可以提供帮助。尝试:a)重新启动。b)手动删除所有中间文件和输出文件并重建。
  3. 查看您的代码以检查是否有可能调用未定义行为的潜在位置。如果您使用C ++已有一段时间,您会知道有些地方会认为“我不确定是否允许我假设...”-在Google上搜索或在此处询问有关特定信息的信息查看是否为未定义行为的代码类型。
  4. 如果仍然不是这种情况,请为引起问题的文件生成经过预处理的输出。意外的宏扩展会带来各种各样的乐趣(我想起一个同事用H的名字决定宏的时间是个好主意...)。检查预处理后的输出,以了解项目配置之间的意外更改。
  5. 不得已-现在您确实在编译器错误领域-查看程序集输出。这可能需要花些时间进行挖掘和战斗,才能掌握程序集的实际操作,但实际上很有帮助。您也可以使用在这里获得的技能来评估微优化,因此一切都不会丢失。

为“未定义的行为” +1。我被那个咬了。编写一些依赖于int + int溢出的代码,就像编译成硬件ADD指令一样。使用旧版本的GCC编译时,效果很好,但使用较新的编译器编译时,效果不佳。显然,GCC的好心人决定,由于整数溢出的结果是不确定的,因此他们的优化器可以在假设它永远不会发生的情况下运行。它根据代码优化了一个重要的分支。
所罗门慢

2

如果您想知道它是代码还是编译器,则必须完全了解C ++的规范。

如果仍然存在疑问,则必须完全了解x86程序集。

如果您不打算同时学习两个方面的知识,那么编译器根据优化级别做出不同的解析几乎可以肯定是一种不确定的行为。


(+1)@mouviciel:即使在规范中,也取决于编译器是否支持该功能。我的gcc有一个奇怪的错误。我用“函数指针”声明了一个“纯c结构”,它在规范中是允许的,但是在某些情况下可以工作,而在其他情况下则不能。
umlcat 2011年

1

比优化器错误更容易在标准代码上遇到编译错误或内部编译错误。但是我听说编译器错误地优化了循环,忘记了方法引起的一些副作用。

我对如何知道它是您还是编译器没有任何建议。您可以尝试其他编译器。

有一天,我想知道是否是我的代码,有人向我建议了valgrind。我花了5到10分钟来用它运行程序(我认为valgrind --leak-check=yes myprog arg1 arg2确实有,但是我使用了其他选项),它立即向我显示了在一种特定情况下出现问题的一行。自那时以来,我的应用一直运行平稳,没有出现奇怪的崩溃,错误或奇怪的行为。valgrind或其他类似工具是了解您的代码是否正确的好方法。

旁注:我曾经想知道为什么我的应用程序的性能糟透了。事实证明,我所有的性能问题也都在同一行。我写了for(int i=0; i<strlen(sz); ++i) {。sz是几MB。出于某种原因,即使在优化之后,编译器也每次都运行不畅。一条线可能很重要。从表演到崩溃


1

越来越普遍的情况是,编译器破坏了为C语言的方言编写的代码,这些代码支持标准未规定的行为,并允许针对这些方言的代码比严格符合标准的代码更有效。在这种情况下,将“破损”代码描述为在实现目标方言的编译器上100%可靠的代码,或者将处理不支持所需语义的方言的编译器描述为“破损”是不公平的。 。相反,问题仅源于以下事实:启用了优化的现代编译器处理的语言与曾经流行的方言有所不同(并且许多禁用了优化甚至甚至启用了优化的编译器仍在处理该方言)。

例如,为方言编写了许多代码,这些方言将gcc解释的标准未规定的许多指针别名模式识别为合法,并利用这些模式使代码的直接翻译更加可读和高效。比gcc对C标准的解释要高。此类代码可能与gcc不兼容,但这并不意味着它已损坏。它仅依赖于gcc仅在禁用优化的情况下支持的扩展。


好吧,只要对C标准X +扩展Y和Z进行编码,就可以确定没有什么错,只要这给您带来了明显的好处,您知道自己做到了,并对其进行了详细记录。可悲的是,这三个条件通常都不满足,因此可以说代码已损坏。
Deduplicator

@Deduplicator:C89编译器被升级为与其先前版本兼容,并且与C99等兼容。尽管C89对以前在某些平台上未定义但在其他平台上未定义的行为没有任何要求,但向上兼容性表明C89编译器适用于已经将行为视为已定义的平台应继续这样做;将短无符号类型提升为符号的理由表明,无论标准是否强制要求标准的作者,都希望编译器以这种方式行事。进一步...
超级猫

...对别名规则的严格解释将使向上的兼容性无法实现,并使许多代码无法工作,但进行一些细微调整(例如,确定应预期并因此允许交叉类型别名的某些模式)将解决这两个问题。该规则的整个陈述目的是避免要求编译器做出“悲观”别名假设,但是给定“ float x”,即使“ foo((int *)&x)”不会不会写任何类型为“ float *”或“ char *”的指针被视为“悲观”还是“明显”?
supercat

0

隔离问题点,并将观察到的行为与根据语言规范应发生的行为进行比较。绝对不容易,但这就是您必须了解的(而不仅仅是假设)。

我可能不会那么细致。相反,我会问编译器制造商的支持论坛/邮件列表。如果确实是编译器中的错误,那么他们可能会修复它。无论如何,可能是我的代码。例如,关于线程中的内存可见性的语言规范可能会很违反直觉,并且只有在某些特定的硬件(!)上使用某些特定的优化标志时,它们才会变得明显。规范可能无法定义某些行为,因此它可以与某些编译器/某些标志一起使用,而不能与其他某些一起使用,等等。


0

您的代码很可能具有某些未定义的行为(正如其他人所解释的那样,与C编译器相比,即使C ++编译器如此复杂以至于它们确实存在错误,您在代码中也比在编译器中更容易出现错误;即使C ++规范也存在设计错误) 。即使已编译的可执行文件碰巧可以正常工作,UB也可以在这里。

因此,您应该阅读Lattner的博客,有关每个C程序员应该了解的未定义行为(大多数内容也适用于C ++ 11)。

Valgrind的工具,以及最近-fsanitize= 的仪器选择GCC(或铛/ LLVM),也应该是有帮助的。当然,请启用所有警告:g++ -Wall -Wextra

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.