[项目设计] 从零实现的高并发内存池(四)

🌈 博客个人主页Chris在Coding

🎥 本文所属专栏[高并发内存池]

❤️ 前置学习专栏[Linux学习]

 我们仍在旅途                     

目录

6.内存回收

        6.1 ThreadCache回收内存

        6.2 CentralCache回收内存

        ReleaseListToSpans

        MapObjToSpan

         6.3 PageCache回收内存

        ReleaseSpanToPageCache

回收内存测试

7.解决大内存申请释放

        7.1 申请内存

        RoundUp

        ConcurrentAlloc

        NewSpan

        7.2 释放内存

        ConcurrentFree

        ReleaseSpanToPageCache

大内存测试


6.内存回收

        6.1 ThreadCache回收内存

这是之前我们deallocate的代码:

//void ThreadCache::deallocate(void* ptr, size_t size)
//{
//	assert(ptr);
//	assert(size <= MAXSIZE);
//	// 找对映射的空闲链表桶,对象插入进入
//	size_t index = SizeTable::Index(size);
//	_freelists[index].push(ptr);
//}

这样可能潜在着这样的问题 : 随着线程不断的释放,对应自由链表的长度也会越来越长,这些内存堆积在一个ThreadCache中如果不去使用那么其实就是一种浪费,因此我们应该将这些内存还给CentralCache。这样一来,这些内存对其他线程来说也是可申请的,因此当ThreadCache某个桶当中的自由链表太长时我们应该做更多处理。

 于是我们在原来的基础上加入下面的判断:

inline void _FreeListTooLong(FreeList& list, size_t size)
{
	//我们从_FreeList中抽回MaxSize,返还给CentralCache
	void* start = nullptr;
	list.pop_range(start,list.MaxSize());
	CentralCache::GetInstance().ReleaseListToSpans(start, size);
}
void ThreadCache::deallocate(void* ptr, size_t size)
{
	assert(ptr);
	assert(size <= MAXSIZE);
	// 找对映射的自由链表桶,对象插入进入
	size_t index = SizeTable::Index(size);
	_freelists[index].push(ptr);
	if (_freelists[index].size() > _freelists[index].MaxSize())
	{
		//_FreeList过长了
		_FreeListTooLong(_freelists[index],size);
	}
}

这里我们选择认为,当该空闲链表的内存块数量大于该链表可以向CentralCache申请的数量时,我们就执行_FreeListTooLong的逻辑归还

因为空闲链ReleaseListToSpans表用到内存块数量,这里我们就得对之前的FreeList类进行一些改造以适应需求

class FreeList
{
public:
	void push(void* obj) 
	{
		// ...
		++_size;
	}
	void push_range(void* start, void* end,size_t n)
	{
		// ...
		_size += n;
	}
	void* pop() //头删
	{
		// ...
		--_size;
        return obj;
	}
    // ...
	size_t size()
	{
		return _size;
	}
	void pop_range(void*& start,size_t n)
	{
		assert(n <= _size);
		start = _FreeList;
		void* end = start;
		for (int i = 0; i < n - 1; i++)
		{
			end = *(void**)end;
		}
		_FreeList = *(void**)end;
		*(void**)end = nullptr;
		_size-=n;
	}
private:
	// ...
	size_t _size = 0;
};

因为这里我们改动了push_range接口,那么当时ThreadCache从CentralCache获取了actual_num个对象,剩下的actual_num-1个挂到了ThreadCache对应的桶当中的代码也会随之修改

_freelists[index].push_range(*(void**)start, end,actual_num-1);

        6.2 CentralCache回收内存

ReleaseListToSpans

void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
	//先找到派发出_FreeList的span类
	//该过程中会改动span类需要加锁
	size_t index = SizeTable::Index(size);
	_spanlists[index]._mtx.lock();
	while (start)
	{
		void* next = *(void**)start;
		Span* span=PageCache::GetInstance().MapObjToSpan(start);
		*(void**)start = span->_freeList;
		span->_freeList = start;
		span->_used--;
		if (span->_used == 0)
		{
			_spanlists->erase(span);
			span->_next = nullptr;
			span->_prv = nullptr;
			span->_freeList = nullptr;
			_spanlists[index]._mtx.unlock();

			PageCache::GetInstance()._pagemtx.lock();
			PageCache::GetInstance().ReleaseSpanToPageCache(span);
			PageCache::GetInstance()._pagemtx.unlock();

			_spanlists[index]._mtx.lock();
		}
		start = next;
	}
	_spanlists[index]._mtx.unlock();
}
  • SizeTable::Index(size):根据对象大小确定在大小表中的索引,用于找到对应的 span 列表。
  • 锁操作:在对 span 列表进行操作时,先对其加锁 _mtx,以确保线程安全。
  • 遍历释放对象:通过循环遍历释放对象,首先获取下一个对象的指针 next,然后根据对象的地址找到其对应的 span,将当前对象插入到 span 的空闲列表 _freeList 中,并更新 span 的使用计数 _used。
  • 判断 span 是否完全空闲:如果 span 的使用计数 _used 减为 0,表示这个 span 分配出去的对象全部都已经回收回来了。在这种情况下,需要将该 span 从 span 列表 _spanlists[index] 中移除,并将其自身的指针 _next、_prev、_freeList 置空。
  • 回收 span:一旦确定要回收 span,就需要将其释放回页缓存(PageCache)中。这一步涉及到了两个锁的操作,首先释放 span 需要持有 _pagemtx 锁,以确保对页缓存的操作线程安全;同时,也需要确保释放 span 的过程是原子的,因此在释放前后都需要释放掉 _spanlists[index]._mtx 锁。

MapObjToSpan

通过内存块地址怎么找到原先的Sapn

在实现ReleaseListToSpans时,我们是默认通过MapObjToSpan()实现从内存块地址找到Span。

首先我们必须理解的是,某个页当中的所有地址除以页的大小都等该页的页号。比如我们这里假设一页的大小是100,那么地址0~99都属于第0页,它们除以100都等于0,而地址100~199都属于第1页,它们除以100都等于1。

虽然我们现在可以通过对象的地址得到其所在的页号,但是我们还是不能知道这个对象到底属于哪一个span。因为一个span管理的可能是多个页。为了解决这个问题,我们可以建立页号和span之间的映射。这里可以用C++当中的unordered_map进行实现,并且添加一个函数接口,用于让central cache获取这里的映射关系。

class PageCache
{
public:
    // ...
	Span* MapObjToSpan(void* obj);
private:
    // ...
	std::unordered_map<PAGEID,Span*> _IdToSpan;
};
Span* PageCache::MapObjToSpan(void* obj)
{
	PAGEID id = ((PAGEID)obj >> PAGESHIFT);

	std::unique_lock<std::mutex> lock(_pagemtx);

	auto ret = _IdToSpan.find(id);
	if (ret != _IdToSpan.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}

 建立映射关系

在 PageCache 类中,当分配 Span 给 CentralCache 时,需要记录下分配的 span 的页号和 span 之间的映射关系。因此,在 NewSpan 函数中,分配完 Span 后,我们会遍历该 Span 的所有页,将每个页号与该 Span 建立映射关系。

Span* PageCache::NewSpan(size_t k)
{
	if (!_Spanlists[k].empty())
	{
		Span* kspan = _Spanlists[k].pop_front();
		for (PAGEID i = 0; i < kspan->_n; i++)
		{
			_IdToSpan[kspan->_pageId + i] = kspan;
		}
		return kspan;
	}
	for (size_t n = k + 1; n < NPAGES; n++)
	{

		if (!_Spanlists[n].empty())
		{
			Span* nspan = _Spanlists[n].pop_front();
			Span* kspan = new Span;

			kspan->_pageId = nspan->_pageId;
			kspan->_n = k;

			nspan->_pageId += k;
			nspan->_n -= k;

			_Spanlists[nspan->_n].push_front(nspan);
			for (PAGEID i = 0; i < kspan->_n; i++)
			{
				_IdToSpan[kspan->_pageId + i] = kspan;
			}
			return kspan;
		}
	}
	Span* maxspan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
	maxspan->_pageId = (PAGEID)ptr >> PAGESHIFT;
	maxspan->_n = NPAGES - 1;
	_Spanlists[maxspan->_n].push_front(maxspan);
	return NewSpan(k);
}

         6.3 PageCache回收内存

Page Cache 回收内存的过程是为了释放已经被 Central Cache 使用过的 Span,并尝试将这些 Span 与其他空闲的 Span 进行合并,从而缓解内存碎片问题。

这个过程涉及以下几个关键步骤:

  • 判断 Span 是否可以合并: 首先,需要判断回收的 Span 周围是否有空闲的 Span 可以合并。这包括向前合并和向后合并两种情况。如果回收的 Span 的前后页号对应的 Span 存在且未被使用,则可以进行合并。

  • 向前合并和向后合并: 如果条件允许,就可以进行向前和向后的合并操作。向前合并就是将回收的 Span 与前一个未使用的 Span 进行合并,向后合并则是将回收的 Span 与后一个未使用的 Span 进行合并。合并的过程包括更新 Span 的起始页号和页数,并从 Page Cache 的对应双链表中移除被合并的 Span。

  • 建立映射关系: 在合并结束后,需要重新建立 Span 的页号与 Span 之间的映射关系,以便后续操作能够快速定位到对应的 Span。这包括更新合并后 Span 的首尾页号与 Span 之间的映射关系,并将合并后的 Span 挂到对应的双链表中。

  • 更新 Span 的状态: 最后,需要将合并后的 Span 标记为未被使用的状态,以便之后的分配过程能够正确识别并使用这些 Span。

通过以上步骤,Page Cache 在回收内存时不仅释放了被使用过的 Span,还尝试将空闲的 Span 进行合并,以优化内存的利用效率,并且通过建立映射关系,确保后续操作能够快速定位到对应的 Span,提高了内存回收的效率和性能。

如何判断Span是否正在使用

由于PageCache回收内存的步骤中涉及到了Span内存块的合并,在这一过程中判断Span内存块是否正在使用将很重要。但是我们不能通过Span结构当中的_used成员,来判断某个Span到底是在CentralCache还是在PageCache。因为当CentralCache刚向PageCache申请到一个Span时,这个span的_used就是等于0的,这时可能当我们正在对该Span进行切分的时候,PageCache就把这个Span拿去进行合并了。

为了避免这些问题,我们直接在最初就给Span类中新增一个成员变量用来标记这个Span内存块是否正在使用的过程中。

struct Span
{
    // ...
	bool _isused = false;
};

因此当central cache向page cache申请到一个span时,需要立即将该span的_isUse改为true。

span->_isused = true;

而当central cache将某个span还给page cache时,也就需要将该span的_isUse改成false。

span->_isused = false;

ReleaseSpanToPageCache

void PageCache::ReleaseSpanToPageCache(Span* span)
{
	//先判断销毁的span类型是不是大于能维护的最大值
	if (span->_n>NPAGES-1)
	{
		//直接调用系统接口销毁
		void* ptr = (void*)(span->_pageId<<PAGESHIFT);
		SystemFree(ptr);
		_Spanpool.Delete(span);
		return;
	}
	// 对span前后的页,尝试进行合并,缓解内存碎片问题
	while (1)
	{
		PAGEID prvid = span->_pageId - 1;
		Span* prvit = (Span*)_IdToSpan.get(prvid);
		if (prvit == nullptr|| prvit->_isused == true||(prvit->_n+span->_n)>NPAGES-1) //前页不在或在使用或合成后超过最大值
		{
			break;
		}
		span->_pageId = prvit->_pageId;
		span->_n += prvit->_n;
		_Spanlists[prvit->_n].erase(prvit);
		_Spanpool.Delete(prvit);
	}
	while (1)
	{
		PAGEID nxtid = span->_pageId +span->_n;
		Span* nxtit = (Span*)_IdToSpan.get(nxtid);
		if (nxtit == nullptr || nxtit->_isused == true || (nxtit->_n + span->_n) > NPAGES - 1) //后页不在或在使用或合成后超过最大值
		{
			break;
		}
		span->_pageId = nxtit->_pageId;
		span->_n += nxtit->_n;
		_Spanlists[nxtit->_n].erase(nxtit);
		_Spanpool.Delete(nxtit);
	}
	_Spanlists[span->_n].push_front(span);
	span->_isused = false;
	//此时从span->_pageId到span->_pageId + span->_n - 1页的所有Id在_IdToSpan的映射关系还没有更新,但没必要现在更新,在newspan时,会覆盖原来的更新
	//现在更新头尾是防止出现合成后再被别的span合成的情况(兼容我们的前后页合并接口)
	_IdToSpan.set(span->_pageId,span);
	_IdToSpan.set(span->_pageId + span->_n - 1, span);
}
  1. 处理大于最大值的 Span: 首先,函数检查传入的 Span 的页数是否大于 NPAGES - 1,如果是的话,说明这个 Span 超过了系统能维护的最大值,那么就直接调用系统接口销毁内存,并释放 Span 对象的内存,然后返回。这个操作是为了防止系统无法管理过大的 Span 而造成的问题。

  2. 合并相邻的 Span: 接下来,函数尝试对传入的 Span 的前后页进行合并操作,从而缓解内存碎片问题。首先,通过循环向前查找前一个 Span 是否可以合并,如果找到了合适的 Span,就进行合并操作,并在合并后将该 Span 从对应的 SpanList 中删除,并释放其内存。然后,再通过循环向后查找后一个 Span 是否可以合并,同样找到合适的 Span 后进行合并操作,并释放其内存。

  3. 更新数据结构: 在合并结束后,将合并后的 Span 插入到对应的 SpanList 中,并将其标记为未使用。然后,更新 Span 的页号与 Span 之间的映射关系,以便后续操作能够快速定位到对应的 Span。

在合并 page cache 中的 span 时,需要通过页号找到对应的 span。与 central cache 不同的是,page cache 中的 span 需要建立页号与 span 之间的映射关系。在 page cache 中,只需建立一个 span 的首尾页号与该 span 之间的映射关系即可。因为在进行合并时,只需要通过一个 span 的尾页或首页找到这个 span,无需建立每个页与 span 的映射关系。

当申请 k 页的 span 时,如果将 n 页的 span 切割为一个 k 页的 span 和一个 n-k 页的 span,除了需要建立 k 页 span 中每个页与该 span 之间的映射关系外,还需要建立剩余的 n-k 页 span 的首尾页号与其间的映射关系。

Span* PageCache::NewSpan(size_t k)
{
	// ...
	for (size_t n = k + 1; n < NPAGES; n++)
	{

		if (!_Spanlists[n].empty())
		{
			// ...
		   /*nspan->_pageId += k;
			nspan->_n -= k;*/


			// 存储nSpan的首位页号跟nSpan映射,方便page cache回收内存时
			// 进行的合并查找
			_IdToSpan[nspan->_pageId] = nspan;
			_IdToSpan[nspan->_pageId + nspan->_n - 1] = nspan;

			/*_Spanlists[nspan->_n].push_front(nspan);
			for (PAGEID i = 0; i < kspan->_n; i++)
			{
				_IdToSpan[kspan->_pageId + i] = kspan;
			}
			return kspan;*/
		}
	}
	// ...
}

回收内存测试

void MultiThreadAlloc1()
{
	std::vector<void*> v;
	for (size_t i = 0; i < 1000; ++i)
	{
		void* ptr = ConcurrentAlloc(6);
		v.push_back(ptr);
	}

	for (auto e : v)
	{
		ConcurrentFree(e, 6);
	}
}
void MultiThreadAlloc2()
{
	std::vector<void*> v;
	for (size_t i = 0; i < 1000; ++i)
	{
		void* ptr = ConcurrentAlloc(16);
		v.push_back(ptr);
	}

	for (auto e : v)
	{
		ConcurrentFree(e, 16);
	}
}
void MultiThreadAlloc3()
{
	std::vector<void*> v;
	for (size_t i = 0; i < 1000; ++i)
	{
		void* ptr = ConcurrentAlloc(256);
		v.push_back(ptr);
	}

	for (auto e : v)
	{
		ConcurrentFree(e, 256);
	}
}
void MultiThreadAlloc4()
{
	std::vector<void*> v;
	for (size_t i = 0; i < 1000; ++i)
	{
		void* ptr = ConcurrentAlloc(2048);
		v.push_back(ptr);
	}

	for (auto e : v)
	{
		ConcurrentFree(e, 2048);
	}
}
void MultiThreadAlloc5()
{
	std::vector<void*> v;
	for (size_t i = 0; i < 1000; ++i)
	{
		void* ptr = ConcurrentAlloc(66*1024);
		v.push_back(ptr);
	}

	for (auto e : v)
	{
		ConcurrentFree(e, 66 * 1024);
	}
}
void TestMultiThread()
{
	std::thread t1(MultiThreadAlloc1);
	std::thread t2(MultiThreadAlloc2);
	std::thread t3(MultiThreadAlloc3);
	std::thread t4(MultiThreadAlloc4);
	std::thread t5(MultiThreadAlloc5);
	t1.join();
	t2.join();
	t3.join();
	t4.join();
	t5.join();
}
int main()
{
	//TLSTest();
	//TestAlloc();
	TestMultiThread();
	return 0;
}

这里我们简单的跑一下程序,看一下是否能稳定运行 。

7.解决大内存申请释放

        7.1 申请内存

这里我们用梳理一下之前的内存申请方式, ThreadCache是用于申请小于等于256KB的内存的,而对于大于256KB的内存,我们可以考虑直接向PageCache申请,但PageCache中最大的页也就只有128页,所以我们现在还无法实现大于128页内存的申请释放 。于是我们现在选择把大于128页的内存直接向堆上申请释放来解决这个问题。

当申请的内存大于256KB时,虽然不是从ThreadCache进行获取,但在分配内存时也是需要按页为单位向上对齐的。

RoundUp

class SizeTable
{
public:
	static size_t RoundUp(size_t bytes)
	{
		// ...
		else if (bytes <= 256 * 1024)
		{
			// return _RoundUp(bytes, 8 * 1024);
		}
		else
		{
			return _RoundUp(bytes, 1 << PAGESHIFT);
		}
};

ConcurrentAlloc

我们最初申请内存时的逻辑也会随之改变,我们就不选择走向ThreadCache申请内存的流程,直接判断完后调用Newspan接口申请页。

static void* ConcurrentAlloc(size_t size)
{
	if (size > MAXSIZE)
	{
		//超过256KB的内存,ThreadCache中已经放不下了,直接申请Span来用
		size_t align_size = SizeTable::RoundUp(size);
		size_t kpage = align_size >> PAGESHIFT; //计算一共要去PageCache中去拿k页Span

		PageCache::GetInstance()._pagemtx.lock();
		Span* span = PageCache::GetInstance().NewSpan(kpage);
		PageCache::GetInstance()._pagemtx.unlock();

		return (void*)(span->_pageId << PAGESHIFT);
	}
	// ...
}

NewSpan

我们在这里实现超过128页内存的申请,并且对申请的span类建立头指针的映射关系,这样便于后续销毁工作。

Span* PageCache::NewSpan(size_t k)
{
	if (k > NPAGES - 1)
	{
		//此时申请的内存大于了1024*1024 bytes ,即1M ,超过PageCache维护的最大限制,此时我们直接去堆上申请内存
		void* ptr = SystemAlloc(k);
		Span* kspan = new Span;
		kspan->_n = k;
		kspan->_pageId = (PAGEID)ptr >> PAGESHIFT;
		//对申请的span类建立头指针的映射关系,便于后续销毁工作
		_IdToSpan[kspan->_pageId] = kspan;
		return kspan;
	}
	// ...
}

        7.2 释放内存

我们释放的流程总结如下:

释放内存的大小释放方式
x ≤ 256KB (32页)释放给 thread cache
32页 < x ≤ 128页释放给 page cache
x ≥ 128页释放给堆

ConcurrentFree

因此当释放对象时,我们需要先找到该对象对应的span,但是在释放对象时我们只知道该对象的起始地址。但是我们是给申请到的内存建立了span结构,并建立起始页号与该span之间的映射关系的原因。此时我们就可以通过释放对象的起始地址计算出起始页号,进而通过页号找到该对象对应的span。

static void ConcurrentFree(void* ptr, size_t size)
{
	if (size > MAXSIZE)
	{
		Span* span = PageCache::GetInstance().MapObjToSpan(ptr);
		//超过256KB的内存,ThreadCache中无法维护,都是以Span类型储存并维护
		PageCache::GetInstance()._pagemtx.lock();
		PageCache::GetInstance().ReleaseSpanToPageCache(span);
		PageCache::GetInstance()._pagemtx.unlock();
	}
	else
	{
		assert(pTLSThreadCache);
		pTLSThreadCache->deallocate(ptr, size);
	}
}

ReleaseSpanToPageCache

因此Page Cache在回收Span时也需要进行判断,如果该span的大小是大于128页的,那么说明该span是直接向堆申请的,我们直接将这块内存释放给堆,然后将这个span结构进行delete就行了。

void PageCache::ReleaseSpanToPageCache(Span* span)
{
	if (span->_n > NPAGES - 1)
	{
		void* ptr = (void*)(span->_pageId << PAGESHIFT);
		SystemFree(ptr);
		delete span;
		return;
	}
    // ...
}

大内存测试

void BigAlloc()
{
	void* p1 = ConcurrentAlloc(257 * 1024);
	ConcurrentFree(p1, 257 * 1024);

	void* p2 = ConcurrentAlloc(129 * 8 * 1024);
	ConcurrentFree(p2, 129 * 8 * 1024);
}
int main()
{
	//TLSTest();
	//TestAlloc();
	//TestMultiThread();
	BigAlloc();
	return 0;
}

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

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

相关文章

STM32CubeMX学习笔记11 ---RTC实时时钟

1、RTC实时时钟简介 STM32的实时时钟RTC是一个独立的定时器&#xff0c;RTC模块拥有一组连续计数的计数器&#xff0c;在相应软件配置下&#xff0c;可提供时钟日历的功能&#xff0c;修改计数器的值可以重新设置系统当前的时间和日期 RTC模块和时钟配置系统&#xff08;RCC_B…

100 spring-security 中 /oauth/token 发送请求不携带参数 报错 “401 Unauthorized“

前言 最近存在这样的一个问题, 大致的复现方式是 访问 /oauth/token 接口, 然后不携带任何参数, 结果 服务器抛出了一个 "401 Unauthorized" 针对这个 401, 这里 梳理一下这个流程, 也会衍生出一些其他的问题 测试用例 客户端这边大致的情况是 构造参数, 然后发…

Linux 之三:CentOS7 目录结构 和 日期及时区设置

Linux 目录 以下是对这些目录的解释&#xff1a; /bin&#xff1a;bin是Binary的缩写, 这个目录存放着最经常使用的命令。/boot&#xff1a; 这里存放的是启动Linux时使用的一些核心文件&#xff0c;包括一些连接文件以及镜像文件。/dev &#xff1a; dev是Device(设备)的缩写…

[Java安全入门]二.序列化与反序列化

一.概念 Serialization&#xff08;序列化&#xff09;是一种将对象以一连串的字节描述的过程&#xff1b;反序列化deserialization是一种将这些字节重建成一个对象的过程。将程序中的对象&#xff0c;放入文件中保存就是序列化&#xff0c;将文件中的字节码重新转成对象就是反…

【广度优先搜索】【堆】【C++算法】407. 接雨水 II

作者推荐 【二分查找】【C算法】378. 有序矩阵中第 K 小的元素 本文涉及知识点 广度优先搜索 堆 LeetCoce407. 接雨水 II 给你一个 m x n 的矩阵&#xff0c;其中的值均为非负整数&#xff0c;代表二维高度图每个单元的高度&#xff0c;请计算图中形状最多能接多少体积的雨…

【C语言】还有柔性数组?

前言 也许你从来没有听说过柔性数组&#xff08;flexible array&#xff09;这个概念&#xff0c;但是它确实是存在的。C99中&#xff0c;结构中的最后⼀个元素允许是未知⼤⼩的数组&#xff0c;这就叫做『柔性数组』成员。 欢迎关注个人主页&#xff1a;逸狼 创造不易&#xf…

二级水平导航菜单栏的实现

1. 这个是本人设计的一带一路的二级水平导航栏HTML代码&#xff1b; 这里最后实现的效果是鼠标悬停在导航栏上面&#xff0c;就会显示下面的4个部分页面&#xff0c;这里只是以评论热 点作为例子&#xff0c;其他的类似&#xff1b; 2.首先要设计DIV&#xff0c;然后利用无…

GO语言环境安装---VScode.2024

目录 一、下载并安装GO 二、配置环境变量 三、VScode环境安装 由于工作原因&#xff0c;需要用到go来写web后端&#xff0c;正好从零记录下环境安装 一、下载并安装GO 首先在官网根据PC系统选择对应的包下载 源地址&#xff1a;https://go.dev/dl/ 打不开的用这个也行&a…

论文阅读:Dataset Quantization

摘要 最先进的深度神经网络使用大量&#xff08;百万甚至数十亿&#xff09;数据进行训练。昂贵的计算和内存成本使得在有限的硬件资源上训练它们变得困难&#xff0c;特别是对于最近流行的大型语言模型 (LLM) 和计算机视觉模型 (CV)。因此最近流行的数据集蒸馏方法得到发展&a…

第三天 Kubernetes进阶实践

第三天 Kubernetes进阶实践 本章介绍Kubernetes的进阶内容&#xff0c;包含Kubernetes集群调度、CNI插件、认证授权安全体系、分布式存储的对接、Helm的使用等&#xff0c;让学员可以更加深入的学习Kubernetes的核心内容。 ETCD数据的访问 kube-scheduler调度策略实践 预选与…

Python内置模块

目录 什么是模块 模块分类 通过模块创建者分类 系统内置模块 第三方模块 在线安装 离线安装 模块导入 math和random模块介绍 math模块 random模块 什么是模块 在我们编写程序时&#xff0c;需要导入包。例如随机数的产生&#xff0c;需要import random。import XXX&…

【MATLAB】兔子机器人总系统_动力学模型解读(及simulink中的simscape的各模块介绍)

1、动力学模型 Rectangular Joint 控制平面上&#xff08;x&#xff0c;y轴&#xff09;的移动&#xff0c;去掉以后&#xff0c;机器人在原地翻滚不移动 Rigid Transform 坐标转换&#xff0c;B站视频已收藏 去掉&#xff0c;机体与地面贴合 此处的作用是设定机体的初…

【conda】实现conda环境迁移的4种方式

文章目录 方案1: 使用conda pack制作压缩包并在目标环境解压使用方案2: 使用package列表文件重新创建conda环境方案3: scp将环境文件夹拷贝到目标主机上方案4: 通过--clone先克隆一个环境再conda pack打包迁移 方案1: 使用conda pack制作压缩包并在目标环境解压使用 适合离线环…

不买后悔的阿里云服务器租用价格表_优惠活动整理_2024新版

2024阿里云服务器优惠活动政策整理&#xff0c;阿里云99计划ECS云服务器2核2G3M带宽99元一年、2核4G5M优惠价格199元一年&#xff0c;轻量应用服务器2核2G3M服务器61元一年、2核4G4M带宽165元1年&#xff0c;云服务器4核16G10M带宽26元1个月、149元半年&#xff0c;云服务器8核…

Python基于opencv的人脸识别上课签到考勤系统,附源码

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

JS 实现AES方式加密数据实现示例

简介&#xff1a;全称高级加密标准&#xff08;英文名称&#xff1a;Advanced Encryption Standard&#xff09;&#xff0c;在密码学中又称 Rijndael 加密法&#xff0c;由美国国家标准与技术研究院 &#xff08;NIST&#xff09;于 2001 年发布&#xff0c;并在 2002 年成为有…

最近开发中遇到的一些问题

puppeteer下载失败问题 使用的淘宝镜像&#xff0c;但执行命令npm i puppeteer之后&#xff0c;报错&#xff1a; npm ERR! code 1 npm ERR! path E:\项目-临时\test_install_puppeteer\node_modules\puppeteer npm ERR! command failed npm ERR! command C:\WINDOWS\system3…

00X集——CAD vba 填充(hatch)及挖空

首先&#xff0c;画个椭圆&#xff0c;并填充&#xff0c;直接上代码&#xff1a; Sub 画椭圆填充() 2024年3月6日21:10:22 by qq443440204 Dim hat As AcadHatch 填充 Dim ell(0) As AcadEllipse 椭圆 Dim cent(0 To 2) As Double 椭圆中心点 Dim dd(0 To 2) As Double 椭圆长…

【爬虫】单首音乐的爬取(附源码)

以某狗音乐为例 import requests import re import time import hashlibdef GetResponse(url):# 模拟浏览器headers {User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0}# 发送请求…

python+django+vue房屋租赁系统 8gwmf

房屋租赁系统在设计与实施时&#xff0c;采取了模块性的设计理念&#xff0c;把相似的系统的功能整合到一个模组中&#xff0c;以增强内部的功能&#xff0c;减少各组件之间的联系&#xff0c;从而达到减少相互影响的目的。如房源信息、预约信息、求租信息模块等[12]。 管理员后…
最新文章