C++高并发内存池的设计和实现

一、整体设计

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、申请内存流程图

 相关视频推荐

200行代码实现slab,开启内存池的内存管理(准备linux环境)

池式组件为性能飙升提供技术保障-线程池,内存池,异步请求池,数据库连接池

​​​​​​内存泄漏的3个解决方案与原理实现,知道一个可以轻松应用开发工作

免费学习地址:c/c++ linux服务器开发/后台架构师

需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,内核,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)

 

二、详细设计

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个哈希映射。
  • 哈希映射的结构如下:

 如何保证每个线程独有?

大于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;
}

结果比较

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/358.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

卷王都在偷偷准备金三银四了...

年终奖没发&#xff1b; 简历石沉大海&#xff1b; 发消息只读不回 打开某招聘&#xff0c;看了看岗位&#xff0c;这个厂还不错&#xff0c;可是要求好高&#xff0c;我啥都不会。 “哎&#xff0c;算了&#xff0c;我简历还没更新呢&#xff0c;我躺到6月份拿到年终奖再跑…

3-1 SpringCloud快速开发入门: Ribbon 是什么

接上一章节Eureka 服务注册中心自我保护机制&#xff0c;这里讲讲Ribbon 是什么 Ribbon 是什么 通常说的负载均衡是指将一个请求均匀地分摊到不同的节点单元上执行&#xff0c;负载均和分为硬件负载均衡和软件负载均衡&#xff1a; **硬件负载均衡&#xff1a;**比如 F5、深信…

记第一次面试的过程(C++)

说实话三月份上旬过得很充实&#xff0c;而且感觉蛮值&#xff0c;但还有不足的地方&#xff0c;今晚特地看完资料分析来复盘复盘。 时间还要回到3.2中午13.35&#xff08;别问我为什么那么准确&#xff0c;刚刚掏手机看的&#xff09;&#xff0c;我正在吃着饭看着王者荣耀的直…

STL sort 分析

前言 STL 中提供了很多算法&#xff0c;sort 是我们经常使用的&#xff0c;那它究竟是如何实现的呢&#xff1f; STL 的 sort 算法&#xff0c;数据量大时采用快速排序&#xff0c;分段递归。一旦分段的数据量小于某个门槛&#xff0c;为避免快速排序的递归调用带来过大的额外…

三天吃透计算机网络面试八股文

本文已经收录到Github仓库&#xff0c;该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点&#xff0c;欢迎star~ Github地址&#xff1a;https://github.com/…

Linux常用命令

个人简介&#xff1a;云计算网络运维专业人员&#xff0c;了解运维知识&#xff0c;掌握TCP/IP协议&#xff0c;每天分享网络运维知识与技能。座右铭&#xff1a;海不辞水&#xff0c;故能成其大&#xff1b;山不辞石&#xff0c;故能成其高。个人主页&#xff1a;小李会科技的…

C++STL 容器案例 员工分组 实现步骤与代码分析与展示 实现步骤的注意事项

STL容器 员工分组案例 文章目录STL容器 员工分组案例1 案例描述2 实现步骤3 案例代码与分析1 案例描述 公司今天招聘了10个员工&#xff08;ABCDEFGHIJ&#xff09;&#xff0c;10名员工进入公司之后&#xff0c;需要指派员工在哪个部门工作员工信息有: 姓名 工资组成&#xf…

CANoe中使用CAPL刷写流程详解(Trace图解)(CAN总线)

&#x1f345; 我是蚂蚁小兵&#xff0c;专注于车载诊断领域&#xff0c;尤其擅长于对CANoe工具的使用&#x1f345; 寻找组织 &#xff0c;答疑解惑&#xff0c;摸鱼聊天&#xff0c;博客源码&#xff0c;点击加入&#x1f449;【相亲相爱一家人】&#x1f345; 玩转CANoe&…

史上最全最详细的Java架构师成长路径图,程序员必备

从新手码农到高级架构师&#xff0c;要经过几步&#xff1f;要多努力&#xff0c;才能成为为人倚重的技术专家&#xff1f;本文将为你带来一张程序员发展路径图&#xff0c;但你需要知道的是&#xff0c;天下没有普适的道理&#xff0c;具体问题还需具体分析&#xff0c;实践才…

Verilog实现组合逻辑电路

在verilog 中可以实现的数字电路主要分为两类----组合逻辑电路和时序逻辑电路。组合逻辑电路比较简单&#xff0c;仅由基本逻辑门组成---如与门、或门和非门等。当电路的输入发生变化时&#xff0c;输出几乎&#xff08;信号在电路中传递时会有一小段延迟&#xff09;立即就发生…

马上要面试了,还有八股文没理解?让ChatGPT来给你讲讲吧——如何更好使用ChatGPT?

最近这段时间 ChatGPT 掀起了一阵 AI 热潮&#xff0c;目前来看网上大部分内容都是在调戏 AI&#xff0c;很少有人写如何用 ChatGPT 做正事儿。 作为一个大部分知识都是从搜索引擎和 GitHub 学来的程序员&#xff0c;第一次和 ChatGPT 促膝长谈后&#xff0c;基本认定了一个事…

AI又进化了,突破性革命来了

大家好&#xff0c;我是 Jack。 2023 年&#xff0c;AI 真的杀疯了。短短不到一年的时间&#xff0c;当我们还在感慨 AI 一键生成的二次元画作精美万分的时候&#xff0c;它已经进化到了写实美照也能手到擒来的地步。 更多的效果&#xff0c;可以看刚刚发布的视频&#xff0c;…

爽,我终于掌握了selenium图片滑块验证码

因为种种原因没能实现愿景的目标&#xff0c;在这里记录一下中间结果&#xff0c;也算是一个收场吧。这篇文章主要是用selenium解决滑块验证码的个别案列。 思路&#xff1a; 用selenium打开浏览器指定网站 将残缺块图片和背景图片下载到本地 对比两张图片的相似地方&#x…

十大经典排序算法(上)

目录 1.1冒泡排序 1. 算法步骤 3.什么时候最快 4. 什么时候最慢 5.代码实现 1.2选择排序 1. 算法步骤 2. 动图演示 3.代码实现 1.3 插入排序 1. 算法步骤 2. 动图演示 3. 算法实现 1.4 希尔排序 1. 算法步骤 2. 动图演示 3.代码实现 1.5 归并排序 1. 算法步骤 2…

2023年中国高校计算机大赛-团队程序设计天梯赛(GPLT)上海理工大学校内选拔赛(同步赛) A — E

2023年中国高校计算机大赛-团队程序设计天梯赛&#xff08;GPLT&#xff09;上海理工大学校内选拔赛&#xff08;同步赛) 文章目录A -- A Xor B Problem题目分析codeB -- 吃苹果题目分析codeC -- n皇后问题题目分析codeD -- 分苹果题目分析codeE -- 完型填空题目分析codeA – A…

图像缩放对相机内外参矩阵的影响

参考资料&#xff1a;https://zhuanlan.zhihu.com/p/87185139 一、3D空间中点到图像的投影 设3D空间中的点(x,y,z)(x,y,z)(x,y,z)投影到图像上的像素坐标&#xff08;连续值&#xff0c;以左上角像素的左上角为原点的坐标系&#xff0c;注意与整数值的图像像素索引相区别&…

HTTPS的加密原理(工作机制)

现在很多网站使用的都是HTTPS协议,比如CSDN他们为什么要使用HTTPS协议而不是继续使用HTTP协议呢?以及HTTPS都做了些什么?HTTP协议与HTTPS有哪些区别? 下面我来 讲解这些问题?(篇幅可能有些长,请求耐心观看,我以0基础的角度去讲解这些东西, 如果你有一定的基础前面的跳过就好…

docker安装elasticsearch与head教程完整版—.NET Core Web Api与elasticsearch打造全站全文搜索引擎

默认已经有docker环境 下载与安装 elasticsearch &#xff0c;从hub.docker里面可以看到最新版本的镜像&#xff0c;选择你想要的版本 本教程是以 7.17.7 为案例&#xff0c;为啥不适用最新的&#xff0c;首先个人一般需用最新的版本&#xff0c;如果有亢很难填&#xff0c;其次…

三体到底是啥?用Python跑一遍就明白了

文章目录拉格朗日方程推导方程组微分方程算法化求解画图动图绘制温馨提示&#xff0c;只想看图的画直接跳到最后一节拉格朗日方程 此前所做的一切三体和太阳系的动画&#xff0c;都是基于牛顿力学的&#xff0c;而且直接对微分进行差分化&#xff0c;从而精度非常感人&#xf…

如何用Python求解微分方程组

文章目录odeint简介示例odeint简介 scipy文档中将odeint函数和ode, comples_ode这两个类称为旧API&#xff0c;是scipy早期使用的微分方程求解器&#xff0c;但由于是Fortran实现的&#xff0c;尽管使用起来并不方便&#xff0c;但速度没得说&#xff0c;所以有的时候还挺推荐…