• TCMalloc:线程缓冲的Malloc


    TCMalloc:线程缓冲的Malloc

  • Author:Echo Chen(陈斌)

  • Email:chenb19870707@gmail.com

  • Blog:Blog.csdn.net/chen19870707

  • Date:October 10th, 2014

    这段时间比較闲。研究下内存管理,从官方文档開始啃起《TCMalloc : Thread-Caching Malloc》。

    一、动机

    TCMalloc要比glibc 2.3的malloc(能够从一个叫作ptmalloc2的独立库获得)和其它我測试过的malloc都快。ptmalloc在一台2.8GHz的P4机器上运行一次小对象malloc及free大约须要300纳秒,而TCMalloc的版本号相同的操作大约仅仅须要50纳秒。

    malloc版本号的速度是至关重要的。由于假设malloc不够快。应用程序的作者就倾向于在malloc之上写一个自己的内存释放列表。这就可能导致额外的代码复杂度。以及很多其它的内存占用――除非作者本身很细致地划分释放列表的大小并常常从中清除空暇的对象。

    TCMalloc也降低了多线程程序中的锁竞争情况。对于小对象。已经基本上达到了零竞争。对于大对象。TCMalloc尝试使用恰当粒度和有效的自旋锁。

    ptmalloc相同是通过使用每线程各自的空间来降低锁的竞争,可是ptmalloc2使用每线程空间有一个非常大的问题。

    在ptmalloc2中,内存不可能会从一个空间移动到还有一个空间。这有可能导致大量内存被浪费。比如,在一个Google的应用中,第一阶段可能会为其URL标准化的数据结构分配大约300MB内存。

    当第一阶段结束后。第二阶段将从相同的地址空间開始。假设第二个阶段被安排到了与第一阶段不同的空间内,这个阶段不会复用不论什么第一阶段留下的的内存,并会给地址空间加入另外一个300MB。类似的内存爆炸问题也能够在其它的应用中看到。

    TCMalloc的还有一个优点表如今小对象的空间效率。比如。分配N个8字节对象可能要使用大约8N * 1.01字节的空间,即多用百分之中的一个的空间。

    而ptmalloc2中每一个对象都使用了一个四字节的头,我觉得并将终于的尺寸圆整为8字节的倍数,最后使用了16N字节。

    二、使用

    要使用TCMalloc,仅仅要将tcmalloc通过“-ltcmalloc”链接器标志接入你的应用就可以。

    你也能够通过使用LD_PRELOAD在不是你自己编译的应用中使用tcmalloc:

    $ LD_PRELOAD="/usr/lib/libtcmalloc.so" 

    LD_PRELOAD比較麻烦。我们也不十分推荐这样的使用方法。

    TCMalloc还包括了一个堆检查器以及一个堆測量器

    假设你更想链接不包括堆測量器和检查器的TCMalloc版本号(比方可能为了降低静态二进制文件的大小),你应该链接 libtcmalloc_minimal

    三、综述

     TCMalloc给每一个线程分配了一个线程局部缓存。小对象的分配是直接由线程局部缓存来完毕的。

    假设须要的话会将对象从中央数据结构移动到线程局部缓存中,同一时候定期的用垃圾收集器把内存从线程局部缓存迁移回中央数据结构中。

    TCMalloc将尺寸小于等于32K的对象(“小”对象)和大对象区分开来。

    大对象直接使用页级分配器(page-level alloctor)(一个页是一个4K的对齐内存区域)从中央堆直接分配。即,一个大对象总是页对齐的并占领了整数个数的页。

    连续的一些页面能够被切割为一系列相等大小的小对象。比如。一个连续的页面(4K)能够被划分为32个128字节的对象。

    四、小对象分配

    每一个小对象的大小都会被映射到与之接近的 60个可分配的尺寸类别中的一个。比如,全部大小在833到1024字节之间的小对象时,都会归整到1024字节。60个可分配的尺寸类别这样隔开:较小的尺寸相差8字节,较大的尺寸相差16字节,再大一点的尺寸差32字节,如此等等。

    最大的间隔是控制的,这样刚超过上一个级别被分配到下一个级别就不会有太多的内存被浪费。

    一个线程缓存包括了由各个尺寸内存的对象组成的单链表,如图所看到的:

    image

    当分配一个小对象时:(1)我们将其大小映射到相应的尺寸中。 (2)查找当前线程的线程缓存中相应的尺寸的内存链表。 (3)假设当前尺寸内存链表非空,那么从链表中移除的第一个对象并返回它。当我们依照这样的方式分配时,TCMalloc不须要不论什么锁。这就能够极大提高分配的速度。由于锁/解锁操作在一个2.8GHz Xeon上大约须要100纳秒的时间。

    假设当前尺寸内存链表为空:(1)从Central Heap中取得一系列这样的尺寸的对象(Central Heap是被全部线程共享的)。

    (2)将他们放入该线程线程的缓冲区。 (3)返回一个新获取的对象给应用程序。

    假设Central Heap也为空:(1) 我们从中央页分配器分配了一系列页面。

    (2) 将他们切割成该尺寸的一系列对象。(3)将新分配的对象放入Central Heap的链表上 (4) 像前面一样,将部分对象移入线程局部的链表中。

    五、线程缓冲的大小的确定

    恰当线程缓冲区大小至关重要,假设缓冲区太小,我们须要常常去Central Heap分配;假设线程缓冲区太大,又致使大量对象闲置而浪费内存。

    注意到恰当的线程缓冲区的大小对内存的释放一样重要。假设没有线程缓冲,每次内存释放都须要把内存移回到Central Heap。相同。一些线程有不正确称的内存分配和释放行为(比如:生产者和消费者线程)。所以确定恰当的缓冲区大小也非常棘手。

    确定缓冲区大小。我们採用“慢開始”算法来确定每个尺寸内存链表的最大长度。当某个链表使用更频繁,我们就扩大他的长度。

    假设我们某个链表上释放的操作比分配操作很多其它,它的最大长度将被增长到整个链表能够一次性有效的移动到Central Heap的长度。

    以下的伪代码说明了这样的慢開始算法。注意到num_objects_to_move每个尺寸是不同的。

    通过移动特定长度的对象链表,中央缓冲能够高效的将链表在线程中传递。

    假设线程缓冲区的须要小于num_objects_to_move,在中央缓冲区上的这样的操作具有线性的时间复杂度。

    使用num_objects_to_move作为从中央缓冲区传递的对象数量的缺点是。它将不须要的那部分对象浪费在线程缓冲区。

       1: Start each freelist max_length at 1.
       2:  
       3: Allocation
       4:   if freelist empty {
       5:     fetch min(max_length, num_objects_to_move) from central list;
       6:     if max_length < num_objects_to_move {  // slow-start
       7:       max_length++;
       8:     } else {
       9:       max_length += num_objects_to_move;
      10:     }
      11:   }
      12:  
      13: Deallocation
      14:   if length > max_length {
      15:     // Don't try to release num_objects_to_move if we don't have that many.
      16:     release min(max_length, num_objects_to_move) objects to central list
      17:     if max_length < num_objects_to_move {
      18:       // Slow-start up to num_objects_to_move.
      19:       max_length++;
      20:     } else if max_length > num_objects_to_move {
      21:       // If we consistently go over max_length, shrink max_length.
      22:       overages++;
      23:       if overages > kMaxOverages {
      24:         max_length -= num_objects_to_move;
      25:         overages = 0;
      26:       }
      27:     }
      28:   }

    六、大对象的分配

    一个大对象的尺寸(> 32K)会被中央页堆处理,被圆整到一个页面尺寸(4K)。中央页堆是由 空暇内存列表组成的数组。对于i < 256而言,数组的第k个元素是一个由每一个单元是由k个页面组成的空暇内存链表。第256个条目则是一个包括了长度>= 256个页面的空暇内存链表:

    pageheap

    k个页面的一次分配通过在第k个空暇内存链表中查找来完毕。

    假设该空暇内存链表为空,那么我们则在下一个空暇内存链表中查找,如此继续。终于。假设必要的话,我们将在最后空暇内存链表中查找。假设这个动作也失败了,我们将向系统获取内存(使用sbrkmmap或者通过在/dev/mem中进行映射)。

    假设k个页面的分配是由连续的> k个页面的空暇内存链表完毕的。剩下的连续页面将被又一次插回到与之页面大小接近的空暇内存链表中去。

    七、跨度

    TCMalloc管理的堆由一系列页面组成。一系列的连续的页面由一个“跨度”(Span)对象来表示。

    一个跨度能够是已被分配或者是空暇的。假设是空暇的,跨度则会是一个页面堆链表中的一个条目。假设已被分配,它会或者是一个已经被传递给应用程序的大对象。或者是一个已经被切割成一系列小对象的一个页面。

    假设是被切割成小对象的,对象的尺寸类别会被记录在跨度中。

    由页面号索引的中央数组能够用于找到某个页面所属的跨度对象。比如。以下的跨度a占领了2个页面,跨度b占领了1个页面。跨度c占领了5个页面最后跨度d占领了3个页面。如图:

    image

    在一个32位的地址空间中。中央数组由一个2层的基数树来表示。当中根包括了32个条目,每一个叶包括了 215个条目(一个32为地址空间包括了 220个 4K 页面(2^32 / 4k),一层则是用25整除220个页面)。

    这就导致了中央阵列的初始内存使用须要128KB空间(215*4字节),看上去还是能够接受的。

    在64位机器上,我们将使用一个3层的基数树。

    八、释放

    当一个对象被释放时。我们先计算他的页面号并在中央数组中查找相应的跨度对象。该跨度会告诉我们该对象是大是小。假设它是小对象的话尺寸类别是多少。假设是小对象的话,我们将其插入到当前线程的线程缓存中相应的空暇内存链表中。

    假设线程缓存如今超过了某个预定的大小(默觉得2MB),我们便执行垃圾收集器将未使用的对象从线程缓存中移入中央自由列表。

    假设该对象是大对象的话,跨度对象会告诉我们该对象包括的页面的范围。

    假设该范围是[p,q]。我们还会查找页面p-1和页面q+1相应的跨度对象。假设这两个相邻的跨度中有不论什么一个是空暇的,我们将他们和[p,q]的跨度接合起来。最后跨度会被插入到页面堆中合适的空暇链表中。

    九、小对象的重要空暇内存链表

    就像前面提过的一样,我们为每一个尺寸类别设置了一个中央空暇列表。

    每一个中央空暇列表由两层数据结构来组成:一系列跨度和每一个跨度对象的一个空暇内存的链表。

    一个对象是通过从某个跨度对象的空暇列表中取出第一个条目来分配的。(假设全部的跨度里仅仅有空链表,那么首先从中央页面堆中分配一个尺寸合适的跨度。)

    一个对象通过将其加入到它包括的跨度对象的空暇内存链表中来将还回中央空暇列表。假设链表长度如今等于跨度对象中全部小对象的数量。那么该跨度就是全然自由的了。就会被返回到页面堆中(跨度对象中全部小对象都回收完了,整个跨度对象就空暇了)。

    十、线程缓冲区的垃圾回收

    垃圾回收对象保证线程缓冲区的大小可控制并将未使用的对象交还给中央空暇列表。有的线程须要大量的缓冲来保证工作有非常好的性能,而有的线程仅仅须要非常少甚至不须要缓冲就能工作,当一个线程的缓冲区超过它的max_size,垃圾回收对象介入,之后这个线程就要和其他线程竞争获取更大的缓冲。

    垃圾回收只会在内存释放的时候才会同意。

    我们检查全部的空暇内存链表并把一些数量的对象从空暇列表移动到中央链表。

    从某个空暇链表中移除的对象的数量是通过使用一个每空暇链表的低水位线L来确定的。L记录了自上一次垃圾收集以来列表最短的长度。注意。在上一次的垃圾收集中我们可能仅仅是将列表缩短了L个对象而没有对中央列表进行不论什么额外訪问。

    我们利用这个过去的历史作为对未来訪问的预測器并将L/2个对象从线程缓存空暇列表列表中移到对应的中央空暇链表中。

    这个算法有个非常好的特性是。假设某个线程不再使用某个特定的尺寸时,该尺寸的全部对象都会非常快从线程缓存被移到中央空暇链表,然后能够被其它缓存利用。

    假设在线程中,某个大小的内存对象持续释放比分配操作多。这样的2/L行为会引起至少有L/2的对象长期处于空暇链表中。为了避免这样的内存浪费,我们降低每一个链表的最大长度num_objects_to_move个。

       1: Garbage Collection
       2:   if (L != 0 && max_length > num_objects_to_move) {
       3:     max_length = max(max_length - num_objects_to_move, num_objects_to_move)
       4:   }

    线程的缓冲区超过max_size的事实表明假设提高线程的缓冲区,线程将执行的更加有效率。简单的提高max_size的值将用掉过度的内存对于一个有非常多现场的应用程序。

    开发人员须要通过 –tcmalloc_max_total_thread_cache_bytes标志来限制内存的用量。

    max_size每一个线程缓冲区从一个最小的max_size(比如64k)開始,这样空暇的线程就不会在当它们不许的时候预分配内存。每次同意垃圾回收器,假设线程的缓冲区大小小于tcmalloc_max_total_thread_cache_bytes,也会尝试添加max_size,max_size增长非常easy。

    否则,线程1将通过减小线程2的max_size来尝试从线程2偷取内存。採用这样的方式,更加活跃的线程将从其他线程偷取内存。这样大多数空暇线程将保持非常小的缓冲区而活跃的线程将保持较大的缓冲区。注意这样的偷取可能引起线虫缓冲区的总和比tcmalloc_max_total_thread_cache_bytes大知道线程2释放内存到垃圾回收器。

    十一、性能

    PTMalloc2单元測试

    PTMalloc2包(如今已经是glibc的一部分了)包括了一个单元測试程序t-test1.c。它会产生一定数量的线程并在每一个线程中进行一系列分配和解除分配;线程之间没有不论什么通信除了在内存分配器中同步。

    t-test1(放在tests/tcmalloc/中,编译为ptmalloc_unittest1)用一系列不同的线程数量(1~20)和最大分配尺寸(64B~32KB)执行。这些測试执行在一个2.4GHz 双核心Xeon的RedHat 9系统上,并启用了超线程技术, 使用了Linux glibc-2.3.2,每一个測试中进行一百万次操作。在每一个案例中。一次正常执行,一次使用LD_PRELOAD=libtcmalloc.so

    以下的图像显示了TCMalloc对照PTMalloc2在不同的衡量指标下的性能。

    首先,现实每秒操作次数(百万)以及最大分配尺寸,针对不同数量的线程。用来生产这些图像的原始数据(time工具的输出)能够在t-test1.times.txt中找到。

    image

    image

    • TCMalloc要比PTMalloc2更具有一致地伸缩性——对于全部线程数量>1的測试,小分配达到了约7~9百万操作每秒。大分配降到了约2百万操作每秒。

      单线程的案例则明显是要被剔除的,由于他仅仅能保持单个处理器繁忙因此仅仅能获得较少的每秒操作数。PTMalloc2在每秒操作数上有更高的方差——某些地方峰值能够在小分配上达到4百万操作每秒。而在大分配上降到了<1百万操作每秒。

    • TCMalloc在绝大多数情况下要比PTMalloc2快,而且特别是小分配上。线程间的争用在TCMalloc中问题不大。
    • TCMalloc的性能随着分配尺寸的添加而减少。这是由于每线程缓存当它达到了阈值(默认是2MB)的时候会被垃圾收集。对于更大的分配尺寸,在垃圾收集之前仅仅能在缓存中存储更少的对象。

    • TCMalloc性能在约32K最大分配尺寸附件有一个明显的下降。

      这是由于在每线程缓存中的32K对象的最大尺寸;对于大于这个值得对象TCMalloc会从中央页面堆中进行分配。

    以下是每秒CPU时间的操作数(百万)以及线程数量的图像,最大分配尺寸64B~128KB。

    image

    image

    这次我们再一次看到TCMalloc要比PTMalloc2更连续也更高效。对于<32K的最大分配尺寸,TCMalloc在大线程数的情况下典型地达到了CPU时间每秒约0.5~1百万操作,同一时候PTMalloc通常达到了CPU时间每秒约0.5~1百万,还有非常多情况下要比这个数字小非常多。

    在32K最大分配尺寸之上,TCMalloc下降到了每CPU时间秒1~1.5百万操作。同一时候PTMalloc对于大线程数降到差点儿仅仅有零(也就是,使用PTMalloc,在高度多线程的情况下。非常多CPU时间被浪费在轮流等待锁定上了)。

    十二、改动执行行为

    能够通过环境变量来控制tcmalloc的行为,通常实用的标志。

    标志 默认值 作用
    TCMALLOC_SAMPLE_PARAMETER 0 採样时间间隔
    TCMALLOC_RELEASE_RATE 1.0 释放未使用内存的概率
    TCMALLOC_LARGE_ALLOC_REPORT_THRESHOLD 1073741824 内存最大分配阈值
    TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES 16777216 分配给线程缓冲的最大内存上限

    微调參数:

    TCMALLOC_SKIP_MMAP default: false If true, do not try to use mmap to obtain memory from the kernel.
    TCMALLOC_SKIP_SBRK default: false If true, do not try to use sbrk to obtain memory from the kernel.
    TCMALLOC_DEVMEM_START default: 0 Physical memory starting location in MB for /dev/mem allocation. Setting this to 0 disables/dev/mem allocation.
    TCMALLOC_DEVMEM_LIMIT default: 0 Physical memory limit location in MB for /dev/mem allocation. Setting this to 0 means no limit.
    TCMALLOC_DEVMEM_DEVICE default: /dev/mem Device to use for allocating unmanaged memory.
    TCMALLOC_MEMFS_MALLOC_PATH default: "" If set, specify a path where hugetlbfs or tmpfs is mounted. This may allow for speedier allocations.
    TCMALLOC_MEMFS_LIMIT_MB default: 0 Limit total memfs allocation size to specified number of MB. 0 means "no limit".
    TCMALLOC_MEMFS_ABORT_ON_FAIL default: false If true, abort() whenever memfs_malloc fails to satisfy an allocation.
    TCMALLOC_MEMFS_IGNORE_MMAP_FAIL default: false If true, ignore failures from mmap.
    TCMALLOC_MEMFS_MAP_PRVIATE default: false If true, use MAP_PRIVATE when mapping via memfs, not MAP_SHARED.

    十三、在代码中改动行为

    在malloc_extension.h中的MallocExtension类提供了一些微调的接口来改动tcmalloc的行为来使得你的程序达到更高的效率。

    归还内存给操作系统

    默认情况下,tcmalloc将逐渐的释放长时间未使用的内存给内核。

    tcmalloc_release_rate标志控制归还给操作系统内存的速度大,你也能够长治释放内存通过运行例如以下操作:

       1: MallocExtension::instance()->ReleaseFreeMemory();

    你相同能够调用SetMemoryReleaseRate()来在执行时改动tcmalloc_release_rate的值。或者调用GetMemoryReleaseRate来查看当前释放的概率值。

    内存诊断

    有几种操作能够获取可读的当前内存的使用情况:

       1: MallocExtension::instance()->GetStats(buffer, buffer_length);
       2: MallocExtension::instance()->GetHeapSample(&string);
       3: MallocExtension::instance()->GetHeapGrowthStacks(&string);

    后面两个方法创建如同heap-profiler一样的文件格式。能够直接传递给pprof。第一个方法主要用于调试。

    一般的Tcmalloc状态

    tcmalloc支持设置和获取状态属性

       1: MallocExtension::instance()->SetNumericProperty(property_name, value);
       2: MallocExtension::instance()->GetNumericProperty(property_name, &value);

    设置这些属性对于应用程序而言是可惜的。最经常使用的是当库设置了属性。这样应用程序就能够都这些属性。这里是Tcmalloc定义的属性,你能够获取它们通过调用接口。比方MallocExtension::instance()->GetNumericProperty("generic.heap_size", &value);:

    generic.current_allocated_bytes Number of bytes used by the application. This will not typically match the memory use reported by the OS, because it does not include TCMalloc overhead or memory fragmentation.
    generic.heap_size Bytes of system memory reserved by TCMalloc.
    tcmalloc.pageheap_free_bytes Number of bytes in free, mapped pages in page heap. These bytes can be used to fulfill allocation requests. They always count towards virtual memory usage, and unless the underlying memory is swapped out by the OS, they also count towards physical memory usage.
    tcmalloc.pageheap_unmapped_bytes Number of bytes in free, unmapped pages in page heap. These are bytes that have been released back to the OS, possibly by one of the MallocExtension "Release" calls. They can be used to fulfill allocation requests, but typically incur a page fault. They always count towards virtual memory usage, and depending on the OS, typically do not count towards physical memory usage.
    tcmalloc.slack_bytes Sum of pageheap_free_bytes and pageheap_unmapped_bytes. Provided for backwards compatibility only. Do not use.

    十四、附加说明

    对于某些系统,TCMalloc可能无法与没有链接libpthread.so(或者你的系统上同等的东西)的应用程序正常工作。它应该能正常工作于使用glibc 2.3的Linux上。可是其它OS/libc的组合方式尚未经过不论什么測试。

    TCMalloc可能要比其它malloc版本号在某种程度上更吃内存,(可是倾向于不会有其它malloc版本号中可能出现的爆发性增长。)尤其是在启动时TCMalloc会分配大约240KB的内部内存。

    不要试图将TCMalloc加载到一个执行中的二进制程序中(比如,在Java中使用JNI)。二进制程序已经使用系统malloc分配了一些对象。并会尝试将它们传递到TCMalloc进行解除分配

    TCMalloc是无法处理这样的对象的。

    -

    Echo Chen:Blog.csdn.net/chen19870707

    -

  • 相关阅读:
    996工作制是奋斗还是剥削?
    动态链接的PLT与GOT
    The Product-Minded Software Engineer
    缓冲区溢出
    golang的加法比C快?
    C errno是否是线程安全的
    golang 三个点的用法
    GDB 单步调试汇编
    为什么CPU需要时钟这种概念?
    fliebeat配置手册
  • 原文地址:https://www.cnblogs.com/cynchanpin/p/6789242.html
  • Copyright © 2020-2023  润新知