Mozilla披露改进macOS Firefox响应性的细节

经过Mozilla的改进,从Firefox 103版本开始,macOS上的Firefox更顺畅,回应能力明显提高,特别是用户打开许多标签,或是计算机忙于执行其他应用程序的时候,Firefox现在能更即时地回应用户的操作,而官方解释,这是通过调整内存分配器中锁实做的方法完成的。

Firefox在所有架构中使用高度自定义的jemalloc内存分配器版本,以达到Firefox最佳性能和内存使用,内存分配器必须为线程安全,而且为了提高性能,还需要为来自不同线程的大量平行请求提供服务,为了实现这件事,jemalloc在内部结构使用锁。

分配器中锁实例方法和程序代码库的其余部分都不同,具体来说,创建和使用互斥锁不能发出新的内存分配要求,因为这会导致分配器本身无限递归,所以分配器倾向使用底层操作系统的原生Thin Lock,而在macOS中,Firefox一直都是使用OSSpinLock的锁实例。

而OSSpinLock自旋锁(Spin Lock)并非一般的互斥锁,互斥锁在被一个线程占用时,则使另一个试图取得锁的线程进入睡眠状态,自旋锁则是当线程要锁定一个已经被锁定的OSSpinLock实例,该线程会一直忙于轮询该锁,而非睡眠等待锁释放,这个行为被称为自旋。

Mozilla解释,虽然自旋会消耗CPU周期和电力,但是让线程睡眠却会对性能产生更重大的影响,要将睡眠中的线程唤醒,需要两次上下文切换,经历将线程状态存入内存和恢复的过程,而且线上程恢复时,可能会在不同核心上执行,该核心可能充满不相关的资料。这些因素会让线程采用互斥锁比自旋锁速度更慢。

所以当线程试图获得的锁仅会锁定很短暂的时间,那让线程短暂自旋可能是相对有利的,付出的成本也会低于睡眠。但自旋锁缺点便是自旋太久会造成计算资源浪费,特别是当计算机负载本来就很重的时候,可能会降低已经拥有锁的线程运行速度,进而造成更多线程需要锁的机会,导致许多线程不停自旋。

过去Firefox在macOS上使用的OSSpinLock,在负载轻的系统提供非常好的性能,但是在负载增加时表现不佳,其具有两个最根本的缺陷,就是OSSpinLock会在用户空间中自旋,并且从来不睡眠。

用户空间不会知道系统正执行多少负载,因此如果是在核心空间,那锁就可以做出更加明智的决定,也就是当负载很高的时候便不自旋,官方提到,线上程无法获得锁的时候,不睡眠会使状况更糟,特别是核心不知道存在等待锁的线程,又另外唤醒正在争夺同一个锁的线程,产生更多的自旋和竞争,最终可能使Firefox宕掉。

Apple因为OSSpinLock的问题决定弃用该锁实例,取而代之的是官方替代实例os_unfair_lock,但在Mozilla使用os_unfair_lock并进行自动化测试后,发现部分性能下降30%,而这个原因正是在os_unfair_lock中,线程不会因为争用而自旋,反而立即进入睡眠,这使内存分配器性能更差。

Mozilla进一步发现Apple函数库有os_unfair_lock_with_options自旋锁可供使用,由于能够于核心空间执行自旋,因此也就可以在适合的时机执行加载和调度。在使用新的自旋锁后,Firefox在轻负载系统上的性能与OSSpinLock大致相同,在负载较重的系统能够提供更好的回应,并且降低自旋造成的电力浪费。