高并发内存池项目(concurrent memory pool)

2023-11-11

一、高并内存池概念

内存池(Memory Pool) 是一种动态内存分配与管理技术。 通常情况下,程序员习惯直接使用 new、delete、malloc、free 等API申请分配和释放内存,这样导致的后果是:当程序长时间运行时,由于所申请内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。内存池则是在真正使用内存之前,先申请分配一大块内存(内存池)留作备用,当程序员申请内存时,从池中取出一块动态分配,当程序员释放内存时,将释放的内存再放入池内,再次申请池可以 再取出来使用,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。

由于现在硬件条件已经很成熟,大多数运行环境都是多核的,为了提高效率,则高并发这一情况应运而生,对于高并发内存池,则是基于多线程并发申请使用的一个内存池称为高并发内存池。


二、项目介绍

本项目参考了谷歌 tcmalloc 设计模式,设计实现了高并发的内存池。基于 win10 环境 VS2013,采用 C++进行编程,池化技术、多线程、TLS、单例模式、互斥锁、链表、哈希等数据结构。该项目利用了 thread cache、central、cache、page cache 三级缓存结构,基于多线程申请释放内存的场景,最大程度提高了效率,解决了绝大部分内存碎片问题。

三、项目细节

(一)项目设计目标

现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。所以这次我们实现的内存池需要考虑以下几方面的问题。
1. 内存碎片问题。
2. 性能问题。
3. 多核多线程环境下,锁竞争问题。
 

(二)项目结构

  • concurrent memory pool主要由线程缓存(threadcache)、中心缓存(centralcache)、页缓存(pagecache)3个部分构成,如下图

 ​​​

1.thread cache

  •  为了保证效率,我们使用线程局部存储(thread local storage,TLS)技术保存每个线程本地的ThreadCache的指针,这样大部分情况下申请释放内存是不需要锁的,线程缓存是每个线程独有的,用于小于64k的内存的分配,但并不是一定是要64k,只是前人总结的一个合适值。线程从这里申请内存不需要加锁,每个线程独享一个cache,这也就是这个并发线程池高效的地方。
class ThreadCache
{
public:

	// 申请和释放内存对象
	void* Allocate(size_t size);
	void Deallocate(void* ptr, size_t size);

	// 从中心缓存获取对象
	void* FetchFromCentralCache(size_t index, size_t size);

	// 释放对象时,链表过长时,回收内存回到中心缓存
	void ListTooLong(FreeList& list, size_t size);
private:

    //创建了一个哈希结构,每个位置里存放一个FreeList对象
	FreeList _freeLists[NFREELISTS];
};

//TLS技术
static __declspec(thread) ThreadCache* tls_threadcache = nullptr;

该结构每个位置存放一个FreeList,每个自由链表下都可以挂自己的内存块。

申请内存:
1. 当内存申请size<=64k时在thread cache中申请内存,计算size在自由链表中的位置,如果自由链表中有内存对象时,直接从FistList[i]中Pop一下对象,时间复杂度是O(1),且没有锁竞争。
2. 当FreeList[i]中没有对象时,则批量从central cache中获取一定数量的对象,插入到自由链表并返回一个对象。
释放内存:
1. 当释放内存小于64k时将内存释放回thread cache,计算size在自由链表中的位置,将对象Push到FreeList[i].
2. 当链表的长度过长,则回收一部分内存对象到central cache。
 

2. central cache

  • 中心缓存是所有线程所共享,本质是由一个哈希映射的span对象自由链表构成thread cache是按需从central cache中获取的对象。central cache周期性的回收thread cache中的对象,避免一个线程占用了太多的内存,而其他线程的内存吃紧。达到内存分配在多个线程中更均衡的按需调度的目的。central cache是存在竞争的,所以从这里取内存对象是需要加锁,不过一般情况下在这里取内存对象的效率非常高,所以这里竞争不会很激烈。
  • 这里要注意,要保证centralcache是全局唯一的,这里我们需要将centralcache类设计成单例模式。

class CentralCache
{
public:
    //唯一获得该对象的接口
	static CentralCache* GetInstance()
	{
		return &_sInst;
	}

	// 从中心缓存获取一定数量的对象给thread cache
	size_t FetchRangeObj(void*& start, void*& end, size_t n, size_t byte_size);

	// 从SpanList或者page cache获取一个span
	Span* GetOneSpan(SpanList& list, size_t byte_size);

	// 将一定数量的对象释放到span跨度
	void ReleaseListToSpans(void* start, size_t byte_size);
private:
	SpanList _spanLists[NFREELISTS]; // 按对齐方式映射

private:
	CentralCache()
	{}
    //封死它的拷贝构造
	CentralCache(const CentralCache&) = delete;
    CentralCache& operator=(const CentralCache&) = delete;
    //定义全局唯一类
	static CentralCache _sInst;
};

申请内存:
1. 当thread cache中没有内存时,就会批量向central cache申请一些内存对象,central cache也有一个哈希映射的freelist,freelist中挂着span,从span中取出对象给thread cache,这个过程是需要加锁的。
2. central cache中没有非空的span时,则将空的span链在一起,向page cache申请一个span对象,span对象中是一些以页为单位的内存,切成需要的内存大小,并链接起来,挂到span中。
3. central cache的span中有一个use_count,分配一个对象给thread cache,就++use_count。
释放内存:
1. 当thread_cache过长或者线程销毁,则会将内存释放回central cache中的,释放回来时--use_count。当use_count减到0时则表示所有对象都回到了span,则将span释放回pagecache,page cache中会对前后相邻的空闲页进行合并。

这里要注意centralcache中span是用双向链表进行连接的,每个span对象里都有一个list,就是每个向pagecache要的span都已经被切好了,比如说8k大小桶中,一块大的span里已经被切分成许多8k大小的内存块,他们分别用list进行内部连接,他当treadcache需要时,就给其一定数量的。但是整个span整体是被挂在spanlist中。

可以用珍珠项链可以形容,许多串整体的珍珠项链被挂在一个支架上,当顾客突然想要这串项链中的3个珍珠,那么就需要将珍珠项链里的绳子拆开取出那三个给顾客,这里支架就相当于spanlist,珍珠项链里的绳子相当于span中的list。

 3.page cache

页缓存是在central cache缓存上面的一层缓存,存储的内存是以页为单位存储及分配的,central cache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小的小块内存,分配给central cache。page cache会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。

class PageCache
{
public:
	static PageCache* GetInstance()
	{
		return &_sInst;
	}

	// 向系统申请k页内存挂到自由链表
	void* SystemAllocPage(size_t k);

	Span* NewSpan(size_t k);

	// 获取从对象到span的映射
	Span* MapObjectToSpan(void* obj);

	// 释放空闲span回到Pagecache,并合并相邻的span
	void ReleaseSpanToPageCache(Span* span);
private:
	SpanList _spanList[NPAGES];	// 按页数映射

	//std::mutex _map_mtx;    //专门给map用的锁
	std::unordered_map<PageID, Span*> _idSpanMap;
	std::recursive_mutex _mtx;


private:
	PageCache()
	{}

	PageCache(const PageCache&) = delete;
    PageCache& operator=(const PageCache&) = delete;
	// 单例
	static PageCache _sInst;
};

 申请内存:
1. 当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有则向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4page,4page后面没有挂span,则向后面寻找更大的span,假设在10page位置找到一个span,则将10page span分裂为一个4page span和一个6page span。
2. 如果找到128 page都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式申请128page span挂在自由链表中,再重复1中的过程。
释放内存:
1. 如果central cache释放回一个span,则依次寻找span的前后page id的span,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。

  • 这里要注意该结构下的span都是未切分的一个个整体span,当每次centralcache需要时在进行切分,并且将每个span地址与页号通过哈希map进行映射关系建立。这里一页我们按照window下1页4k为基准进行设置。


 4.对象大小的映射对齐、向中心缓存获取内存个数及向pagecache获取页大小计算

// 8 + 7 = 15
// 7 + 7
// ...
// 1 + 7 = 8
static size_t Index(size_t size)
{
	return ((size + (2 ^ 3 - 1)) >> 3) - 1;
}

// 管理对齐和映射等关系
class SizeClass
{
public:
	// 控制在1%-12%左右的内碎片浪费
	// [1,128]					8byte对齐	     freelist[0,16)
	// [129,1024]				16byte对齐		 freelist[16,72)
	// [1025,8*1024]			128byte对齐	     freelist[72,128)
	// [8*1024+1,64*1024]		1024byte对齐     freelist[128,184)

	// [1,8]   +7 [8,15]    8
	// [9, 16] +7 [16,23]   16
	static inline size_t _RoundUp(size_t bytes, size_t align)
	{
		return (((bytes)+align - 1) & ~(align - 1));
	}

	// 对齐大小计算,浪费大概在1%-12%左右
	static inline size_t RoundUp(size_t bytes)
	{
		//assert(bytes <= MAX_BYTES);

		if (bytes <= 128) {
			return _RoundUp(bytes, 8);
		}
		else if (bytes <= 1024) {
			return  _RoundUp(bytes, 16);
		}
		else if (bytes <= 8192) {
			return  _RoundUp(bytes, 128);
		}
		else if (bytes <= 65536) {
			return  _RoundUp(bytes, 1024);
		}
		else
		{
			return _RoundUp(bytes, 1 << PAGE_SHIFT);
		}

		return -1;
	}

	static inline size_t _Index(size_t bytes, size_t align_shift)
	{
		return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
	}

	// 计算映射的哪一个自由链表桶
	static inline size_t Index(size_t bytes)
	{
		assert(bytes <= MAX_BYTES);

		// 每个区间有多少个链
		static int group_array[4] = { 16, 56, 56, 56 };
		if (bytes <= 128) {
			return _Index(bytes, 3);
		}
		else if (bytes <= 1024) {
			return _Index(bytes - 128, 4) + group_array[0];
		}
		else if (bytes <= 8192) {
			return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];
		}
		else if (bytes <= 65536) {
			return _Index(bytes - 8192, 10) + group_array[2] + group_array[1] + group_array[0];
		}

		assert(false);

		return -1;
	}

	// 一次从中心缓存获取多少个
	static size_t NumMoveSize(size_t size)
	{
		if (size == 0)
			return 0;

		// [2, 512],一次批量移动多少个对象的(慢启动)上限值
		// 小对象一次批量上限高
		// 小对象一次批量上限低
		int num = MAX_BYTES / size;
		if (num < 2)
			num = 2;

		if (num > 512)
			num = 512;

		return num;
	}

	// 计算一次向系统获取几个页
	// 单个对象 8byte
	// ...
	// 单个对象 64KB
	static size_t NumMovePage(size_t size)
	{
		size_t num = NumMoveSize(size);
		size_t npage = num*size;

		npage >>= 12;
		if (npage == 0)
			npage = 1;

		return npage;
	}
};

5.FreeList、span、SpanList结构

void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
	// nworks个线程
	//每个线程跑rounds轮
	//每个线程跑ntimes次

	std::vector<std::thread> vthread(nworks);// 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)//每个线程跑rounds轮
			{
				size_t begin1 = clock();
				for (size_t i = 0; i < ntimes; i++)//每个线程跑ntimes次
				{
					v.push_back(malloc(260));//线程开辟空间
				}
				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); // n
	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(ConcurrentAlloc(260));
				}
				size_t end1 = clock();

				size_t begin2 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{
					ConcurrentFree(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(10000, 4, 10);
	cout << endl << endl;

	BenchmarkConcurrentMalloc(10000, 4, 10);
	cout << "==========================================================" << endl;

	system("pause");
	return 0;
}

5.加锁场景

a.在centralcache结构中需要进行加桶锁,也就是给FetchRangeObj函数和ReleseListToSpans函数进行加。

b.在pagecache结构中加大锁,也就是NewSpan函数和ReleaseSpanToPageCache函数进行加,还有就是在对span地址和页号映射时需要的MapObjectToSpan函数进行加。

四、项目测试

测试时一定要确保编译器在relese情况下,而不是debug!!!

五、项目总结

(一)优点

  1. 增加动态申请的效率
  2. 减少陷入内核的次数
  3. 减少系统内存碎片
  4. 提升内存使用率
  5. 尽量减少锁竞争
  6. 应用于多核多线程场景

(二)不足

1.当前实现的项目中我们并没有完全脱离malloc,比如在内存池自身数据结构的管理中,如SpanList中的span等结构,我们还是使用的new Span这样的操作,new的底层使用的是malloc,所以还不足以替换malloc,因为们本身没有完全脱离它。

解决方案:malloc、new(频繁申请的小块内存,如:span->对象池     内存大块内存:virtualalloc/brk/mmap)map、thread、mutex....

2.平台及兼容性
linux等系统下面,需要将VirtualAlloc替换为brk等。这个是小问题 。
x64系统下面,当前的实现支持不足。比如:id查找Span得到的映射,我们当前使用的是map<id, Span*>。在64位系统下面,这个数据结构在性能和内存等方面都是撑不住。需要改进后基数树。

(三)面试常见问题

1.如何去替代malloc?替代malloc的原理是什么?

不同平台替换方式不一样,linux下使用weak alias的方式进行替换,window下使用hook的钩子技术进行替换,不用更改原来的代码,只需要使用钩子将代码中使用malloc的地方勾过来让其执行该内存池代码,所有的malloc的调用都跳转到了tc_malloc的实现。它也通常用来写外挂,用来进行系统层面函数更改。Map使用基数树来进行替换,malloc使用对象池或者virtual alloc申请大块内存。

2.能不能把threadcache和centralcache合并掉,减掉一层?

不能,Central核心作用是承上启下的作用,假设把centracache直接去掉,就意味着threadcache和centralcache直接进行对接,会产生一个问题,pagecache中的span有些是切好的,有些是没有切好的,而且不是一下子就给threadcache,有可能给一部分,可能会留下一部分,这是第一个问题,第二个问题是还回来是还一部分,切过的和没切过的混在一起会有问题,比如申请一块大块内存,在pagecache中找,但是却不知道找到的到底是切好的还是没切好的,虽然USecount也能进行判断,但是切的多了混在一起找的时候查找效率没有那么高。再其次均衡调度作用就不明显了,threadcache中8字节专门给centralcahe8字节用的,但是如果在pagecache中就比较混乱,因为pagecache是按照页进行映射的,更大的问题在于centralcache加的是桶锁,pagecache虽然也是一个一个映射的桶,但是它涉及到一个span的合并和切分,span会在各个桶之间流动,就不能使用桶锁,就必须使用一把大锁进行锁住,但是centralcache就不会再各个桶之间进行流动。

小结:

1.Centralcache均衡多个线程之间的同一个大小的内存需求

2.他的span都是至少有部分在用的,区分pagecache都是大块完整。

3.它实现的是桶锁,因为一个span只会给一个桶用,不会再桶之间流动,效率更高,如果没有他的话,pagecache是一把大锁,因为pagecache中的span需要切小和合大,会在多个映射桶之间流动。

3.max一定是64k吗?一定是以8k对齐按照我们那种分段映射对齐吗?

不一定,这个根据设计者需求,可能换个人参数就全变了,依据64k控制10%左右的浪费设计了映射规则,后面为什么是128页呢也就是一次性要了0.5兆?这个也是不确定的,这个值至少要比最大的单个对象大小大,也就是至少大于16个页,最少也得5、60页大小,但是不能过分大,太大表示一次对系统要的太大了,会造成浪费。

4.threadcache销毁了,如果他还有内存没给centrlcache怎么办?假设这个线程有内存泄露或者它还没有达到listtolang哪个条件,有可能有一些内存还没有还回来或者挂在threadcache中,但是这个线程销毁了,那么这个内存就没有回到这个centralcache,centralcache也不会回到pagecache,会耽搁小页合大,还会导致一些内存的泄露,有没有什么办法解决呢?

解决方法就是给这个项目注册一个回调函数,只要线程结束,函数作用是把threadcache里面给clear掉,把每个桶数据都往下一个还,在此创建tls时就进行处理,在new线程空间时同时注册一个回调函数,一旦崩溃就是清理掉这个回调函数。

六、项目源码

关于项目源码centralcache和pagecache设计成两种单例模式,一种为饿汉,一种为懒汉。

https://gitee.com/ishao97/ishao97.git

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

高并发内存池项目(concurrent memory pool) 的相关文章

随机推荐

  • 关于COLA架构的讨论

    理解 分层 概念网上可以搜到很多 大体分为 adapter client app infra domain 这五层 图例这里有 就不贴了 adapter和app相当于spring里的controller service domain是领域模
  • 推荐引擎分为哪几类,个性化推荐引擎的介绍

    在信息时代的今天 大数据为用户获取方方面面的信息提高了效率 更可以智能的帮助用户从海量内容中快速找到想要阅读的信息 或者从海量商品中快速找到想要购买的商品 推荐引擎的发展让选择不明确的用户更加了解她们的需求和喜好 下面以内容产品和电商产品为
  • apk包加固后重新签名

    使用jarsigner对未签名的加固包进行签名 建议您使用之前对APP签名时使用的keystore对加固包进行签名 jarsigner digestalg SHA1 sigalg MD5withRSA verbose keystore yo
  • 动态代码块、静态代码块、静态方法、静态变量(属性 )、构造方法

    1 动态代码块 class Super int a 10 public void m1 System out println m1 动态代码块 System out println 动态代码块开始执行 System out println
  • imblearn 安装

    imblearn 安装 官网安装教程 踩坑经过 1 有些库版本达不到要求 imblearn需要依赖某些Python模块 下面是最新版0 7 0的依赖要求 python gt 3 6 numpy gt 1 13 3 scipy gt 0 19
  • smbms-AJAX验证旧密码实现

    优化密码修改使用ajax 主要是与前端接轨 1 阿里巴巴fastjson maven导入 导入包
  • matlab:txt数据文件的读出与读取

    输出数据 fid fopen hello txt w 需要改文件名称的地方 fprintf fid 10 3f n data data 需要导出的变量名称 10位有效数字 保留3位小数 包含小数点 f为双精度 g为科学计数法 fclose
  • 全志V3S开启启动

    一 TurnOffMute sh 创建自己需要的脚本 我这里创建关闭静音的脚本 vi TurnOffMute sh 然后往其中添加需要执行的命令 然后赋予可执行的权限 chmod 777 TurnOffMute sh 二 etc rc lo
  • 使用matlab对行人视频进行检测的代码的分析

    function F hogcalculator img cellpw cellph nblockw nblockh nthet overlap isglobalinterpolate issigned normmethod HOGCALC
  • linux定时清理文件的脚本

    1 新建清理文件脚本 vim autodelfile sh bin sh find 对应目录 mtime 天数 name 文件名 exec rm rf find linux的查找命令 用户查找指定条件的文件 home trans app f
  • 虚拟机的三种网络模式详解

    虚拟机的三种网络模式详解 1 桥接模式 此模式下 虚拟机的操作系统就像和物理机同一段网络中的物理机一样 它可以访问网络中的任何机器 同时只要物理机可以访问网络 虚拟机也可以实现上网 此模式是懒人模式首选 但换来一个问题就是 如果你的物理机网
  • PyCharm 新项目关联到码云(Gitee)源代码管理

    1 在 PyCharm 中创建新项目 打开 PyCharm 点击 Create New Project 或 File gt New Project 然后按照提示完成新项目的创建 2 在码云上创建新仓库 登录到你的码云账户 点击 新建仓库 输
  • Elasticsearch实战(七)---BestFields MostFields CrossFields 多字段搜索策略

    Elasticsearch实战 BestFields MostFields 搜索策略 文章目录 Elasticsearch实战 BestFields MostFields 搜索策略 1 字段中心及词条中心查询 2 Multi match q
  • qt实现QLabel上显示的文字有描边

    qt实现文字描边 效果图 开发环境 项目示例 综述 效果图 此程序运行的效果 开发环境 1 关于我的开发环境 我目前有点迷惑 我的QtCreator中帮助 关于QtCreator 得到如下所示 但是我的安装包上却写着5 12 9 我的理解就
  • Bash 脚本 set 命令教程

    转自 http www ruanyifeng com blog 2017 11 bash set html utm source tool lu 服务器的开发和管理离不开 Bash 脚本 掌握它需要学习大量的细节 set命令是 Bash 脚
  • 在自己电脑上的idea运行java web项目 如何用外网访问

    目的 本人目前Android开发比如手机的销售统计激活数据上传 自己先写一个网络接口测试等后端写好了换上去就行 用自己的电脑当作服务器 使用IntelliJ IDEA 创建一个springboot 部署在自己电脑上 使用手机请求网络接口 或
  • ddt数据驱动常见的用法【多测师_王sir】

    一 背景 一般进行接口测试时 每个接口的传参都不止一种情况 一般会考虑正向 逆向等多种组合 所以在测试一个接口时 通常会编写多条case 而这些除了传参不同外 并没有什么区别 这个时候就可以利用ddt来管理测试数据 提高代码复用率 二 dd
  • 2017今日头条网招在线编程题(部分)

    第一题 P 为 给 定 的 二 维 平 面 整 数 点 集 定 义 P 中 某 点 如 果 满 足 P 中 任 意 点 都 不 在 的 右 上 方 区 域 内 横 纵 坐标 都 大 于 则 称 其 为 最 大 的 求 出 所 有 最 大 的
  • 牛客-困难及极难难度python

    1 字符串最后一个单词的长度 计算字符串最后一个单词的长度 单词以空格隔开 字符串长度小于5000 def get length input str input str list input str strip split if len s
  • 高并发内存池项目(concurrent memory pool)

    一 高并内存池概念 内存池 Memory Pool 是一种动态内存分配与管理技术 通常情况下 程序员习惯直接使用 new delete malloc free 等API申请分配和释放内存 这样导致的后果是 当程序长时间运行时 由于所申请内存