最近一直被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,我相信即使换用Java或者Python,效果都不会好到哪里去。即便最大限度避免锁的等待,高并发带来的上下文切换仍然是很严重的开销。和很多人一样,我习惯了用易用的工具开发各种不要求性能的程序。而今天简单一块10W-TDP的CPU告诉我,这样的程序瓶颈很低。当然我也可以试着寻找更好的开源库,或者自己实现性能更佳的轮子,但我意外发现其实有更简单的解决方案。

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

非阻塞是个流行了好几年的词汇,但dotnet异步标准库的实现仍然基于底层线程池,且poll机制与平台高度相关,因此在Linux上的实测体验并不理想,在某些情况下性能甚至低于直接使用线程池——例如短时间内的流量突峰——线程池好歹还能方便地做线程生成速度的管理。而Golang原生的用户态线程,带来的轻量开销,从源头上就解决了这些问题。

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

发表评论

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