之前在群里和群友讨论,如果你不打算搞纯理论方向的研究,那么学习理论知识究竟有什么意义,就拿计算机语言说事,看上去现在的绝大部分语言已经是开箱即用的了,不需要你去了解底层的原理,比如代码是如何被机器理解的,比如你写出来的烂代码是怎么被自动优化的,在编译器已经如此智能的今天,学习编译器设计或编程语言相关理论的用处到底是什么呢?
  其一,是让你知道自己应该写什么样的代码,不应该写什么样的代码,举个例子,你知道了正则表达式引擎的原理,你就知道什么样的正则表达式可能引发问题(相关问题:正则回溯),在遇到问题的时候,也不至于晕头转向,再比如常见的内存问题,代码的性能问题,你知道虚拟机和编译器是如何处理接口的,你就知道什么时候使用接口会降低性能,什么时候使用接口不会降低性能(相关优化:Devirtualization),你知道编译器是如何对待异步对象和闭包的,你就知道为什么使用async/await容易在内存中留下大量内存碎片(相关主题:CPS变换),更进一步的,如果你知道闭包和普通匿名函数的区别以及编译器是如何对待他们的,你就能写出更高性能的代码(举个例子,CommunityToolkit.HighPerformance中的Messenger通过增加匿名函数的参数避免闭包创建,从而优化性能),你知道struct什么时候会分配在栈上,什么时候会逃逸到堆上(相关主题:装箱与拆箱,Java不存在这个问题,因为Java目前没有struct,而是会使用逃逸分析自动将确定不会逃逸到堆上的对象改为栈上分配)。在了解这些东西你以后,你就能在实际写代码的时候避免去写出对内存/运行速度来说不利的代码,没学过的时候也许会觉得这些东西是编译器的魔法,想要记住如何使用只能死记硬背,但是如果你把上面提到的理论和实践有机结合起来,这些东西就显得非常自然,从掌握的知识中就能把这些结论和其原因推导出来;就像在学习中背公式和推导公式,背单词和记词根以及构词法的区别一样,换言之,也就是“鱼”和“渔”,前者也许可以在数量级较小的时候给你带来较高的效率,但是随着你拓宽知识面,增加学习深度,它最终一定会变成一个痛点;而后者虽然相比之下起步较慢,但是在掌握之后便能以不变应万变,让自己在海量的知识洪流中宛如中流砥柱,事半功倍。
  其二,是避免做无用功,一个例子就是让自己明白什么样的优化是不需要的,之前看到一篇别人发给我的文章讲优化C#的foreach循环在数组上面的性能,认为foreach在数组上居然会调用GetEnumerator()等一大堆乱七八糟的东西简直是舍近求远,明明直接用[]运算符取下标不就好了,文中又是看MSIL又是上WinDbg,诸多宝器轮番上阵,最后得出一个把foreach换成for即可的结论。在看完这篇文章之后我颇觉奇怪,foreachGetEnumerator()CurrentMoveNext()的语法糖这一点应该是人尽皆知的事实,任何情况下foreach都会生成使用这三个方法的MSIL,仔细思考之后,我总结出了文中的几点漏洞:

  1. 也就是上面提到过的,foreach任何情况下都会生成使用上述三个方法的IL,数组能使用foreach是因为其在CLR中对应的类本身也包含了GetEnumerator()方法,所以当然落入此类中
  2. 基于MSIL看优化手段是不准确的,因为大量的优化手段在JIT编译过程中才会被应用,MSIL仅仅是Roslyn生成的中间表示,很多优化在其身上是体现不出来的
  3. C#早就拥有了数组的foreach优化,会将其自动转换为for循环,在Java中有一个RandomAccess接口同样支持此操作,因此完全没有必要去手动优化
  4. foreach基于Duck Typing,这甚至在一定程度上在很多场景下避免了虚方法调用开销

之所以该文章会花时间进行一个没必要的微优化,答案就是对这些知识缺乏掌握,而这些东西在很多编译器设计相关文献中都有提及,甚至是MSDN和dotnet/runtime这两个面向开发者的文档和仓库都有不太深入但已经足够帮助你认识到上面几点的介绍。
  当然,这并不是说注重实践就是坏的,毕竟,所谓理论都是人们从实践中发现,总结,并形式化的经验和规律,不注重实践只注重理论,必然会造成纸上谈兵,而只注重实践不注重理论,则难免南辕北辙,把两者结合起来,才能在乘前人之风遨游天地的同时保留自己的求知欲和洞察欲。很多看理论文献时候绞尽脑汁的东西,自己去动手实现一下就会迎刃而解,而很多看实现看不出名堂的复杂结构,了解一些其基于的理论就会拨云见日。合理的管理这两者之间的关系,根据自身实际情况来决定两者的占比,才是工程相关领域的正确学习思路。(仅为个人意见)

LG如是说:



We Choose to Go to the Moon