.NET程序性能的基本要领

  • Post author:
  • Post category:IT
  • Post comments:0评论

说起Roslyn大家肯定都已经有所耳闻了,这是下一代C#和VB.NET的编译器实现。Roslyn使用纯托管代码开发,但性能超过之前使用C++编写的原生实现。Bill Chiles是Roslyn的PM(程序经理,Program Manager),他最近写了一篇文章叫做《Essential Performance Facts and .NET Framework Tips》,其中总结了几条经验,目前是个CodePlex上的PDF文件,以后可能会发布在MSDN上。

他在文章里谈到以下几点:

  1. 不要进行过早优化。程序员有了一定经验以后,往往会对性能有所直觉,但也要避免盲目优化。
  2. 没有评测,便是猜测。例如,有的时候重复计算都比使用哈希表进行缓存来的快。
  3. 好工具很重要。这里他推荐了PerfView,这是个微软发布的免费工具,将来分析某些案例时我可能也会用到这个工具。
  4. 性能的关键,在于内存分配。凭直觉可能很多人会觉得编译器是一个CPU密集型的场景,但实际上它终究还是个IO密集型的程序。
  5. 其他一些细节。例如,对于字典的内存开销要有一些概念,还有例如我每次面试都会问到的class与struct的区别等等。

第4点值得多说几句。对于托管环境来说,GC对于性能的影响重大。假如一段程序写的不够GC友好,让GC发生的多,尤其是那种Stop-the-World GC,这对性能的影响远胜某些“多花了几条拷贝指令”之类的“探索”。而且很多时候,用户眼中的“性能”在于程序的“响应程度(responsiveness)”,一旦GC暂停了所有的线程,程序便很容易发生卡顿,这甚至不是通过简单评测程序性能能够体现出来的。

相较于Java平台来说,.NET已经是个相对GC友好的运行环境了。其中最重要的方面之一便是自定义值类型,即struct。struct让程序员进行一定程度上可控的内存分配,避免在堆上产生对象。而在Java中,只有几种原生类型是值类型,它们还不能包含成员。要知道在Java里无法使用一个未装箱的int值作为一个字典的键,这对一个.NET程序员来说可能很难想象,但事实便是如此。

当然,Java似乎已经有打算作这方面的改进,但离真正可用还遥遥无期。目前Java只能通过一些如逃逸分析的手段,发现某个对象不会被共享到堆上,于是便将其分配在栈上,避免对GC产生压力。

不过.NET提供再多对GC友好的功能,也抵不过开发人员的误用。Bill的文章里举了一些常见案例,这些其实都是每个.NET开发人员必须了解的基础。最后那个例子颇为有趣,他谈到,对于性能敏感的地方,有时候都要避免LINQ或Lambda。因为使用Lambda构造匿名函数时,编译器会产生闭包,因为所谓闭包,便是一个用来保存上下文的,分配在堆上的对象。此外,如List<T>的迭代器被有意实现为struct,但使用通用的LINQ接口,则会被转化为IEnumerable<T>和IEnumerator<T>,进而产生装箱。

无独有偶,不久前@连城404在新浪微博上说到:

按照Michael的建议把HiveTableScan关键路径上的FP风格的代码换成while循环加可复用的mutable对象,扫表性能提升40%。”,这其实也正和这次的话题密切相关。

半夜不清醒,四则运算都算错了…发上条微博的时候实际上是提升了100%多点。之后继续蚊子腿上刮肉,不光是while循环,关键路径上的模式匹配代码也能刮出油水(例如省掉Array.unapplySeq调用开销)。目前使用LazySimpleSerDe的普通CVS表扫表性能提到2.2x,RCFile加上列剪枝可以提到3x。#Spark SQL#

Alan Perlis同样说过:

Lisp programmers know the value of everything but the cost of nothing.

可谓颇为有趣。常用的FP手段的确会带来性能开销,这是事实,不过假如你现在立即得出“不要用FP”或“还好没学FP”这样的结论,那我也只能用怜悯的眼光看着你了。

最后,您有减少内存分配,优化GC这方面的实践吗?不妨联系我吧,有机会我也会谈一下我在这方面的一些技巧和案例的。

发表评论