最近一直被dotnet的性能问题所困扰。起因是将基于dotnet的程序部署到低功耗平台,发现每次触发GC都会带来难以接受的服务挂起。不得已给程序加上对象池,却引入严重的资源竞争,哪怕我已尽可能减少锁的碰撞(引入分段锁等手段),低功耗处理器仍然难以应付。很显然,dotnet的Concurrent官方库并没有坊间所传扬的那样性能强劲。果然,我在M$ Docs找到《何时使用线程安全集合》。

In pure producer-consumer scenarios, where the processing time for each element is very small (a few instructions), then System.Collections.Concurrent.ConcurrentQueue can offer modest performance benefits over a System.Collections.Generic.Queue that has an external lock.

网上有人说dotnet的ConcurrentQueue采用了无锁结构,但从性能表现来看恐怕并不是。

其实这不能怪dotnet,我相信即使换用Java或者Python,效果都不会好到哪里去。即便最大限度避免锁的等待,高并发带来的上下文切换仍然是很严重的开销。和很多人一样,我习惯了用易用的工具开发各种不要求性能的程序。而今天简单一块10W-TDP的CPU告诉我,这样的程序瓶颈很低。

最后我试着改用Golang,却发现事情变得意外的简单。倒不是说Golang就没有同样的问题,只因为使用Golang,便天然拥有异步非阻塞的特性,加上编译型语言的性能优势,大量的CPU时间被节约出来,填上了数据竞争或GC的坑。直观体验,使用go带来的并发能力是数倍于dotnet线程池的。其内存占用却只有原来的几分之一。

其实非阻塞设计我早就想启用,但那意味着要将程序的代码结构全部推倒重新设计,对我来讲这很难接受。更重要的是,dotnet非阻塞标准库的底层实现与平台高度相关,在Linux上的实测体验实在不堪(在某些情况下效率甚至低于线程池)。而Golang就是瞌睡临头的枕头,让我能够在很短的时间内,“不经大脑”地复刻原版C#代码。

当然,Golang并非完美的工具——尽管起初我以为它很完美。我会在《初用Go语言时易犯的错误》里记录我犯的错误。我认为其中某些错误Golang本身应该部分负责。

发表评论

电子邮件地址不会被公开。 必填项已用*标注