问题

如何评价《王垠:C 编译器优化过程中的 Bug》?

回答
要评价《王垠:C 编译器优化过程中的 Bug》这篇技术文章,我们需要从多个维度进行深入分析。这篇技术文章(通常指的是王垠在其博客或其他平台发表的关于 C 编译器优化问题的讨论)的核心在于揭示编译器在进行复杂优化时可能引入的软件缺陷,以及这些缺陷对程序行为的潜在影响。

文章的核心内容与主要观点:

王垠在其相关的技术讨论中,往往会聚焦于以下几个方面:

1. 优化器的复杂性与潜在的Bug: C 编译器(尤其是像 GCC, Clang 这样的开源编译器)为了提升程序运行效率,会应用大量的优化技术,如循环展开、内联函数、常量折叠、死代码消除、寄存器分配、指令调度等等。这些优化过程本身非常复杂,而且不同优化之间可能存在相互作用。这种复杂性使得编译器自身也成为一个庞大且易于出错的软件系统。王垠的文章通常会通过具体的例子来展示,即使是经过广泛使用的成熟编译器,也可能在某些特定的优化场景下产生bug。

2. 特定优化技术引发的问题: 文章可能会深入分析某个或某类优化技术是如何导致bug的。例如:
函数内联 (Function Inlining): 如果内联时没有正确处理副作用、变量作用域或别名问题,可能导致逻辑错误。
循环优化 (Loop Optimizations): 如循环不变量外提 (LoopInvariant Code Motion, LICM) 或循环展开 (Loop Unrolling),如果处理不当,可能改变循环的执行顺序或依赖关系。
别名分析 (Alias Analysis): 这是编译器优化中的一个核心难点。编译器需要确定哪些指针可能指向同一块内存。如果别名分析过于激进或过于保守,都可能导致错误。例如,编译器错误地认为两个指针指向不同的内存,从而对它们的访问顺序进行重排,但实际上它们指向同一块内存,导致数据损坏。
向量化 (Vectorization): 将循环中的操作转换为 SIMD 指令,如果处理不当,可能影响精度或行为。
寄存器分配 (Register Allocation): 错误地分配或溢出寄存器,导致变量值丢失或混淆。
指令调度 (Instruction Scheduling): 改变指令的执行顺序以利用处理器流水线,但如果忽略了数据依赖或副作用,可能导致问题。

3. 可重现性和测试的挑战: 编译器bug,尤其是优化相关的bug,往往难以重现。它们可能只在特定的代码结构、特定的编译器版本、特定的优化级别(如 `O2`, `O3`)、特定的目标架构,甚至是在一个极大的代码库中才出现。这使得发现和修复这些bug变得异常困难,需要大量的耐心和细致的调试。

4. 对开发者和用户的影响: 王垠的文章强调了这类bug的严重性。它们可能导致程序行为异常,产生难以追踪的运行时错误,甚至导致安全漏洞。对于依赖这些编译器的开发者来说,一旦遇到这类bug,可能需要花费大量时间去排查是自己的代码问题还是编译器问题,并且可能需要禁用特定的优化选项来绕过bug,这又会牺牲程序的性能。

5. 社区反馈与贡献: 像王垠这样的技术博主,通过分享自己的发现和分析,能够引起开发者社区的关注,促进编译器开发者(如 GCC 或 Clang 的维护者)对这些问题的重视,并最终推动bug的修复。这体现了开源社区协作和改进的力量。

如何评价这篇文章(或这类讨论):

要评价王垠关于 C 编译器优化 Bug 的讨论,可以从以下几个角度来衡量:

优点:

1. 揭示了技术的深度和复杂性: 文章深入剖析了编译器优化这一非常专业和复杂的领域,让读者认识到现代编译器背后隐藏的技术深度以及其固有的挑战性。这对于很多只关注代码逻辑而忽略编译过程的开发者来说,是一次很好的启迪。
2. 提供了具体的案例分析: 通常这类文章会附带可重现的代码片段、汇编代码分析,甚至是触发bug的特定编译选项。这种具象化的论证方式,使得读者能够更直观地理解问题,并且可以自己动手验证。
3. 突显了软件质量的重要性: 编译器是软件开发的基础设施,编译器的质量直接影响到所有在其之上构建的软件。文章通过揭示编译器bug,强调了软件工程中严谨的测试、验证和质量控制的重要性。
4. 激发了学习和研究的兴趣: 对于有志于深入了解编译器、底层开发或性能优化的开发者来说,这类文章提供了宝贵的学习材料和研究方向。它鼓励开发者去挑战已有的工具,而不是盲目接受。
5. 促进了社区的进步: 如前所述,通过公开讨论和分享,这类文章能够推动编译器社区对问题的关注和修复,最终提升整个生态系统的质量。

可能的局限性或需要注意的地方:

1. 非系统性教材: 这类文章通常是个人经验的分享,可能不是一个系统性的编译器优化学习教材。它可能侧重于某些特定的bug类型,而对其他方面覆盖不足。
2. 针对特定版本和架构: 编译器bug往往与特定的编译器版本、优化级别以及目标架构强相关。一篇讨论可能只针对某个特定场景,读者在借鉴时需要注意其普遍性和适用性。
3. 可能引发“过度担忧”: 虽然揭示问题很重要,但过于强调编译器bug可能会让一些初级开发者产生“编译器不可信”的担忧,从而不敢充分利用优化选项,影响程序性能。关键在于理解问题的概率和场景。
4. 修复的难度与成本: 编译器bug的修复往往是极具挑战性的,需要深入理解编译器内部机制,并且需要通过社区的同行评审和广泛测试才能被接受。文章可能更多地展示问题,而对修复过程的详细描述会比较少。
5. 信息时效性: 技术发展迅速,编译器也在不断更新。今天发现的bug可能在未来的版本中已经被修复。因此,评价文章内容时,需要考虑其发布时间。

总结来说:

《王垠:C 编译器优化过程中的 Bug》这类技术讨论,非常有价值且值得肯定。它以其深入的技术分析、具体的案例呈现和对软件质量的关注,揭示了现代编译器复杂性下的潜在陷阱,并促成了社区的进步。它鼓励开发者保持批判性思维,不仅关注自己的代码,也关注支撑我们开发的底层工具链的质量。

然而,读者在阅读时也应保持理性,认识到编译器bug的出现是概率事件,并且往往与特定环境相关。核心价值在于理解编译器的工作原理、优化技术的挑战,以及如何更有效地调试和排查问题,而非因此对优化技术产生不必要的抵触。对于开发者而言,了解这些信息有助于在遇到难以解释的程序行为时,多一个排查方向——即编译器自身可能也存在问题。

如果您能提供具体的文章链接或讨论的主题,我可以给出更具针对性的评价。但总的来说,王垠(或其他类似技术博客作者)对编译器优化bug的探讨,是技术社区宝贵的知识贡献。

网友意见

user avatar

在责怪编译器优化之前应该先看看是否有编译警告没解决。

那个未引用局部变量dead是一定报警告的,各个编译器可能会有不同的警告信息警告号,但是有警告事一定的。

我觉得工程上来说,首先要去警告,然后过静态代码检查,最后才是开优化。

一般正规点的上点规模的C/C++项目基本都要走这个流程。

因为C/C++太复杂,各种稀奇古怪的玩法太多,但是能通过编译器4级警告和静态代码检查的古怪玩法就少多了。

       void contains_null_check_after_RNCE(int *P) {   int dead = *P; // 未引用变量警告   if (P == 0)      return;   *P = 4; }      
user avatar

垠神这篇挺好的啊。写C或C++程序的时候遇到前人给埋了一大堆UB坑那真是欲哭无泪。

我上周正好刚刚撞上一个因为我们的前人写的C++代码有UB坑而造成的bug…刚修。有时候有UB坑的代码未必会立即显现出问题,因为可能(C/C++)编译器还没利用上这块UB信息;这种才是最坑爹的——前人一甩锅,后面还不得不接。

我们内部在力求

UBSan

bug free,因为有些有问题的代码就算没有立即因为UB而被优化成错误的形式,它们常常也隐含着使用不正确的问题。例如说一个经典的,由于 << 导致int overflow的问题。这种问题排查起来真是极其痛苦…

========================================

在给编译器找bug方面,

Zhendong Su

老师的研究确实好玩。同

@Wish Night

的回答,推荐感兴趣的同学去看看那系列研究。

然后推荐一组UB入门演示稿:

BKK16-503 Undefined Behavior and Compiler Optimizations – Why Your Program Stopped Working With A Newer Compiler - Linaro - SlideShare

里面涉及的一些例子或许就是垠神会感兴趣用来进一步说明的。

========================================

下面开始跑个题。垠神所引用的例子是C语言的:

       void contains_null_check(int* p) {   int dead = *p;   if (p == 0) {     return;   }   *p = 4; }      

在C(以及C++)里,对空指针解引用确实是未定义行为,所以确实可以引出垠神所引用的Chris Lattner大大文章中所描述的问题——某个编译器有没有那样做是它们的自由,关键是根据规范所述的UB它们是可以那样做的。

那么或许会有吃瓜群众想了解一下像Java这样的语言在同样的场景下会是个什么状况。我就来跑一下这个题。

重点在于:在Java里,对null解引用是有明确定义其正确行为是怎样的——要抛出NullPointerException——所以在Java里具体到这个场景没有任何问题。放个传送门:

在Java中,return null 是否安全, 为什么? - RednaxelaFX 的回答

用Java来写一个类似形式的例子:

       public class TrapDemo {   public static void demo(IntBox p) {     int dead = p.value;     if (p == null) return;     p.value = 42;   }    public static void main(String[] args) throws Exception {     demo(null);   } }  class IntBox {   public int value; }      

这里的TrapDemo.demo(IntBox)就跟垠神引用的contains_null_check(int*)例子对应。

运行这个程序的正确结果是:

       Exception in thread "main" java.lang.NullPointerException  at TrapDemo.demo(TrapDemo.java:3)  at TrapDemo.main(TrapDemo.java:9)      

而当我们用Oracle JDK8u101在Mac OS X / x86-64上,其中的JIT编译器来编译TrapDemo.demo(IntBox)方法,会发现用其中的Server Compiler(C2)会在第一次编译时编译出等价于下面形式的代码:

         public static void demo(IntBox p) {     p.value = 42;   }      

(注意:强调了“第一次编译时”。后面再展开解释)

这个形式有没有看似跟垠神引用的C语言例子的“错误形式”一样?——实际上是不一样的喔。

       void contains_null_check_after_RNCE_and_DCE(int* p) {   //int dead = *p;    // 死代码消除   //if (false) {      // 死代码   //  return;         // 死代码   //}   *p = 4; }      

上述Java例子的C1与C2初次编译的详细结果我放在gist里了,免得这个回答太长:

gist.github.com/rednaxe

上面的JIT编译结果对Java来说为啥是正确的,待我慢慢道来。

解引用(dereference)动作隐含着null检查,如果被解引用的引用为null则需要当场抛出NullPointerException。这个语义是完全定义好的,没有回避的余地。

所以例子的原始形式,把null检查显式写出来的话,是这个样子的:

         public static void demo(IntBox p) {     if (p == null) throw new NullPointerException(); // implied null check     int dead = p.value;     if (p == null) return;     if (p == null) throw new NullPointerException(); // implied null check     p.value = 42;   }      

即便p.value的结果被赋值给了一个无用的局部变量(int dead),使得p.value的值自身并没有被使用,但它的副作用——null检查——则必须留下。

<- 这个由规范所强制要求的行为,就是Java版例子与原本的C版例子最大的不同。

把 int dead = p.value; 这句无用代码消除并留下null检查的副作用之后,剩下的代码是:

         public static void demo(IntBox p) {     if (p == null) throw new NullPointerException(); // implied null check     if (p == null) return; // 'return' now becomes unreachable code     if (p == null) throw new NullPointerException(); // implied null check     p.value = 42;   }      

于是通过条件常量传播(conditional constant propagation)把相同条件的代码合并在一起,剩下的代码就只有:

         public static void demo(IntBox p) {     if (p == null) throw new NullPointerException(); // implied null check     p.value = 42;   }      

然后从这里就开始就有更有趣的事情了。

JVM对上面要实现JVM规范,而对下面则是依托于底层的具体平台。所以一个JVM实现可以用尽各种平台相关的办法,来实现出对上层Java应用来说一致的、符合JVM规范的行为。

在Mac OS X(以及诸如Linux等各种POSIX平台)上,对0地址表示的空指针以及0地址附近的一定范围内解引用(读或者写),会可靠地触发SIGSEGV信号。

利用这个平台相关行为,JVM实现就可以采用“隐式空指针检查”(implicit null check)方式来对通常非null的引用的解引用动作进行优化,而不需要显式生成null检查的代码。JVM可以给这些使用了隐式空指针检查的地方关联上一定的符号信息,并且向OS注册SIGSEGV信号的处理函数,在里面查询看fault pc是不是一个已知的隐式空指针检查指令,如果是的话则根据关联的符号信息分派到相应的处理代码去。

回到上文的例子,C2初次编译实际编译出来的代码逻辑是这样的:

         public static void demo(IntBox p) {     p.value = 42; // implicit null check: dispatch to Label_null_check     return;  Label_null_check:     uncommon_trap(Reason_null_check); // go back to interpreter and throw NPE   }      

于是当p不是空指针的时候,这个代码就可以最快速度完成有用的写操作并返回;而当p真的是空指针的时候,它在尝试对p.value做写操作的时候就会触发SIGSEGV,然后经由HotSpot VM注册的信号处理函数跳转到Label_null_check的地方去抛出NullPointerException。

(HotSpot VM在Windows上的实现则是通过SEH来达到同样的隐式空指针检查的效果。微软自家的CLR里的编译器也有同样的优化)

细心的同学可能会留意到上文中的一些细节:如果在代码中某个位置,被解引用的引用绝大多数情况都不是null,那么用上面的隐式空指针检查显然是最快的,因为这个检查是硬件完成的,无论是否利用它硬件都得做这个检查,利用隐式检查可以避免生成显式的null检查+分支。

但如果这个位置上时常会遇到对null解引用,隐式空指针检查就不是最快的了。事实上如果null的情况占多数的话,这种需要通过发信号 -> 信号处理 -> 跳转到空指针检查的后续处理代码的路径,比起直接生成显式检查的路径要长得多也慢得多。所以这种“优化”并不是总是值得的。

HotSpot VM的C1追求实现简单,只针对常见情况优化,它在可以使用隐式空指针检查的平台上会总是选择生成这种形式的代码。

Oracle JDK8u101的C1编译出来的上面的例子是这样的形式:

         public static void demo(IntBox p) {     p.value;      // implicit null check: dispatch to Label_null_check     if (p == null) return;     p.value = 42; // no null check here     return;  Label_null_check:     uncommon_trap(Reason_null_check); // go back to interpreter and throw NPE   }      

嗯…有改善空间。

而C2则追求高性能,所以当它发现某个被C2 JIT编译过的方法遇到了至少3次隐式空指针异常之后,就会抛弃这个JIT编译的版本,然后重新JIT编译并生成显式空指针检查的代码:

         public static void demo(IntBox p) {     if (p == null) throw new NullPointerException(); // implied null check, explicit check     p.value = 42;   }      

一个例子可以引出很多有趣的讨论对不对? >_<

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有