又是一个拖了半年的系列,可能是前几篇主要以事实为准,举例子的文章总是比较容易写的,因此十分顺畅。而最后一篇打算做一个总结,以讲道理为主——却发现该将的似乎都已经讲完了。不过做事要有始有终,该完成的也必须要完成。那么现在就来谈谈我的一些个人看法:什么时候应该学IL,以及应该怎么学IL。
对了,先表个态,我个人并不支持普通程序员学习IL——至于什么是“普通”,什么叫做“学习”我们再慢慢谈。
经常听到有朋友有人说:如果要成为一个优秀的.NET程序员,那么IL很重要,一定要学习。这时候我经常会问一句“为什么”,得到的答复往往是“IL比较接近底层”。不过在我看来,“底层”只是IL本身的特性,并不足以证明“IL是高级程序员的必知必会”。IL的确比C#等高级语言来的所谓“底层”,但是IL本身其实也是一种高级抽象,它所能表现出来的几乎所有东西,都可以从C#等高级语言中获取到,而我们平时所了解到的如“装箱”,“引用类型和值类型的分配方式”,“垃圾收集”都属于CLR的范畴,您可以从书中看到或听人讲起,但是无法从IL上看出来。
当然,无论怎么说IL总是相对较为“底层”的东西,但是“底层”就应该学习吗?从不同层次可以获得不同信息,我们追求“底层”的目的肯定也不是“底层”这两个字,而是一种收获。所有人的时间或精力都是宝贵的,而对于一个优秀的程序员来说,知道了解自身需要什么,然后能够选择一个合理的层次进入,并得到更好的收益,这本身也是一种能力——而且可以说是必须的能力。这种能力不光是体现在有选择性的“学习”上,而可以体现在更多方面,因为几乎做任何一件事情都有多种方式,我们要选择最合适的。
方法,很重要。
学习IL的另一个重要“原因”,可能是有些朋友认为学习IL对于优化.NET程序性能有帮助,对于这个问题在之前的文章中也有过讨论,这次再拿出来一谈。性能优化是最需要“方法”的工作之一,如果方法不对,不仅仅是工作效率的问题,甚至很难得到正确的结果。IL和普通高级语言的代码一样,是一种静态的事物,您就算对它了解地再透彻,您也只能“阅读”它。而对于性能优化来说,要做的事情有很多,“阅读代码”在其中的重要性其实并不高——而且它也最容易误入歧途的一种。如果要进行性能优化,首先要进行的其实是“发现需要优化的地方”。徐宥写过这样一个八卦:
话说当年在贝尔实验室,一群工程师围着一个巨慢无比的小型机发呆。为啥呢,因为他们觉得这个机器太慢了。什么超频,液氮等技术都用了,这个小型机还是比不上实验室新买的一台桌上计算机。这些家伙很不爽,于是准备去优化这个机器上的操作系统。他们也不管三七二十一,就去看究竟那个进程占用CPU时间最长,然后就集中优化这个进程。他们希望这样把每个程序都优化到特别高效,机器就相对快了。于是,他们终于捕捉到一个平时居然占50% CPU的进程,而且这个进程只有大约20K的代码。他们高兴死了,立即挽起袖子敲键盘,愣是把一个20K的C语言变成了快5倍的汇编。这时候他们把此进程放到机器上这么一实验,发现居然整体效率没变化。百思不得其解的情况下他们去请教其他牛人。那个牛人就说了一句话:你们优化的进程,叫做System Idle。
无论这个八卦是否存在“艺术加工”,但都至少说明一个问题,就是如果没有经过合适的Profiling,没有找到性能的问题所在,优化是几乎不会有效果的。因此,我们一直说要把代码写的易于理解,说make clean code fast远比make fast code clean要容易,都是同样的道理,因为代码清晰,我们可以找出其性能瓶颈,然后有针对性地加以优化——那么,了解IL对此会有帮助吗?IL虽然并不难懂(稍后会谈到),但也比如C#代码要“间接”很多——试想,如果您使用阅读IL的方式来了解字符串连接的实现细节会是什么情况呢?
因为程序是动态的,动态的事情要用“动态的方式”地决定,也就是Profiling。而即便是最最普通的Profiling方式,如使用CodeTimer来统计时间,也比“阅读代码”要靠谱许多,因为它可以直接反应出不同实现方式之间运行时间的区别——而不是靠猜测。事实上您也可以发现,我在研究性能问题的时候,使用的最多的还是CodeTimer(如泛型问题、并发缓存容器、字符串连接)。的确,CodeTimer得到的只是“表象”,因此发现性能瓶颈可能还需要依赖Profiler——这也是我为什么在这个问题上尝试半天的原因。有了Profiler之后,发现性能的Hot Path便有更多依据了。
方法,很重要。
再次表达观点:我认为,对于一个“普通”的.NET程序员来说,是不需要去“学习”IL的。这里的“普通”二字是针对“工作内容”,而不是“人员水平”。也就是说,和“普通程序员”相对的是“工作内容特别的程序员”而不是“高级程序员”。“特殊”是指您的工作需要“直接”用到IL,例如您如果想要写一个.NET上的新语言,就必须了解IL,了解怎样使用IL写程序。而“直接”二字自然是和“间接”二字对应,或者说您的工作中会“顺便”用到IL——例如,您像我一样想要写一个延迟加载类库,或者为NHibernate实现个通用的UserType支持——在这种情况您可能也并不需要花功夫去学它。事实上,按照我的分类方式,我也是个普通程序员,所以我没有学IL,我很难看懂复杂的IL代码,更不会用IL写程序,但这并不影响我完成一些简单的IL相关工作——这并不矛盾。
为什么这么说呢?因为IL和C#这样的高级语言实在太接近了。IL几乎只是C#的另一种表现形式,它和C#一样,都几乎直接表现出.NET中定义的各种概念:泛型、数组、类、接口、继承、异常……甚至连部分关键字都一样。还有相当重要的一点,它们都是命令式的语言,因此.NET Reflector可以将其“还原”成C#代码,因为在这个过程中实在没有丢失太多信息。但是,如果想要把IL还原成F#,这就非常困难了,因为F#和IL无法做到十分对应。因此,您在通过C#学习.NET时,已经在不断降低IL的门槛了。最后您会发现,IL真不是什么特别的东西。
于是,在用到Emit的时候,您就可以先写一些C#目标代码并编译成程序集,然后用.NET Reflector将其反编译成IL,一条一条指令地“抄”至程序中——甚至现在已经有了插件来生成这些Emit代码。同样,只要有些耐心和细心,修改个程序集可能也只是“写C#”,“反编译”,“复制/粘贴”的过程而已。
很多时候,IL也只是被“神化”了。因此会有朋友觉得,了解IL的就是高手,要成为高手必须学习IL——其实IL和水平高低的关系并不大。多说一句,即便是所谓“底层工作者”,这也是在不同抽象上办事,并不代表他们一定就是牛人。我之前谈的很多东西,其实也是想打破这个“IL神话”罢了。有时候我也纳闷,为什么Java平台上学习Byte Code或JVM的“氛围”就远不如.NET平台上学习IL和CLR呢?
当然,如果您真心希望,您感兴趣,自然可以学习IL——多了解一些总是好的。我建议,如果您真要学习IL,可以先去学习一些汇编——这么说似乎也不够确切,因为我始终认为它们的可比性并不大——只是从“表现形式”上来说,IL和汇编略有相似(例如逻辑跳转方面)。您可以先看看《深入理解计算机系统》这本书,它讲的不是IL,它讲的是“计算机系统”。在学习过程中会涉及到一些汇编——甚至您可以直接翻到对应章节学习这部分内容。您无需“学完”,只需要大概了解一下,再配合C#编程经验,就会发现IL其实“真的很直观”,此时您可能已经不太会去找一本讲IL的书去特意学习IL编程了。
嗯,“找一本讲IL的书去特意学习IL编程”,这就是我在文章开始提到的学习方式——我不推荐这种做法。