精华内容
下载资源
问答
  • 对象——利弊与使用场景
    千次阅读
    2018-06-17 15:47:32

    对象池使用

    通常我们如此实现一个业务对象池,实现org.apache.commons.pool2.*的一些接口。

    /**
     * @author zhangshuo
     *
     */
    @Slf4j
    public class ResourceLoaderFactory extends BasePooledObjectFactory<ResourceLoader> {
    
    	/**
    	 * 单例工厂
    	 * 
    	 * @return
    	 */
    	public static ResourceLoaderFactory getInstance() {
    		return ResourceFactoryHolder.resourceLoaderFactory;
    	}
    
    	private static class ResourceLoaderFactoryHolder {
    		public final static ResourceLoaderFactory resourceLoaderFactory= new ResourceLoaderFactory();
    	}
    
    	private final GenericObjectPool<ResourceLoader> objectsPool;
    
    	public ResourceLoaderFactory() {
    		GenericObjectPoolConfig config = new GenericObjectPoolConfig();
    		config.setMaxTotal(1024);
    		config.setMaxIdle(50);
    		config.setMinIdle(8);
    		// 当Pool中没有对象时不等待,而是直接new个新的
    		config.setBlockWhenExhausted(false);
    		AbandonedConfig abandonConfig = new AbandonedConfig();
    		abandonConfig.setRemoveAbandonedTimeout(300);
    		abandonConfig.setRemoveAbandonedOnBorrow(true);
    		abandonConfig.setRemoveAbandonedOnMaintenance(true);
    		objectsPool = new GenericObjectPool<ResourceLoader>(this, config, abandonConfig);
    	}
    
    	public ResourceLoader takeResourceLoader() throws Exception {
    		try {
    			return objectsPool.borrowObject();
    		} catch (Exception e) {
    			log.error("ResourceLoader error:{}",e);
    		}
    		return create();
    	}
    
    	@Override
    	public ResourceLoader create() throws Exception {
    		return ResourceLoader.builder().build();
    	}
    
    	@Override
    	public PooledObject<ResourceLoader> wrap(ResourceLoader obj) {
    		return new DefaultPooledObject<ResourceLoader>(obj);
    	}
    
    	public void returnObject(ResourceLoader loader) {
    		loader.reset();
    		objectsPool.returnObject(loader);
    	}
    }
    

    对象池的优点

    复用池中对象,消除创建对象、回收对象 所产生的内存开销、cpu开销以及(若跨网络)产生的网络开销.

    常见的使用对象池有:在使用socket时(包括各种连接池)、线程等等

    对象池的缺点:

    • 现在Java的对象分配操作不比c语言的malloc调用慢, 对于轻中量级的对象, 分配/释放对象的开销可以忽略不计;
    • 并发环境中, 多个线程可能(同时)需要获取池中对象, 进而需要在堆数据结构上进行同步或者因为锁竞争而产生阻塞, 这种开销要比创建销毁对象的开销高数百倍;
    • 由于池中对象的数量有限, 势必成为一个可伸缩性瓶颈;
    • 很难正确的设定对象池的大小, 如果太小则起不到作用, 如果过大, 则占用内存资源高,

    对象池有其特定的适用场景:

    • 受限的, 不需要可伸缩性的环境(cpu\内存等物理资源有限): cpu性能不够强劲, 内存比较紧张, 垃圾收集, 内存抖动会造成比较大的影响, 需要提高内存管理效率, 响应性比吞吐量更为重要;
    • 数量受限的资源, 比如数据库连接;
    • 创建成本高昂的对象, 可斟酌是否池化, 比较常见的有线程池(ThreadPoolExecutor), 字节数组池等;

    http://www.infoq.com/cn/news/2015/07/ClojureWerkz

    更多相关内容
  • Boost内存池使用与测试

    千次阅读 2018-08-02 21:58:23
    例如,当程序使用内存池内存池恰好处于已经满了的状态,那么这次内存申请会导致内存池自我扩充,肯定比直接new一块内存要慢。但在大部分时候,内存池要比new或者malloc快很多。 内存池效率测试 测试1:...

    转自 http://tech.it168.com/a2011/0726/1223/000001223399_all.shtml

    • Boost库是一个可移植的开源C++函数库,鉴于STL(标准模板库)已经成为C++语言的一个组成部分,可以毫不夸张的说,Boost是目前影响最大的通用C++库。Boost库由C++标准委员会库工作组成员发起,其中有些内容有望成为下一代C++标准库内容,是一个“准”标准库。
    • Boost内存池,即boost.pool库,是由Boost提供的一个用于内存池管理的开源C++库。作为Boost中影响较大的一个库,Pool已经被广泛使用。

    Boost内存池使用与测试

    什么是内存池

       是在计算机技术中经常使用的一种设计模式,其内涵在于:将程序中需要经常使用的核心资源先申请出来,放到一个池内,由程序自己管理,这样可以提高资源的使用效率,也可以保证本程序占有的资源数量
    经常使用的池技术包括内存池、线程池和连接池等,其中尤以内存池和线程池使用最多。
    内存池(Memory Pool) 是一种动态内存分配与管理技术。
    通常情况下,程序员习惯直接使用 new、delete、malloc、free 等API申请分配和释放内存,这样导致的后果是:当程序长时间运行时,由于所申请内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能
    内存池则是在真正使用内存之前,先申请分配一大块内存(内存池)留作备用,当程序员申请内存时,从池中取出一块动态分配,当程序员释放内存时,将释放的内存再放入池内,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。

    内存池的应用场景

      早期的内存池技术是为了专门解决那种频繁申请和释放相同大小内存块的程序,因此早期的一些内存池都是用相同大小的内存块链表组织起来的。
      Boost的内存池则对内存块的大小是否相同没有限制,因此只要是频繁动态申请释放内存的长时间运行程序,都适用Boost内存池。这样可以有效减少内存碎片并提高程序运行效率。

    安装

      Boost的pool库是以C++头文件的形式提供的,不需要安装,也没有lib或者dll文件,仅仅需要将头文件包含到你的C++工程中就可以了。Boost的最新版本可以到 http://www.boost.org/ 下载。

    内存池的特征

    无内存泄露

    正确的使用内存池的申请和释放函数不会造成内存泄露,更重要的是,即使不正确的使用了申请和释放函数,内存池中的内存也会在进程结束时被全部自动释放,不会造成系统的内存泄露。

    申请的内存数组没有被填充

      例如一个元素的内存大小为A,那么元素数组若包含n个元素,则该数组的内存大小必然是A*n,不会有多余的内存来填充该数组。尽管每个元素也许包含一些填充的东西。

    任何数组内存块的位置都和使用operator new[]分配的内存块位置一致

      这表明你仍可以使用那些通过数组指针计算内存块位置的算法。

    内存池要比直接使用系统的动态内存分配快

      这个快是概率意义上的,不是每个时刻,每种内存池都比直接使用new或者malloc快。例如,当程序使用内存池时内存池恰好处于已经满了的状态,那么这次内存申请会导致内存池自我扩充,肯定比直接new一块内存要慢。但在大部分时候,内存池要比new或者malloc快很多。

    内存池效率测试

    测试1:连续申请和连续释放

      分别用内存池和new连续申请和连续释放大量的内存块,比较其运行速度,代码如下:

    #include "stdafx.h"
    #include <iostream>
    #include <ctime>
    #include <vector>
    #include <boost/pool/pool.hpp>
    #include <boost/pool/object_pool.hpp>
    using namespace std;
    using namespace boost;
    
    const int MAXLENGTH = 100000;
    
    int main ( )
    {
        boost::pool<> p(sizeof(int));
        int* vec1[MAXLENGTH];
        int* vec2[MAXLENGTH];
    
        clock_t clock_begin = clock();
        for (int i = 0; i < MAXLENGTH; ++i)
        {
            vec1[i] = static_cast<int*>(p.malloc());
        }
        for (int i = 0; i < MAXLENGTH; ++i)
        {
            p.free(vec1[i]);
            vec1[i] = NULL;
        }
    
        clock_t clock_end = clock();
        cout<<"程序运行了 "<<clock_end-clock_begin<<" 个系统时钟"<<endl;
    
        clock_begin = clock();
        for (int i = 0; i < MAXLENGTH; ++i)
        {
            vec2[i] = new int;
        }
        for (int i = 0; i < MAXLENGTH; ++i)
        {
            delete vec2[i];
            vec2[i] = NULL;
        }
    
        clock_end = clock();
        cout<<"程序运行了 "<<clock_end-clock_begin<<" 个系统时钟"<<endl;
    
        return 0;
    }

      测试环境:VS2008,WindowXP SP2,Pentium 4 CPU双核,1.5GB内存。
    image.png

      结论:在连续申请和连续释放10万块内存的情况下,使用内存池耗时是使用new耗时的47.46%。

    测试2:反复申请和释放小块内存

    代码如下:

    #include "stdafx.h"
    #include <iostream>
    #include <ctime>
    #include <vector>
    #include <boost/pool/pool.hpp>
    #include <boost/pool/object_pool.hpp>
    using namespace std;
    using namespace boost;
    
    const int MAXLENGTH = 500000;
    
    int main ( )
    {
        boost::pool<> p(sizeof(int));
    
        clock_t clock_begin = clock();
        for (int i = 0; i < MAXLENGTH; ++i)
        {
            int * t = static_cast<int*>(p.malloc());
            p.free(t);
        }
        clock_t clock_end = clock();
        cout<<"程序运行了 "<<clock_end-clock_begin<<" 个系统时钟"<<endl;
    
        clock_begin = clock();
        for (int i = 0; i < MAXLENGTH; ++i)
        {
            int* t = new int;
            delete t;
        }
        clock_end = clock();
        cout<<"程序运行了 "<<clock_end-clock_begin<<" 个系统时钟"<<endl;
    
        return 0;
    }
    

      测试结果如下:
    image.png

      结论:在反复申请和释放50万次内存的情况下,使用内存池耗时是使用new耗时的64.34%。

    测试3:反复申请和释放C++对象

      C++对象在动态申请和释放时,不仅要进行内存操作,同时还要调用构造和析购函数。因此有必要对C++对象也进行内存池的测试。
      代码如下:

    #include "stdafx.h"
    #include <iostream>
    #include <ctime>
    #include <vector>
    #include <boost/pool/pool.hpp>
    #include <boost/pool/object_pool.hpp>
    using namespace std;
    using namespace boost;
    
    const int MAXLENGTH = 500000;
    class A
    {
    public: 
        A()
        {
            m_i++; 
        }
        ~A( )
        {
            m_i--; 
        }
    private:
        int m_i;
    };
    
    int main ( )
    {
        object_pool<A> q;
    
        clock_t clock_begin = clock();
        for (int i = 0; i < MAXLENGTH; ++i)
        {
            A* a = q.construct();
            q.destroy(a);
        }
    
        clock_t clock_end = clock();
        cout<<"程序运行了 "<<clock_end-clock_begin<<" 个系统时钟"<<endl;
    
        clock_begin = clock();
        for (int i = 0; i < MAXLENGTH; ++i)
        {
            A* a = new A; 
            delete a;
        }
        clock_end = clock();
        cout<<"程序运行了 "<<clock_end-clock_begin<<" 个系统时钟"<<endl;
    
        return 0;
    }

      测试结果如下:
    image.png

      结论:在反复申请和释放50万个C++对象的情况下,使用内存池耗时是使用new耗时的112.03%。这是因为内存池的construct和destroy函数增加了函数调用次数的原因。这种情况下使用内存池并不能获得性能上的优化。

    Boost内存池的分类

      Boost内存池按照不同的理念分为四类。主要是两种理念的不同造成了这样的分类。
      一是Object Usage和Singleton Usage的不同。
    - Object Usage意味着每个内存池都是一个可以创建和销毁的对象,一旦内存池被销毁则其所分配的所有内存都会被释放。
    - Singleton Usage意味着每个内存池都是一个被静态分配的对象,直至程序结束才会被销毁,这也意味着这样的内存池是多线程安全的。只有使用release_memory或者 purge_memory方法才能释放内存。
      二是内存溢出的处理方式。第一种方式是返回NULL代表内存池溢出了;第二种方式是抛出异常代表内存池溢出。
      根据以上的理念,boost的内存池分为四种。

    Pool

      Pool是一个Object Usage的内存池,溢出时返回NULL。

    object_pool

      object_pool与pool类似,唯一的区别是当其分配的内存释放时,它会尝试调用该对象的析购函数。

    singleton_pool

      singleton_pool是一个Singleton Usage的内存池,溢出时返回NULL。

    pool_alloc

      pool_alloc是一个Singleton Usage的内存池,溢出时抛出异常。

    内存池溢出的原理与解决方法

    必然溢出的内存

      内存池简化了很多内存方面的操作,也避免了一些错误使用内存对程序造成的损害。但是,使用内存池时最需要注意的一点是要处理内存池溢出的情况。
      没有不溢出的内存,看看下面的代码:

    #include "stdafx.h"
    #include <iostream>
    #include <ctime>
    #include <vector>
    #include <boost/pool/pool.hpp>
    #include <boost/pool/object_pool.hpp>
    using namespace std;
    using namespace boost;
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        clock_t clock_begin = clock();
        int iLength = 0;
        for (int i = 0; ;++i)
        {
            void* p = malloc(1024*1024);
            if (p == NULL)
            {
                break;
            }
            ++iLength;
        }
        clock_t clock_end = clock();
        cout<<"共申请了"<<iLength<<"M内存,程序运行了"<<clock_end-clock_begin<<" 个系统时钟"<<endl;
        return 0;
    }

      运行的结果是“共申请了1916M内存,程序运行了 69421 个系统时钟”,意思是在分配了1916M内存后,malloc已经不能够申请到1M大小的内存块了。
      内存池在底层也是调用了malloc函数,因此内存池也是必然会溢出的。而且内存池可能会比直接调用malloc更早的溢出,看看下面的代码:

    #include "stdafx.h"
    #include <iostream>
    #include <ctime>
    #include <vector>
    #include <boost/pool/pool.hpp>
    #include <boost/pool/object_pool.hpp>
    using namespace std;
    using namespace boost;
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        boost::pool<> pl(1024*1024);
        clock_t clock_begin = clock();
        int iLength = 0;
        for (int i = 0; ;++i)
        {
            void* p = pl.malloc();
            if (p == NULL)
            {
                break;
            }
            ++iLength;
        }
        clock_t clock_end = clock();
        cout<<"共申请了"<<iLength<<"M内存,程序运行了"<<clock_end-clock_begin<<" 个系统时钟"<<endl;
        return 0;
    }

      运行的结果是“共申请了992M内存,程序运行了 1265 个系统时钟”,意思是在分配了992M内存后,内存池已经不能够申请到1M大小的内存块了。

    内存池的基本原理

      从上面的两个测试可以看出内存池要比malloc溢出早,我的机器内存是1.5G,malloc分配了1916M才溢出(显然分配了虚拟内存),而内存池只分配了992M就溢出了。第二点是内存池溢出快,只用了1265微秒就溢出了,而malloc用了69421微秒才溢出。
      这些差别是内存池的处理机制造成的,内存池对于内存分配的算法如下,以pool内存池为例:
    - 1. pool初始化时带有一个块大小的参数memSize,那么pool刚开始会申请一大块内存,例如其大小为32*memSize。当然它还会申请一些空间用以管理链表,为方便述说,这里忽略这些内存。
    - 2. 用户不停的申请大小为memSize的内存,终于超过了内存池的大小,于是内存池启动重分配机制;
    - 3. 重分配机制会再申请一块大小为原内存池大小两倍的内存(那么第一次会申请64*memSize),然后将这块内存加到内存池的管理链表末尾;
    - 4. 用户继续申请内存,终于又一次超过了内存池的大小,于是又一次启动重分配机制,直至重分配时无法申请到新的内存块。
    - 5. 由于每次都是两倍于原内存,因此当内存池大小达到了992M时,再一次申请就需要1984M,但是malloc最多只能申请到1916M,因此malloc失败,内存池溢出。
      通过以上原理也可以理解为什么内存池溢出比malloc溢出要快得多,因为它是以2的指数级来扩大内存池,真正调用malloc的次数约等于log2(1916),而malloc是实实在在进行了1916次调用。所以内存池只用了1秒多就溢出了,而malloc用了69秒。

    内存池溢出的解决方法

      对于malloc造成的内存溢出,一般来说没有太多办法可想。基本上就是报一个异常或者错误,然后让用户关闭程序。当然有的程序会有内存自我管理功能,可以让用户选择关闭一切次要功能来维持主要功能的继续运行。
      而对于内存池的溢出,还是可以想一些办法的,因为毕竟系统内存还有潜力可挖。
      第一个方法是尽量延缓内存池的溢出,做法是在程序启动时就尽量申请最大的内存池,如果在程序运行很久后再申请,可能OS因为内存碎片增多而不能提供最大的内存池。其方法是在程序启动时就不停的申请内存直到内存池溢出,然后清空内存池并开始正常工作。由于内存池并不会自动减小,所以这样可以一直维持内存池保持最大状态。
      第二个方法是在内存池溢出时使用第二个内存池,由于第二个内存池可以继续申请较小块的内存,所以程序可继续运行。代码如下:

    #include "stdafx.h"
    #include <iostream>
    #include <ctime>
    #include <vector>
    #include <boost/pool/pool.hpp>
    #include <boost/pool/object_pool.hpp>
    using namespace std;
    using namespace boost;
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        boost::pool<> pl(1024*1024);
        clock_t clock_begin = clock();
        int iLength = 0;
        for (int i = 0; ;++i)
        {
            void* p = pl.malloc();
            if (p == NULL)
            {
                break;
            }
            ++iLength;
        }
        clock_t clock_end = clock();
        cout<<"共申请了"<<iLength<<"M内存,程序运行了"<<clock_end-clock_begin<<" 个系统时钟"<<endl;
    
        clock_begin = clock();
        iLength = 0;
        boost::pool<> pl2(1024*1024);
        for (int i = 0; ;++i)
        {
            void* p = pl2.malloc();
            if (p == NULL)
            {
                break;
            }
            ++iLength;
        }
        clock_end = clock();
        cout<<"又申请了"<<iLength<<"M内存,程序运行了"<<clock_end-clock_begin<<" 个系统时钟"<<endl;
    
        return 0;
    }

      运行结果如下:
    image.png

      结果表明在第一个内存池溢出后,第二个内存池又提供了480M的内存。

    内存池溢出的终极方案

      如果无论如何都不能再申请到新的内存了,那么还是老老实实告诉用户重启程序吧。

    展开全文
  • 内存池(memory pool)

    千次阅读 2021-11-15 15:49:02
    通常的进程发起申请内存的动作之后,会在系统的空闲内存区寻找合适大小的内存块(底层分配函数__alloc_node_mask),如果满足就直接分配,如果不满足就会向上查找。如果过大就会进行分裂,一部分分给申请进程,一...

    前言

    通常的进程发起申请内存的动作之后,会在系统的空闲内存区寻找合适大小的内存块(底层分配函数__alloc_node_mask),如果满足就直接分配,如果不满足就会向上查找。如果过大就会进行分裂,一部分分给申请进程,一部分放入空闲区。释放时需要找到这个块对应的伙伴,如果伙伴也为空闲,就进行合并,放入高阶空闲链表,如果不空闲就放入对应链表。同时对于多线程申请和释放内存,需要加锁。这样的默认的分配方式考虑到了系统中的大部分情况,具有通用性,但是无可避免的会产生内部碎片,而且加锁,解锁的开销也很大。

    如果可以对特定进程设计适合他自己的内存管理方式,那么它的性能应该会有提升。

    内存池

    程序可以通过系统的内存分配方法预先分配一大块内存来做一个内存池,之后程序的内存分配和释放都由这个内存池来进行操作和管理,当内存池不足时再向系统申请内存。

    我们通常使用malloc等函数来为用户进程分配内存。它的执行过程通常是由用户程序发起malloc申请内存的动作,在标准库找到对应函数,对不满128k的调用brk()系统调用来申请内存(申请的内存是堆区内存),接着由操作系统来执行brk系统调用。

    我们知道malloc是在标准库,真正的申请动作需要操作系统完成。所以由应用程序到操作系统就需要3层。内存池是专为应用程序提供的专属的内存管理器,它属于应用程序层。所以程序申请内存的时候就不需要通过标准库和操作系统,明显降低了开销。

    1

    内存池的分类

    对于线程安全来说,内存池可以分为单线程内存池和多线程内存池。单线程内存池整个生命周期只被一个线程使用,因而不需要考虑互斥访问的问题;多线程内存池有可能被多个线程共享,因此则需要在每次分配和释放内存时加锁。相对而言,单线程内存池性能更高,而多线程内存池适用范围更广。

    从可分配内存大小来说,可以分为固定内存池和可变内存池。所谓固定内存池是指应用程序每次从内存池中分配出来的内存单元大小事先已经确定,是固定不变的;而可变内存池则每次分配的内存单元大小可以按需变化,应用范围更广,而性能比固定内存池要低。

    内存池的工作原理

    固定内存池的设计如图:

    2

    固定内存池的内存块实际上是由链表连接起来的,前一个总是后一个的2倍大小。当内存池的大小不够分配时,向系统申请内存,大小为上一个的两倍,并且使用一个指针来记录当前空闲内存单元的位置。当我们要需要一个内存单元的时候,就会随着链表去查看每一个内存块的头信息,如果内存块里有空闲的内存单元,将该地址返回,并且将头信息里的空闲单元改成下一个空闲单元。
    当应用程序释放某内存单元,就会到对应的内存块的头信息里修改该内存单元为空闲单元。

    线程安全

    对于单线程来说,不需要考虑互斥访问的问题,但是对于多线程来说内存池可能会被多个线程共享,所以需要给内存池加上一个锁,来进行保护。

    如果出现程序中有大量线程申请释放内存,那么这种方案下锁的竞争将会非常激烈,线程这样的场景下使用该方案不会有很好的性能。

    所以就有了一种方法的诞生----------->TLS线程本地存储(线程局部存储)。

    TLS的作用是能将数据和执行的特定的线程联系起来。它可以让程序中由所有线程使用的全局变量,都产生自己的副本。也就是这个变量每个线程都有自己的私有,它们之间的操作互不干扰,不会影响。

    在这样的背景下,我们可以设计出线程本地存储+内存池的方式,这样线程都有了自己的内存池,且彼此之间不会产生影响,也就不需要加锁。

    内存池的设计

    提前创建

    假设服务器程序比较简单,处理请求的时候只使用一种对象,提前创建出一些需要的对象,比如数据结构,我们可以用的时候拿出来,不用的时候还回去就可以,只需要标记使用的和未使用的。这种比较简单。

    可申请不同大小的内存

    这种方法使得用户程序在请求过程中只申请内存,之后当处理完请求之后才一次性释放所有内存,这样可以降低内存申请和释放的开销,而且能减少内部碎片。

    除了这两种还有其他的设计方法,可以自己设计。

    因为内存池并不是一种通用的内存管理模式,它是一种比较特定的,固定某种场景去使用,属于私人定制。

    linux内存池

    在内核中有不少地方内存分配不允许失败。内存池作为一个在这些情况下确保分配的方式,内核开发者创建了一个已知为内存池(或者是 “mempool” )的抽象,内核中内存池真实地只是相当于后备缓存,它尽力一直保持一个空闲内存列表给紧急时使用,而在通常情况下有内存需求时还是从公共的内存中直接分配,这样的做法虽然有点霸占内存的嫌疑,但是可以从根本上保证关键应用在内存紧张时申请内存仍然能够成功。

    数据结构

    typedef struct mempool_s {
        spinlock_t lock; /*保护内存池的自旋锁*/
        int min_nr; /*内存池中最少可分配的元素数目*/
        int curr_nr; /*尚余可分配的元素数目*/
        void **elements; /*指向元素池的指针*/
        void *pool_data; /*内存源,即池中元素真实的分配处*/
        mempool_alloc_t *alloc; /*分配元素的方法*/
        mempool_free_t *free; /*回收元素的方法*/
        wait_queue_head_t wait; /*被阻塞的等待队列*/
    } mempool_t;
    

    内存池的创建函数mempool_create

    mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn,
                    mempool_free_t *free_fn, void *pool_data)
    {
        return mempool_create_node(min_nr,alloc_fn,free_fn, pool_data,-1);
    }
    

    这个函数指定了,内存池大小,分配方法,释放发法,分配源。创建完成之后,会从分配源(pool_data)中分配内存池大小(min_nr)个元素来填充内存池。

    内存池的释放函数mempool_destory

    void mempool_destroy(mempool_t *pool)
    {
        while (pool->curr_nr) {
            void *element = remove_element(pool);
            pool->free(element, pool->pool_data);
        }
        kfree(pool->elements);
        kfree(pool);
    }
    

    他是依次将元素对象从池中移除,再释放给pool_data,最后释放池对象。

    内存池分配对象的函数:mempool_allocmempool_alloc的作用是从指定的内存池中申请/获取一个对象。

    void *mempool_alloc(mempool_t *pool, gfp_t gfp_mask)
    {
    	void *element;
    	unsigned long flags;
    	wait_queue_entry_t wait;
    	gfp_t gfp_temp;
    
    	VM_WARN_ON_ONCE(gfp_mask & __GFP_ZERO);
    	might_sleep_if(gfp_mask & __GFP_DIRECT_RECLAIM);
    
    	gfp_mask |= __GFP_NOMEMALLOC;	/* don't allocate emergency reserves */
    	gfp_mask |= __GFP_NORETRY;	/* don't loop in __alloc_pages */
    	gfp_mask |= __GFP_NOWARN;	/* failures are OK */
    
    	gfp_temp = gfp_mask & ~(__GFP_DIRECT_RECLAIM|__GFP_IO);
    
    repeat_alloc:
    
    	element = pool->alloc(gfp_temp, pool->pool_data);//先从后备源中申请内存
    	if (likely(element != NULL))
    		return element;
    
    	spin_lock_irqsave(&pool->lock, flags);
    	if (likely(pool->curr_nr)) {
    		element = remove_element(pool);/*从内存池中提取一个对象*/
    		spin_unlock_irqrestore(&pool->lock, flags);
    		/* paired with rmb in mempool_free(), read comment there */
    		smp_wmb();
    		/*
    		 * Update the allocation stack trace as this is more useful
    		 * for debugging.
    		 */
    		kmemleak_update_trace(element);
    		return element;
    	}
    
    	/*
    	 * We use gfp mask w/o direct reclaim or IO for the first round.  If
    	 * alloc failed with that and @pool was empty, retry immediately.
    	 */
    	if (gfp_temp != gfp_mask) {
    		spin_unlock_irqrestore(&pool->lock, flags);
    		gfp_temp = gfp_mask;
    		goto repeat_alloc;
    	}
    
    	/* We must not sleep if !__GFP_DIRECT_RECLAIM */
    	if (!(gfp_mask & __GFP_DIRECT_RECLAIM)) {
    		spin_unlock_irqrestore(&pool->lock, flags);
    		return NULL;
    	}
    
    	/* Let's wait for someone else to return an element to @pool */
    	init_wait(&wait);
    	prepare_to_wait(&pool->wait, &wait, TASK_UNINTERRUPTIBLE);//加入等待队列
    
    	spin_unlock_irqrestore(&pool->lock, flags);
    
    	/*
    	 * FIXME: this should be io_schedule().  The timeout is there as a
    	 * workaround for some DM problems in 2.6.18.
    	 */
    	io_schedule_timeout(5*HZ);
    
    	finish_wait(&pool->wait, &wait);
    	goto repeat_alloc;
    }
    EXPORT_SYMBOL(mempool_alloc);
    

    函数先从后备源中申请内存,当从后备源无法成功申请到时,才会从内存池中申请内存使用,因此可以发现内核内存池(mempool)其实是一种后备池,在内存紧张的情况下才会真正从池中获取,这样也就能保证在极端情况下申请对象的成功率,但也不一定总是会成功,因为内存池的大小毕竟是有限的,如果内存池中的对象也用完了,那么进程就只能进入睡眠,也就是被加入到pool->wait的等待队列,等待内存池中有可用的对象时被唤醒,重新尝试从池中申请元素。

    内存池回收对象的函数:mempool_free

    void mempool_free(void *element, mempool_t *pool)
    {
    	unsigned long flags;
    
    	if (unlikely(element == NULL))
    		return;
    
    	/*
    	 * Paired with the wmb in mempool_alloc().  The preceding read is
    	 * for @element and the following @pool->curr_nr.  This ensures
    	 * that the visible value of @pool->curr_nr is from after the
    	 * allocation of @element.  This is necessary for fringe cases
    	 * where @element was passed to this task without going through
    	 * barriers.
    	 *
    	 * For example, assume @p is %NULL at the beginning and one task
    	 * performs "p = mempool_alloc(...);" while another task is doing
    	 * "while (!p) cpu_relax(); mempool_free(p, ...);".  This function
    	 * may end up using curr_nr value which is from before allocation
    	 * of @p without the following rmb.
    	 */
    	smp_rmb();
    
    	/*
    	 * For correctness, we need a test which is guaranteed to trigger
    	 * if curr_nr + #allocated == min_nr.  Testing curr_nr < min_nr
    	 * without locking achieves that and refilling as soon as possible
    	 * is desirable.
    	 *
    	 * Because curr_nr visible here is always a value after the
    	 * allocation of @element, any task which decremented curr_nr below
    	 * min_nr is guaranteed to see curr_nr < min_nr unless curr_nr gets
    	 * incremented to min_nr afterwards.  If curr_nr gets incremented
    	 * to min_nr after the allocation of @element, the elements
    	 * allocated after that are subject to the same guarantee.
    	 *
    	 * Waiters happen iff curr_nr is 0 and the above guarantee also
    	 * ensures that there will be frees which return elements to the
    	 * pool waking up the waiters.
    	 */
    	if (unlikely(pool->curr_nr < pool->min_nr)) {
    		spin_lock_irqsave(&pool->lock, flags);
    		if (likely(pool->curr_nr < pool->min_nr)) {//当前可分配的是否小于内存大小,
    			add_element(pool, element);
    			spin_unlock_irqrestore(&pool->lock, flags);
    			wake_up(&pool->wait);
    			return;
    		}
    		spin_unlock_irqrestore(&pool->lock, flags);
    	}
    	pool->free(element, pool->pool_data);
    }
    EXPORT_SYMBOL(mempool_free);
    

    其实原则跟mempool_alloc是对应的,释放对象时先看池中的可分配元素如果小于池中最少的可分配元素,那么久需要把元素放到内存池中。相反就要把它放到后备源中。

    总结

    用户程序的内存池通常是特殊的,适用于特定场景的专属内存管理法。内核态的内存池则是保证系统中的一些关键应用在内存紧缺的时候确保能够申请内存成功。他们的用途不同,但是做法确是如出一辙。

    展开全文
  • C++高并发内存池的设计和实现

    千次阅读 多人点赞 2021-07-05 16:08:48
    目录 一、整体设计 1、需求分析 1)直接使用new/delete、malloc/free存在的问题 2)普通内存池的优点和缺点 3)高并发内存池要解决的问题 2、总体设计思路 3、申请内存流程图 ​ 二、详细设计 1、各个模块内部结构...

    目录

    一、整体设计

    1、需求分析

    1)直接使用new/delete、malloc/free存在的问题

    2)普通内存池的优点和缺点

    3)高并发内存池要解决的问题

    2、总体设计思路

    3、申请内存流程图

    ​ 二、详细设计

    1、各个模块内部结构详细剖析

    1)thread cache

    2)central control cache

    3)page cache

    2、设计细节

    1)thread cache

    2)Central Control Cache 

     3)Page Cache

    4)加锁问题

    三、测试

    1、单元测试

    2、性能测试


    一、整体设计

    1、需求分析

    池化技术是计算机中的一种设计模式,内存池是常见的池化技术之一,它能够有效的提高内存的申请和释放效率以及内存碎片等问题,但是传统的内存池也存在一定的缺陷,高并发内存池相对于普通的内存池它有自己的独特之处,解决了传统内存池存在的一些问题。

    附:内存池基础知识及简易内存池的实现

    1)直接使用new/delete、malloc/free存在的问题

    new/delete用于c++中动态内存管理而malloc/free在c++和c中都可以使用,本质上new/delete底层封装了malloc/free。无论是上面的那种内存管理方式,都存在以下两个问题:

    • 效率问题:频繁的在堆上申请和释放内存必然需要大量时间,降低了程序的运行效率。对于一个需要频繁申请和释放内存的程序来说,频繁调用new/malloc申请内存,delete/free释放内存都需要花费系统时间,频繁的调用必然会降低程序的运行效率。
    • 内存碎片:经常申请小块内存,会将物理内存“切”得很碎,导致内存碎片。申请内存的顺序并不是释放内存的顺序,因此频繁申请小块内存必然会导致内存碎片,造成“有内存但是申请不到大块内存”的现象。

    2)普通内存池的优点和缺点

    针对直接使用new/delete、malloc/free存在的问题,普通内存池的设计思路是:预先开辟一块大内存,程序需要内存时直接从该大块内存中“拿”一块,提高申请和释放内存的效率,同时直接分配大块内存还减少了内存碎片问题。

    优点:申请和释放内存的效率有所提高;一定程度上解决了内存碎片问题。

    缺点:多线程并发场景下申请和释放内存存在锁竞争问题造成申请和释放内存的效率降低。

    3)高并发内存池要解决的问题

    基于以上原因,设计高并发内存池需要解决以下三个问题:

    • 效率问题
    • 内存碎片问题
    • 多线程并发场景下的内存释放和申请的锁竞争问题。

    2、总体设计思路

    高并发内存池整体框架由以下三部分组成,各部分的功能如下:

    • 线程缓存(thread cache):每个线程独有线程缓存,主要解决多线程下高并发运行场景线程之间的锁竞争问题。线程缓存模块可以为线程提供小于64k内存的分配,并且多个线程并发运行不需要加锁。
    • 中心控制缓存(central control cache):中心控制缓存顾名思义,是高并发内存池的中心结构主要用来控制内存的调度问题。负责大块内存切割分配给线程缓存以及回收线程缓存中多余的内存进行合并归还给页缓存,达到内存分配在多个线程中更均衡的按需调度的目的,它在整个项目中起着承上启下的作用。(注意:这里需要加锁,当多个线程同时向中心控制缓存申请或归还内存时就存在线程安全问题,但是这种情况是极少发生的,并不会对程序的效率产生较大的影响,总体来说利大于弊)
    • 页缓存(page cache):以页为单位申请内存,为中心控制缓存提供大块内存。当中心控制缓存中没有内存对象时,可以从page cache中以页为单位按需获取大块内存,同时page cache还会回收central control cache的内存进行合并缓解内存碎片问题。

    3、申请内存流程图

     二、详细设计

    1、各个模块内部结构详细剖析

    1)thread cache

    逻辑结构设计

    thread cache的主要功能就是为每一个线程提供64K以下大小内存的申请。为了方便管理,需要提供一种特定的管理模式,来保存未分配的内存以及被释放回来的内存,以方便内存的二次利用。这里的管理通常采用将不同大小的内存映射在哈希表中,链接起来。而内存分配的最小单位是字节,64k = 1024*64Byte如果按照一个字节一个字节的管理方式进行管理,至少也得需要1024*64大小的哈希表对不同大小的内存进行映射。为了减少哈希表长度,这里采用按一定数字对齐的方式进行内存分配,将浪费率保持在1%~12%之间。具体结构如下:

    具体说明如下:

    • 使用数组进行哈希映射,每一个位置存放的是一个链表freelists,该链表的作用是将相同大小的内存对象链接起来方便管理。
    • 每个数组元素链接的都是不同大小的内存对象。
    • 第一个元素表示对齐数是8,第2个是16....依次类推。对齐数表示在上一个对齐数和这个对齐数之间的大小的内存都映射在这个位置,即要申请1字节或者7字节的内存都在索引位置为0出找8字节大小的内存,要申请9~16字节大小的内存都在索引为1的位置找16字节的内存对象。
    • 通过上面的分析,可以看出如果进行8字节对齐,最多会浪费7字节的内存(实际申请1字节内存,返回的是8字节大小的内存对象),将这种现象称为内存碎片浪费。
    • 为了将内存碎片浪费保持在12%以下,也就是说最多容忍有12%的内存浪费,这里使用不同的对齐数进行对齐。
    • 0~128采用8字节对齐,129~1024采用16字节对齐,1025~8*1024采用128字节对齐,8*1024~64*1024采用1024字节对齐;内存碎片浪费率分别为:1/8,129/136,1025/1032,8102/8199均在12%左右。同时,8字节对齐时需要[0,15]共16个哈希映射;16字节对齐需要[16,71]共56个哈希映射;128字节对齐需要[72,127]共56个哈希映射;1024字节对齐需要[128,184]共56个哈希映射。
    • 哈希映射的结构如下:

    如何保证每个线程独有?

    TLS(Thread Local Stirage)

    大于64k的内存如何申请?

    当thread cache中申请的内存大于64K时,直接向page cache申请。但是page cache中最大也只能申请128页的内存,所以当thread cache申请的内存大于128页时page cache中会自动给thread cache在系统内存中申请。

    2)central control cache

    central control cache作为thread cache和page cache的沟通桥梁,起到承上启下的作用。它需要向thread cache提供切割好的小块内存,同时他还需要回收thread cache中的多余内存进行合并,在分配给其他其他thread cache使用,起到资源调度的作用。它的结构如下:

    具体说明如下:

    • central control cache的结构依然是一个数组,他保存的是span类型的对象。
    • span是用来管理一块内存的,它里边包含了一个freelist链表,用于将大块内存切割成指定大小的小块内存链接到freelist中,当thread cache需要内存时直接将切割好的内存给thread cache。
    • 开始时,每个数组索引位置都是空的,当thread cache申请内存时,spanList数组会向page cache申请一大块内存进行切割后挂在list中。当该快内存使用完,会继续申请新的内存,因此就存在多个span链接的情况。前边span存在对象是因为有可能后边已经申请好内存了前边的内存也释放回来了。
    • 当某一个span的全部内存都还回来时,central control cache会再次将这块内存合并,在归还到page cache中。
    • 当central control cache为空时,向page cache申请内存,每次至少申请一页,并且必须以页为单位进行申请(这里的页大小由我们自己决定,这里采用4K)。

    这里需要注意的是,thread cache可能会有多个,但是central control cache只有一个,要让多个thread cache对象访问一个central control cache对象,这里的central control cache需要设计成单例模式。

    3)page cache

    page cache是以页为单位进行内存管理的,它是将不同页数的内存利用哈希进行映射,最多映射128页内存,具体结构如下:

    page Cache申请和释放内存流程:

    •  当central control cache向page cache申请内存时,比如要申请8页的内存,它会先在span大小为8的位置找,如果没有就继续找9 10...128,那个有就从那个中切割8页。
    • 例如,走到54时才有内存,就从54处切8页返回给central control cache,将剩余的54-846页挂在46页处。
    • 当page cache中没有内存时,它直接申请一个128页的内存挂在128位置。当central control cache申请内存时再从128页切。

    2、设计细节

    1)thread cache

    根据申请内存大小计算对应的_freelists索引

    • 1~8都映射在索引为0处,9~16都在索引为2处......
    • 因此以8字节对齐时,可以表示为:((size + (2^3 - 1)) >> 3) - 1;
    • 如果申请的内存为129,索引如何计算?
    • 首先前128字节是按照8字节对齐的,因此:((129-128)+(2^4-1))>>4)-1 + 16
    • 上式中16表示索引为0~15的16个位置以8字节对齐。

    代码实现:

    //根据内存大小和对齐数计算对应下标
    static inline size_t _Intex(size_t size, size_t alignmentShift)
    {
    	//alignmentShift表示对齐数的位数,例如对齐数为8 = 2^3时,aligmentShift = 3
    	//这样可以将除法转化成>>运算,提高运算效率
    	return ((size + (1 << alignmentShift) - 1) >> alignmentShift) - 1;
    }
    //根据内存大小,计算对应的下标
    static inline size_t Index(size_t size)
    {
    	assert(size <= THREAD_MAX_SIZE);
    
    	//每个对齐数对应的索引个数,分别表示8 16 128 1024字节对齐
    	int groupArray[4] = {16,56,56,56};
    
    	if (size <= 128)
    	{
    		//8字节对齐
    		return _Intex(size, 3) + groupArray[0];
    	}
    	else if (size <= 1024)
    	{
    		//16字节对齐
    		return _Intex(size, 4) + groupArray[1];
    	}
    	else if (size <= 8192)
    	{
    		//128字节对齐
    		return _Intex(size, 7) + groupArray[2];
    	}
    	else if (size <= 65536)
    	{
    		//1024字节对齐
    		return _Intex(size, 10) + groupArray[3];
    	}
    
    	assert(false);
    	return -1;
    }

    freelist向中心缓存申请内存时需要对申请的内存大小进行对齐

    首先,需要申请的内存大小不够对齐数时都需要进行向上对齐。即,要申请的内存大小为1字节时需要对齐到8字节。如何对齐?不进行对齐可以吗?

    首先,不进行对齐也可以计算出freelist索引,当第一次申请内存时,freelist的索引位置切割后的内存大小就是实际申请的内存大小,并没有进行对齐,造成内存管理混乱。对齐方式如下:

    • 对齐数分别为8  = 2^3; 16 = 2^4 ; 128 = 2^7 ; 1024 = 2^10,转化成二进制后只有1个1.
    • 在对齐区间内,所有数+对齐数-1后一定是大于等于当前区间的最大值且小于下一个相邻区间的最大值。
    • 因此,size + 对齐数 - 1如果是8字节对齐只需将低3位变为0,如果是16字节对齐将低3位变为0......
    • 例如:size = 2时,对齐数为8;则size + 8 - 1 = 9,转为而进制位1001,将低三位变为0后为1000,转为十进制就是对齐数8.

    代码表示如下:alignment表示对齐数

    (size + alignment - 1) & ~(alignment - 1);

    注意:向这些小函数,定义成inline可以减少压栈开销。 ‘

    如何将小块内存对象“挂在”freelist链表中

    哈哈,前边已经为这里做好铺垫了。前边规定单个对象大小最小为8字节,32位系统下一个指针的大小为4字节,64位机器下一个指针的大小为8字节。前边我们规定单个对象最小大小为8字节就是为了无论是在32位系统下还是在64位系统下,都可以保存一个指针将小块对象链接起来。那么,如何使用一小块内存保存指针?

    直接在内存的前4/8个字节将下一块内存的地址保存,取内存时直接对该内存解引用就可以取出地址。

    访问:*(void**)(mem)

    每次从freelist中取内存或者归还内存时,直接进行头插或头删即可。

    从central control cache中申请内存,一次申请多少合适呢?

    这里的思路是采用“慢启动”的方式申请,即第一次申请申请一个,第二次申请2个....当达到一定大小(512个)时不再增加。这样做的好处是,第一次申请给的数量少可以防止某些线程只需要一个多给造成浪费,后边给的多可以减少从central control cache的次数从而提高效率。

    当使用慢启动得到的期望内存对象个数大于当前central control cache中内存对象的个数时,有多少给多少。因为,实际上目前只需要一个,我们多申请了不够,那就有多少给多少。当一个都没有的时候才会去page cache申请。

    什么时候thread cache将内存还给central controlcache?

    当一个线程将内存还给thread cache时,会去判断对应的_freelist的对应位置是否有太多的内存还回来(thread cache中内存对象的大小大于等于最个数的时候,就向central control cache还)。

    2)Central Control Cache 

    SpanList结构

     SpanList在central control cache中最重要的作用就是对大块内存管理,它存储的是一个个span类的对象,使用链表进行管理。结构如下:

    也就是说,SpanList本质上就是一个span链表。这里考虑到后边归还内存需要找到对应页归还,方便插入,这里将spanlist设置成双向带头循环链表。

    Span结构

    Span存储的是大块内存的信息,陪SpanList共同管理大块内存,它的内存单位是页(4K)。它的结构实际上就是一个个size大小的对象链接起来的链表。它同时也作为SpanList的节点,spanList是双向循环链表,因此span中还有next和prev指针。

    struct Span
    {
        PageID _pageId = 0;   // 页号
        size_t _n = 0;        // 页的数量

        Span* _next = nullptr;
        Span* _prev = nullptr;

        void* _list = nullptr;  // 大块内存切小链接起来,这样回收回来的内存也方便链接
        size_t _usecount = 0;    // 使用计数,==0 说明所有对象都回来了

        size_t _objsize = 0;    // 切出来的单个对象的大小
    };

    当spanList中没有内存时需要向PageCache申请内存,一次申请多少合适呢?

    根据申请的对象的大小分配内存,也就是说单个对象大小越小分配的页数越少,单个对象的大小越大分配到的内存越多。如何衡量多少?

    这里我们是通过thread cache中从central control cache中获取的内存对象的个数的上限来确定。也就是说,个数的上限*内存对象的大小就是我们要申请的内存的大小。在右移12位(1页)就是需要申请的页数。

    //计算申请多少页内存
    static inline size_t NumMovePage(size_t memSize)
    {
    	//计算thread cache最多申请多少个对象,这里就给多少个对象
    	size_t num = NumMoveSize(memSize);
    	//此时的nPage表示的是获取的内存大小
    	size_t nPage = num*memSize;
    	//当npage右移是PAGE_SHIFT时表示除2的PAGE_SHIFT次方,表示的就是页数
    	nPage >>= PAGE_SHIFT;
    
    	//最少给一页(体现了按页申请的原则)
    	if (nPage == 0)
    		nPage = 1;
    
    	return nPage;
    }
    	

     向central control cache申请一块内存,切割时如果最后产生一个碎片(不够一个对象大小的内存)如何处理?

    一旦产生这种情况,最后的碎片内存只能丢弃不使用。但是对于我们的程序来说是不会产生的,因为我们每次申请至少一页,4096可以整除我们所对应的任何一个大小的对象。

    central control cache何时将内存还给page cache?

    thread cache将多余的内存会还给central control cache中的spanlist对应的span,span中有一个usecount用来统计该span中有多少个对象被申请走了,当usecount为0时,表示所有对象都还回来了,则将该span还给page cache,合并成更大的span。

     3)Page Cache

    当从一个大页切出一个小页内存时,剩余的内存如何挂在对应位置?

    在Page cache中的span它是没有切割的,都是一个整页,也就是说这里的Span的list并没有使用到。这里计算内存的地址都是按照页号计算的,当一个Span中有多页内存时保存的是第一页的内存,那么就可以计算出剩余内存和切走内存的页号,设置相应的页号进行映射即可。

    从一个大的Span中切时,采用头切还是尾切?

    Span中如何通过页号计算地址?

    每一页大小都是固定的,当我们从系统申请一块内存会返回该内存的首地址,申请内存时返回的都是一块连续的内存,所以我们可以使用内存首地址/页大小的方式计算出页号,通过这种方式计算出来的一大块内存的多个页的页号都是连续的。

    Page Cache向系统申请内存

    Page Cache向系统申请内存时,前边我们说过每次直接申请128页的内存。这里需要说明的是,我们的项目中不能出现任和STL中的数据结构和库函数,因此这里申请内存直接采用系统调用VirtualAlloc。下面对VirtualAlloc详细解释:

    VirtualAlloc是一个Windows API函数,该函数的功能是在调用进程的虚地址空间,预定或者提交一部分页。简单点的意思就是申请内存空间。

    函数声明如下:

    LPVOID VirtualAlloc{

    LPVOID lpAddress, // 要分配的内存区域的地址

    DWORD dwSize, // 分配的大小

    DWORD flAllocationType, // 分配的类型

    DWORD flProtect // 该内存的初始保护属性

    };

    参数说明:

    • LPVOID lpAddress, 分配内存区域的地址。当你使用VirtualAlloc来提交一块以前保留的内存块的时候,lpAddress参数可以用来识别以前保留的内存块。如果这个参数是NULL,系统将会决定分配内存区域的位置,并且按64-KB向上取整(roundup)。
    • SIZE_T dwSize, 要分配或者保留的区域的大小。这个参数以字节为单位,而不是页,系统会根据这个大小一直分配到下页的边界DWORD
    • flAllocationType, 分配类型 ,你可以指定或者合并以下标志:MEM_COMMIT,MEM_RESERVE和MEM_TOP_DOWN。
    • DWORD flProtect 指定了被分配区域的访问保护方式

    注:PageCache中有一个map用来存储pageId和Span的映射。在释放内存时,通过memSize计算出pageId,在通过PageId在map中查找对应的Span从而就可以获得单个对象的大小,在根据单个对象的大小确定是要将内存还给page cache还是还给central control cache。

    central control cache释放回来的内存如何合并成大内存?

    通过span中的页号查找前一页和后一页,判断前一页和后一页是否空闲(没有被申请的内存),如果空闲就进行和并,合并完后重新在map中进行映射。

    注意:将PageCache和CentralControlCache设置成单例模式,因为多个线程对同时使用一个page cache和central control cache进行内存管理。

    单例模式简单介绍

    • 单例模式,顾名思义只能创建一个实例。
    • 有两种实现方式:懒汉实现和饿汉实现
    • 做法:将构造函数和拷贝构造函数定义成私有且不能默认生成,防止在类外构造对象;定义一个本身类型的成员,在类中构造一个对象,提供接口供外部调用。

    4)加锁问题

    • 在central control cache和page cache中都存在多个线程访问同一临界资源的情况,因此需要加锁。
    • 在central control cache中,不同线程只要访问的不是同一个大小的内存对象,则就不需要加锁,可以提高程序的运行效率(加锁后就有可能导致线程挂起等待),也就是说central control cache中是“桶锁”。需要改freelist那个位置的内存,就对那个加锁。
    • page cache中,需要对申请和合并内存进行加锁。
    • 这里我们统一使用互斥锁。

    注意:使用map进行映射,虽然说我们对pagecache进行了加锁,不会早成写数据的冲突,但是我们还向外提供了查找的接口,就有可能导致一个线程在向map中写而另一个线程又查找,出现线程安全问题,但是如果给查找位置加锁,这个接口会被频繁的调用,造成性能的损失。而在tcmalloc中采用基数树来存储pageId和span的映射关系,从而提高效率。

    附:基数树

    三、测试

    1、单元测试

    void func1()
    {
    	for (size_t i = 0; i < 10; ++i)
    	{
    		hcAlloc(17);
    	}
    }
    
    void func2()
    {
    	for (size_t i = 0; i < 20; ++i)
    	{
    		hcAlloc(5);
    	}
    }
    
    //测试多线程
    void TestThreads()
    {
    	std::thread t1(func1);
    	std::thread t2(func2);
    
    
    	t1.join();
    	t2.join();
    }
    
    //计算索引
    void TestSizeClass()
    {
    	cout << SizeClass::Index(1035) << endl;
    	cout << SizeClass::Index(1025) << endl;
    	cout << SizeClass::Index(1024) << endl;
    }
    
    //申请内存
    void TestConcurrentAlloc()
    {
    	void* ptr0 = hcAlloc(5);
    	void* ptr1 = hcAlloc(8);
    	void* ptr2 = hcAlloc(8);
    	void* ptr3 = hcAlloc(8);
    
    	hcFree(ptr1);
    	hcFree(ptr2);
    	hcFree(ptr3);
    }
    
    //大块内存的申请
    void TestBigMemory()
    {
    	void* ptr1 = hcAlloc(65 * 1024);
    	hcFree(ptr1);
    
    	void* ptr2 = hcAlloc(129 * 4 * 1024);
    	hcFree(ptr2);
    }
    
    //int main()
    //{
    //	//TestBigMemory();
    //
    //	//TestObjectPool();
    //	//TestThreads();
    //	//TestSizeClass();
    //	//TestConcurrentAlloc();
    //
    //	return 0;
    //}

    2、性能测试

    void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
    {
    	//创建nworks个线程
    	std::vector<std::thread> vthread(nworks);
    	size_t malloc_costtime = 0;
    	size_t free_costtime = 0;
    
    	//每个线程循环依次
    	for (size_t k = 0; k < nworks; ++k)
    	{
    		//铺货k
    		vthread[k] = std::thread([&, k]() {
    			std::vector<void*> v;
    			v.reserve(ntimes);
    
    			//执行rounds轮次
    			for (size_t j = 0; j < rounds; ++j)
    			{
    				size_t begin1 = clock();
    				//每轮次执行ntimes次
    				for (size_t i = 0; i < ntimes; i++)
    				{
    					v.push_back(malloc(16));
    				}
    				size_t end1 = clock();
    
    				size_t begin2 = clock();
    				for (size_t i = 0; i < ntimes; i++)
    				{
    					free(v[i]);
    				}
    				size_t end2 = clock();
    				v.clear();
    
    				malloc_costtime += end1 - begin1;
    				free_costtime += end2 - begin2;
    			}
    		});
    	}
    
    	for (auto& t : vthread)
    	{
    		t.join();
    	}
    
    	printf("%u个线程并发执行%u轮次,每轮次malloc %u次: 花费:%u ms\n",
    		nworks, rounds, ntimes, malloc_costtime);
    
    	printf("%u个线程并发执行%u轮次,每轮次free %u次: 花费:%u ms\n",
    		nworks, rounds, ntimes, free_costtime);
    
    	printf("%u个线程并发malloc&free %u次,总计花费:%u ms\n",
    		nworks, nworks*rounds*ntimes, malloc_costtime + free_costtime);
    }
    
    
    // 单轮次申请释放次数 线程数 轮次
    void BenchmarkConcurrentMalloc(size_t ntimes, size_t nworks, size_t rounds)
    {
    	std::vector<std::thread> vthread(nworks);
    	size_t malloc_costtime = 0;
    	size_t free_costtime = 0;
    
    	for (size_t k = 0; k < nworks; ++k)
    	{
    		vthread[k] = std::thread([&]() {
    			std::vector<void*> v;
    			v.reserve(ntimes);
    
    			for (size_t j = 0; j < rounds; ++j)
    			{
    				size_t begin1 = clock();
    				for (size_t i = 0; i < ntimes; i++)
    				{
    					v.push_back(hcAlloc(16));
    				}
    				size_t end1 = clock();
    
    				size_t begin2 = clock();
    				for (size_t i = 0; i < ntimes; i++)
    				{
    					hcFree(v[i]);
    				}
    				size_t end2 = clock();
    				v.clear();
    
    				malloc_costtime += end1 - begin1;
    				free_costtime += end2 - begin2;
    			}
    		});
    	}
    
    	for (auto& t : vthread)
    	{
    		t.join();
    	}
    
    	printf("%u个线程并发执行%u轮次,每轮次concurrent alloc %u次: 花费:%u ms\n",
    		nworks, rounds, ntimes, malloc_costtime);
    
    	printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u ms\n",
    		nworks, rounds, ntimes, free_costtime);
    
    	printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u ms\n",
    		nworks, nworks*rounds*ntimes, malloc_costtime + free_costtime);
    }
    
    int main()
    {
    	cout << "==========================================================" << endl;
    	BenchmarkMalloc(100000, 4, 10);
    	cout << endl << endl;
    
    	BenchmarkConcurrentMalloc(100000, 4, 10);
    	cout << "==========================================================" << endl;
    
    	return 0;
    }

    结果比较

     附1:完整代码

    展开全文
  • 项目:高并发内存池

    千次阅读 多人点赞 2022-02-28 23:19:05
    模拟实现出一个自己的高并发内存池,在多线程环境下缓解了锁竞争问题,相比于malloc/free效率提高了25%左右,将内存碎片保持在10%左右。
  • dpdk内存池 mpool 实现机制

    千次阅读 2020-01-19 13:33:56
    另一种是使用内存池,也是通过在大页内存上申请空间方式。 两种有什么区别呢?虽然两者最终都是在大页内存上获取空间,但内存池这种方式直接在大页内存上获取,绕开了rte_malloc调用。rte_malloc一般用于申请小的...
  • 实现一个高并发的内存池

    千次阅读 2019-02-23 12:03:27
    为什么要使用内存池4.三种内存池的演变4.1 最简单的内存分配器4.2 定长内存分配器4.3 Hash映射的多种定长内存分配器5.了解malloc底层原理6. 实现高并发的内存池6.1 高并发内存池设计6.2 设计ThreadCache类6.3 自由...
  • 如果将互联网应用比喻成冲浪的话, 可能需要先学会在中游泳吧。引子AI赋能万物,老码农的伙伴们也曾经开发了一个基于图数据库的知识问答系统,在压力测试的时候发现随着并发数的增加,响应的时延明显变长,看时延...
  • 实现一个高并发内存池-----对比Malloc

    千次阅读 多人点赞 2019-05-25 16:18:27
    放到一个池内,有程序自管理,这样可以提高资源的利用率,也可以保证本程序占有的资源数量,经常使用的池化技术包括内存池,线程池,和连接池等,其中尤以内存池和线程池使用最多。 1.2 内存池 内存池(Memory Pool...
  • 内存池简单实现(一)

    千次阅读 2018-08-08 15:21:23
    概念 内存池就是在程序启动时,预先向堆中申请一部分内存,...而数据包的内存采用原始的new-delete模式,会大大降低服务器性能,所以想到了使用内存池技术。 在此实例中有几个硬性条件: 1、数据包最大长度可...
  • nginx内存占用高---内存池使用思考

    千次阅读 2019-01-17 19:44:03
    nginx内存占用高—内存池使用思考 问题现象 nginx top 进程 虚拟内存 200G 实际内存5G 和 CDN 平台相比要高很多 排查思路 使用pmap -p 进程号,发现从系统角度确实 有分配几百G,但是实际内存5G 说明分配的大部分...
  • 开源C++函数库Boost内存池使用与测试

    千次阅读 2014-03-11 11:53:51
    Boost库是一个可移植的开源C++函数库,鉴于STL(标准模板库)已经成为C++语言的一个组成部分,可以毫不夸张的说,Boost... Boost内存池,即boost.pool库,是由Boost提供的一个用于内存池管理的开源C++库。作为Boost中影
  • 内存池: 自定义内存池的思想通过这个"池"字表露无疑,应用程序可以通过系统的内存分配调用预先一次性申请适当...应用程序自定义的内存池根据不同的适用场景又有不同的类型。   从线程安全的角度来...
  • 一文看懂内存池原理及创建(C++实现)

    千次阅读 多人点赞 2020-09-27 10:27:38
    实现一个高并发的内存...其内涵在于:将程序中需要经常使用的核心资源先申请出来,放到一个池内,有程序自管理,这样可以提高资源的利用率,也可以保证本程序占有的资源数量,经常使用的池化技术包括内存池,线程池...
  • 各种池---内存池的高效实现(C语言)

    万次阅读 多人点赞 2017-09-19 14:30:22
    在编程过程中,尤其是对于C语言开发者,其实编程就是在使用内存,不停地变化内存中的数据。当我们想开辟一片新的内存使用时,就会使用malloc实现。但是通过查阅很多资料,发现频繁的使用malloc并不是很好的选择。...
  • DPDK 内存池rte_mempool实现(二十三)

    千次阅读 2020-05-31 18:48:11
    另一种是使用内存池,也是通过在大页内存上申请空间方式。 两种有什么区别呢?虽然两者最终都是在大页内存上获取空间,但内存池这种方式直接在大页内存上获取,绕开了rte_malloc调用。rte_malloc一般用于申请小的...
  • 最近在STL当中看到了第二级内存分配器,这里有个内存池的内容,在这在知乎上看到了内存池的相关内容,所以萌生了一个想自己写一个简单的内存池的想法。这种简单的内存池,援引自知乎的: 实现固定内存分配器: 即...
  • 应用程序自定义的内存池根据不同的适用场景又有不同的类型。 从线程安全的角度来分,内存池可以分为单线程内存池和多线程内存池。 单线程内存池整个生命周期只被一个线程使用,因而不需要考虑互斥访问的问题; 多...
  • dpdk内存池rte_mempool实现

    千次阅读 2019-08-23 07:44:42
    另一种是使用内存池,也是通过在大页内存上申请空间方式。 两种有什么区别呢?虽然两者最终都是在大页内存上获取空间,但内存池这种方式直接在大页内存上获取,绕开了rte_malloc调用。rte_malloc一般用于申请小的...
  • 【项目设计】高并发内存池

    万次阅读 多人点赞 2022-02-08 10:38:41
    文章目录项目简介内存池的概念定长内存池的实现整体框架设计threadcachethreadcache整体设计threadcache哈希桶映射对齐规则threadcacheTLS无锁访问centralcachecentralcache整体设计centralcache结构设计...
  • 一个简单实用的C++内存池

    千次阅读 2016-02-21 16:11:34
    1 自定义内存池性能优化的原理 如前所述,读者已经了解到"堆"和"栈"的区别。而在编程实践中,不可避免地要大量用到堆上的内存。例如在程序中维护一个链表的数据结构时,每次新增或者删除一个链表的节点,都需要从...
  • C++ 内存池介绍与经典内存池的实现

    万次阅读 多人点赞 2015-11-01 00:04:38
    1.默认内存管理函数的不足利用默认的内存管理函数new/delete或malloc/free在堆上分配和释放内存会有一些额外的开销。系统在接收到分配一定大小内存的请求时,首先查找内部维护的内存空闲块表,并且需要根据一定的...
  • 【Netty】高性能原因:缓冲区内存池

    千次阅读 2021-01-02 17:52:41
    为了尽量重用缓冲区,Netty 提供了基于内存池的缓冲区重用机制。 Netty ByteBuf 继承关系如下 1.性能对比测试 Netty 提供了多种内存管理策略,通过在启动辅助类中配置相关参数,可以实现差异化的定制。下面通过性能...
  • 预制系统 首先需要知道预制系统需要做什么: 如果已经存在已回收的版本,应该重新生成第一个可用的对象 如果不存在已回收的版本,应该从预制体中实例化新的GameObject 在上述两种情况下, 都应该在附加到...
  • 内存池使用场景 1)一个连接一个内存池,连接存在时内存池只分配内存,不释放内存。连接断开时把整个内存池释放。(如果我们手写内存池,只推荐这种场景) 2)一个进程一个内存池,进程退出时才释放。这种情况不推荐...
  • Linux内存池技术

    千次阅读 2015-07-19 21:25:19
    看到一篇关于内存池技术的介绍文章,受益匪浅,转贴至此。  原贴地址:http://www.ibm.com/developerworks/cn/linux/l-cn-ppp/index6.html  6.1 自定义内存池性能优化的原理  如前所述,读者已经了解到...
  • nginx源码分析之内存池实现原理

    千次阅读 2017-02-21 19:47:47
    内存池实质上是接替OS进行内存管理,应用程序申请内存时不再与OS打交道,而是从内存池中申请内存或者释放内存到内存池,因此,内存池在实现的过程中,必然有一部分操作时从OS中申请内存,或者释放内存到OS,如下图所...
  • Golang多级内存池设计与实现

    千次阅读 2018-03-05 00:00:00
    我们生产环境使用的是阿里云,打完补丁后,几台IO密集型的机器性能下降明显,从流量和cpu load估计,性能影响在50%左右,不是说好的最多下降30%麽。在跑的业务是go写的,使用go pprof对程序profiling了一下,无意中...
  • Java数据库连接比较及使用场景

    千次阅读 2016-05-21 13:48:38
    数据库连接是一种池化技术,预先创建好数据库连接,保存在内存中,当需要连接时,从中取出即可,使用完后放回连接。下面我们介绍Java中常用的数据库连接,主要介绍的内容有以下几点: 1. 优点及不足 2. ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 125,108
精华内容 50,043
关键字:

内存池使用的场景