在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
介绍要搞明白 Go 语言的内存管理,就必须先理解操作系统以及机器硬件是如何管理内存的。因为 Go 语言的内部机制是建立在这个基础之上的,它的设计,本质上就是尽可能的会发挥操作系统层面的优势,而避开导致低效情况。 一,操作系统内存管理其实现在计算机内存管理的方式都是一步步演变来的,最开始是非常简单的,后来为了满足各种需求而增加了各种各样的机制,越来越复杂。这里我们只介绍和开发者息息相关的几个机制。 最原始的方式我们可以把内存看成一个数组,每个数组元素的大小是 CPU 在执行指令的时候,就是通过内存地址,将物理内存上的数据载入到寄存器,然后执行机器指令。但随着发展,出现了多任务的需求,也就是希望多个任务能同时在系统上运行。这就出现了一些问题:
举个例子,假设有一个程序,当代码运行到某处时,需要使用 虚拟内存虚拟内存的出现,很好的为了解决上述的一些列问题。用户程序只能使用虚拟的内存地址来获取数据,系统会将这个虚拟地址翻译成实际的物理地址。 所有程序统一使用一套连续虚拟地址,比如 对于内存不够用的问题,虚拟内存本质上是将磁盘当成最终存储,而主存作为了一个 cache。程序可以从虚拟内存上申请很大的空间使用,比如 如下图所示,每个进程所使用的虚拟地址空间都是一样的,但他们的虚拟地址会被映射到主存上的不同区域,甚至映射到磁盘上(当内存不够用时)。 其实本质上很简单,就是操作系统将程序常用的数据放到内存里加速访问,不常用的数据放在磁盘上。这一切对用户程序来说完全是透明的,用户程序可以假装所有数据都在内存里,然后通过虚拟内存地址去访问数据。在这背后,操作系统会自动将数据在主存和磁盘之间进行交换。 虚拟地址翻译虚拟内存的实现方式,大多数都是通过页表来实现的。操作系统虚拟内存空间分成一页一页的来管理,每页的大小为
页表被操作系统放在物理内存的指定位置,CPU 上有个 Memory Management Unit(MMU) 单元,CPU 把虚拟地址给 MMU,MMU 去物理内存中查询页表,得到实际的物理地址。当然 MMU 不会每次都去查的,它自己也有一份缓存叫Translation Lookaside Buffer (TLB),是为了加速地址翻译。
让我们来看一下 CPU 内存访问的完整过程:
内存命中率你可能已经发现,上述的访问步骤中,从第 4 步开始都是些很繁琐的操作,频繁的执行对性能影响很大。毕竟访问磁盘是非常慢的,它会引发程序性能的急剧下降。如果内存访问到第 3 步成功结束了,我们就说页命中了;反之就是未命中,或者说缺页,表示它开始执行第 4 步了。 假设在 n 次内存访问中,出现命中的次数是 m,那么 如果物理内存不足了,数据会在主存和磁盘之间频繁交换,命中率很低,性能出现急剧下降,我们称这种现象叫内存颠簸。这时你会发现系统的 swap 空间利用率开始增高, CPU 利用率中 大多数情况下,只要物理内存够用,页命中率不会非常低,不会出现内存颠簸的情况。因为大多数程序都有一个特点,就是局部性。 局部性就是说被引用过一次的存储器位置,很可能在后续再被引用多次;而且在该位置附近的其他位置,也很可能会在后续一段时间内被引用。 前面说过计算机到处使用一级级的缓存来提升性能,归根结底就是利用了局部性的特征,如果没有这个特性,一级级的缓存不会有那么大的作用。所以一个局部性很好的程序运行速度会更快。 CPU Cache随着技术发展,CPU 的运算速度越来越快,但内存访问的速度却一直没什么突破。最终导致了 CPU 访问主存就成了整个机器的性能瓶颈。CPU Cache 的出现就是为了解决这个问题,在 CPU 和 主存之间再加了 Cache,用来缓存一块内存中的数据,而且还不只一个,现代计算机一般都有 3 级 Cache,其中 L1 Cache 的访问速度和寄存器差不多。 现在访问数据的大致的顺序是 现在存储器的整体层次结构大致如下图: 在这种架构下,缓存的命中率就更加重要了,因为系统会假定所有程序都是有局部性特征的。如果某一级出现了未命中,他就会将该级存储的数据更新成最近使用的数据。 主存与存储器之间以 page(通常是 4K) 为单位进行交换,cache 与 主存之间是以 cache line(通常 64 byte) 为单位交换的。 举个例子让我们通过一个例子来验证下命中率的问题,下面的函数是循环一个数组为每个元素赋值。
参数 step 为 1 时,和普通一层循环一样。假设 step 为 2 ,则效果就是跳跃式遍历数组,如 下面是 nums 长度为
可以看出,2 种遍历方式会出现 3 倍的性能差距。这种问题最容易出现在多维数组的处理上,比如遍历一个二维数组很容易就写出局部性很差的代码。 二,程序的内存布局最后看一下程序的内存布局。现在我们知道了每个程序都有自己一套独立的地址空间可以使用,比如 那最终编译出来的二进制文件,是如何被操作系统加载到内存中并执行的呢? 其实,操作系统已经将一整块内存划分好了区域,每个区域用来做不同的事情。如图:
其实现在的操作系统,进程内部的内存区域没这么简单,要比这复杂多了,比如内核区域,共享库区域。因为我们不是要真的开发一套操作系统,细节可以忽略。这里只需要记住堆空间和栈空间即可。
结论局部性好的程序,可以提高缓存命中率,这对底层系统的内存管理是很友好的,可以提高程序的性能。CPU Cache 层面的低命中率导致的是程序运行缓慢,内存层面的低命中率会出现内存颠簸,出现这种现象时你的服务基本上已经瘫痪了。Go 语言的内存管理是参考 tcmalloc 实现的,它其实就是利用好了 OS 管理内存的这些特点,来最大化内存分配性能的。
|
请发表评论