非一致性内存访问模型与内存分配器

CPU主频涨不上去了,一直停留在2-3G。前端总线的时钟频率也涨不上去了,我现在用的这个小黑,Intel Core2 P8600,前端总线的时钟频率只有266MHz。于是,虽然内存越来越便宜了,但是没有那么大的高速带宽来连接CPU和内存啊。

于是NUMA出现了。CPU组成node,每个node各自管理几十G内存,然后node和node之间通过Point-to-Point的方式建立高速直连。于是系统总线就没了,出现一个新名词,QPI,指那个快速访问通道,它不光连接内存和CPU,还连接其它外设如显卡。但是有个重要的结果是:CPU到每根内存条的"距离"是不相等的。有的是直连,所以速度很快,而有的需要绕到另一个node去拿,这样不仅速度慢,而且很容易把node之间的那个互连通道堵死。想想看,如果你工作在天津,但是偏偏要住在北京,每天上班下班是不是很痛苦?为什么呢?为什么会这样?为什么我想访问的这个内存不在我身边?那它在哪?

从C语言的malloc说起。首先强调一点,malloc分配的是"地址空间",而不是内存!同理,free释放的也只是地址空间,而不是内存。当你访问某个内存地址,而这个地址没有映射到任何物理页的时候,就会发生缺页中断,然后此时,操作系统才分配内存。简单点说,"内存的分配发生在第一次访问的时候!"。

总的来说,内存分配器此时有三种策略:

  1. 糊里糊涂。什么都不知道,随便分。

  2. 就近,找离当前CPU最近的node分配。

  3. round-robin。把你要的东西尽可能的平均分到每个node上。

我不清楚你用的C Runtime到底是哪一种实现,反正以上三种都有可能。喜欢C的人大多都是追求高效,那么自然喜欢第二种咯?于是就有人提出,服务器的启动过程应该做成并行化的,比如IO的buffer就让IO线程去初始化,各自做各自的。这样听起来很有道理,但是!亲爱的,如果这个线程被调度到另一个CPU上怎么办?所以我们不光得控制内存怎么分配的,还得控制线程调度策略,把这个线程绑在固定的CPU组上。更复杂的是,执行任务的是一个线程池怎么办?请问,我是在写Application,还是Operating System? 无论如何,"谁要用谁分配"依然是一个有效的优化策略。

那么说Java吧,它比较清晰。Java有4种垃圾回收策略 串行化、并行、CMS(并发)、G1。如果选择了并行化的垃圾回收策略(Server版的jvm默认如此),它的内存池分为三类:新生代、老生代、永生代。

并行化的垃圾回收器有一个开关,-XX:+UseNUMA,这个影响到新生代的Eden Space这个内存池的分配器的策略。先说Eden Space是干什么的:大部分new出来的对象都是首先在Eden Space上创建,这里面的对象都是临时创建而又立马被销毁的对象,否则会很快(小于1秒)就被挪到老生代里面去。如果打开了-XX:+UseNUMA,那么Eden Space会尽量就近找node分配,从而获得最经济、快捷的内存访问。可是另外,其它几个内存池呢?他们要么是采用的糊里糊涂的方式,要么就是采用的方式3,做round-robin。所以说,前面所说的为了内存访问局部化而把启动过程做成并行的对JAVA来说完全不适用。

那么,现在重新思考NUMA的问题:访问速度、带宽。

访问速度:的确,访问远处的内存会慢些,但是会慢多少呢?30%?50%?200%?你测过吗?我没有。据说,即便再慢,也比Nehalem之前的任何CPU快,因为改进了系统架构去掉北桥做了QPI等等。而且,根据我看到的各种数据来看,速度差距在50%以内。NUMA真是个很有意思的话题。我初次接触到这个概念是在《Solaris内核结构》那本书上,那本书是06年出版的。那个时代的硬件和现在差别很大的,那时候做NUMA可真是死了心的做,差2倍、差20倍它也觉得没问题,因为它和SMP相比增加了带宽嘛。于是OS就得被迫为这样巨大的差异做各种优化。可是现在呢?你看JVM,为什么仅仅对Eden Space做了就近分配?网上有很多关于NUMA的文章,以及如何在NUMA架构上写出更高效的程序。但是,很多观点是陈旧的。

带宽:你确实把CPU间的QPI跑满了吗?近年产的CPU,每条QPI的单向带宽是每秒12GB(理论最大值),如今看来,只有大型的数据库Server或者Cache Server才有可能把它跑满。否则,你不需要考虑带宽的问题。

所以现在有一种新的策略:SUMA,或者叫page interleaving,这个可以做到硬件层面去。把物理地址空间以均匀交替的方式分给每个node。例如0x0000-0x0FFF分给node 1,0x1000-0x1FFF分给node 2,以此类推。然后所有软件完全不在乎NUMA这件事情。做page interleaving的目的是尽可能平均的使用每个QPI通道。我觉得一般来说,这就足够了。Intel NUMA的x86 CPU在3年前才开始陆续面世,先让OS、compiler、jvm这些去适应它,等他们都折腾够了,最后才是我们这些普通的software developer。

此博客中的热门博文

在windows下使用llvm+clang

少写代码,多读别人写的代码

tensorflow distributed runtime初窥