【项目日记(八)】内存回收与联调
前言
我们前面实现了三层缓存申请的过程,并完成了三层缓存申请过程的联调!本期我们来介绍三层的缓存的回收机制以及三层整体联调释放的过程。
目录
前言
一、thread cache 回收内存
二、central cache 回收内存
• 如何确定一个对象对应的span
• central cache 的内存回收
三、page cache 回收内存
• page cache 合并页的策略
• page cache 进行合并 span 的实现
四、内存回收过程的整体联调
一、thread cache 回收内存
当某个线程申请的内存对象不用了时,就可以将其还给thread cache,然后thread cache将该对象插入到对应的哈希表桶即可!
随着线程的不断释放,thread cache对应的哈希桶中的内存对象的自由链表会越来越长,这些内存还回来堆积在一个thread cache中也是一种浪费,其他线程的thread cache 也用不到。此时,我们可以考虑将还回来的部分内存还给下一层central cache,这样一来还回去这部分内存其他线程也就可以申请了。
我们这里采用的释放策略是:如果thread cache 某个桶中的自由链表的长度超过他一次批量向central cache申请的个数,那么我们就取thread cache对应桶中的批量数的内存对象,还给central cache。举个例子:比如当前线程thread cache的0号桶中的对象数是10,一次批量向central cache获取的数是8,那就将0号桶中的8个内存对象拿出来还给central cache。
// 释放内存
void ThreadCache::Deallocate(void* ptr, size_t size)
{assert(ptr);assert(size <= MAX_BYTES);// 找到当前对象大小 对应的,哈希表中映射的 自由链表,然后插入size_t index = SizeClass::Index(size);_freelists[index].Push(ptr);// 当前桶中的自由链表的长度超过 一次向 central cache 申请的数量 我们就还回去一个批量if (_freelists[index].Size() > _freelists[index].MaxSize()){ListTooLong(_freelists[index], size);}
}
ListTooLong 的具体做法就是从当前的自由链表中取出一批内存,然后一批内存对象还给central cache 对应哈希桶位置的 span 对象,即插入到span 的自由链表中即可!
// 还给 central cache 一个批量的内存对象
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{// 记录需要被 拿下来 还回去 的那一批内存的 起始和结束位置void* start = nullptr, *end = nullptr;// 从自由链表list 中获取list.PopRange(start, end, list.MaxSize());// 将取出来的这一批内存还给 central cache 对应的spanCentralCache::GetInstance()->ReleaseListToSpan(start, end);
}
上面的代码中,我们需要在FreeList 中添加一个字段_size记录当前自由链表的长度,还需要新增一个接口 PopRange用于从自由链表中取出一定数量的内存对象。
// 用于管理切分好的小内存对象的自由链表
class FreeList
{
public:void Push(void* obj){assert(obj);// 头插NextObj(obj) = _freelist;_freelist = obj;_size++;}void* Pop(){assert(_freelist);// 头删void* obj = _freelist;_freelist = NextObj(_freelist);_size--;return obj;}void PushRange(void* start, void* end, size_t n){// 头插NextObj(end) = _freelist;_freelist = start;// 更新_size_size += n;}void PopRange(void*& start, void*& end, size_t n)// 这里使用引用,避免使用二级指针{assert(start);assert(end);// 头删start = _freelist;end = start;for (size_t i = 0; i < n - 1; i++)// 因为end 已经指向第一个了,所以走n-1步{end = NextObj(end);}// 将_freelist指向end 的下一个_freelist = NextObj(end);// 让 end 的下一个置空NextObj(end) = nullptr;// 更新_size_size -= n;}bool Empty(){return _freelist == nullptr;}size_t& MaxSize(){return _maxSize;}size_t Size(){return _size;}private:void* _freelist = nullptr;// 自由链表的指针size_t _maxSize = 1; // 默认从 central cache 获取 内存对象的个数size_t _size = 0;// 用于记录当前自由链表的长度
};
这里需要注意的是,我为了在FreeList中更好地更新_size我将PushRange的接口进行了修改,多加了一个参数n,表示要插入这一段的内存的数量,这个值在外面很好计算,但是在里面就需要遍历了,所以这里选择外部给。
外部在使用PushRange的地方修改传参,即新增插入对象的个数。外部如何计算呢?其实很好计算!thread cache从central cache获取到一定批量的内存后,如果是一个直接返回不会调用PushRange的,所以在申请多个的时候才调用,而申请到多个内存时总数是 actualNum 个,第一个返回, actualNum - 1个插入到thread cache 的自由链表中,所以插入的个数就是actualNum-1个。
注意一下:
我们判断thread cache给central cache归还内存时的条件是:当thread cache 的某个哈希桶中的自由链表太长时,需要归还一定的批量。其实我们还可考虑的更加全面一些:例如,加上每个对象的大小。当某个thread cache的总占用内存超过一定限制时,我们就应该将thread cache中的一部分对象还给central cache,这样就可以做到尽可能让每个thread cache占用的内存最小,这一点tcmalloc人家就是结合上述的两点设计的。
二、central cache 回收内存
当 thread cache 中某个自由链表的长度过长时,会将该自由链表的一部分对象拿下来还给下一层 central cache 对应的span。
但是这里有个问题是,还回来的这一批内存对象有可能不是属于一个span的。因此,我们除了确定他们是在哪一个哈希桶中之外,还需要确定每一个内存对象是属于哪一个span 的。
具体做法是:我们可以根据对象的地址先计算出对应的页号,然后用页号找到对应的span。
• 如何确定一个对象对应的span
1、如何根据对象的地址找到对应的页号?
可以通过当前对象的地址 除以 一页的大小就是对应的 页号。因为在某一页中所有地址除以该页的大小都等于该页的页号。举个例子:假设一页大小是 2000,那么地址就是[0,1999] 它们都除以 2000 都是等于 0 的,即页号就是0
2、如何通过页号找到对应的span?
上面已经知道对象的页号了,但是依然无法确定他是属于哪一个span 的因为一个span有可能管理了多个页。
基于上面,我们可以在page cache 向 central cache 分配若干页的span时,构建每一页的页号与span的映射。因为这个映射关系在page cache合并也需要,因此我们直接定义在 page cache层,这里我么直接使用 unordered_map进行做映射,并需要提供一个接口,用于让central cache获取映射关系。
//单例模式
class PageCache
{
public://获取从对象到span的映射Span* MapObjectToSpan(void* obj);
private:std::unordered_map<PAGE_ID, Span*> _idSpanMap;
};
MapObjectToSpan的实现也是很简单,就是将对象的地址是转为页号,然后根据页号,在哈希表中找到对应的span
// 获取从 对象到span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{// 地址 转为 页号PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT;auto ret = _idSpanMap.find(id);if (ret != _idSpanMap.end()){return ret->second;}else{// 理论上不可能走到这里,走到这里说明我们逻辑有问题 直接断言 错误assert(false);return nullptr;}
}
每当page cache分配给central cache时,都需要记录一下每一页的页号与span的映射关系,后面当thread cache还对象给central cache时,才可以知道是具体还给哪一个span。
因此当central cache在调用NewSpan接口向page cache申请内存时,page cache在返回这个k页span之前,就应该将这k页与对应的span进行建立映射。
Span* PageCache::NewSpan(size_t k)
{assert(k > 0 && k < NPAGES);// 1、page cache 的第 k 号 桶中存在 非空的 k页 span,直接头删拿下来返回if (!_spanlists[k].Empty()){Span* kSpan = _spanlists[k].PopFront();// 建立页号与span的映射,方便central cache回收小块内存时查找对应的spanfor (PAGE_ID i = 0; i < kSpan->_n; i++){_idSpanMap[kSpan->_page_id + i] = kSpan;}return kSpan;}// 2、当前的 第 k 号 桶中 不 存在 非空的 k页 span,到[k+1, NPAGES - 1] 中找for (int i = k + 1; i < NPAGES; i++){if (!_spanlists[i].Empty()){Span* nSpan = _spanlists[i].PopFront();Span* kSpan = new Span;// 将 nSpan 分割成 k 页 和 n - k页kSpan->_page_id = nSpan->_page_id;kSpan->_n = k;nSpan->_page_id += k;nSpan->_n -= k;// 将 n - k 页插入到 第 n - k 个桶中,将 k 页返回_spanlists[nSpan->_n].PushFront(nSpan);// 建立页号与span的映射,方便central cache回收小块内存时查找对应的spanfor (PAGE_ID i = 0; i < kSpan->_n; i++){_idSpanMap[kSpan->_page_id + i] = kSpan;}// 将 kSpan返回即可return kSpan;}}// 3、走到这,说明[k+1,NPAGES-1]个桶中都没有大块内存,此时就需要向 OS 申请 NAGES-1 页的大块内存了Span* bigSpan = new Span;void* ptr = SystemAlloc(NPAGES - 1);bigSpan->_page_id = (PAGE_ID)ptr >> PAGE_SHIFT;// 将起始地址除以每一页的大小,就是页号bigSpan->_n = NPAGES - 1;// 将 NPAGES-1 页插入到 Page cache 对应哈希桶的位置_spanlists[NPAGES - 1].PushFront(bigSpan);// 4、直接递归,复用上面的逻辑,分割小对象return NewSpan(k);
}
• central cache 的内存回收
当thread cache还一批内存对象给central cache时,就可以依次遍历这批对象,将这些对象插入到自己对象的span的自由链表中,并及时更新_useCount的计数。
在这个归还的过程中,如果发现central cache的某个span的_useCount减到了 0 就说明该span分出去的所有对象都还回来,此时就是需要向下一步还该span 了。
// 将thread cache还回来的对象 挂到合适的 span 中
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{// 根据size 找到对应的桶size_t index = SizeClass::Index(size);// 加桶锁_spanlists[index]._mtx.lock();// 将对象一个个的挂到合适的spanwhile (start){void* next = NextObj(start);// 找到当前对象对应的spanSpan* span = PageCache::GetInstance()->MapObjectToSpan(start);// 将对象头插到spanNextObj(start) = span->_freeList;span->_freeList = start;// 更新计数span->_useCount--;// 当前span 的所有对象都还回来了,此时就需要将span还给pageif (span->_useCount == 0){// 将当前的span从链表中 移除_spanlists[index].Erase(span);span->_freeList = nullptr;// 自由链表置空span->_next = nullptr;span->_prev = nullptr;// 将 span 还给page cache// 因为当前已经将 span 从链表中拿下来了,且要还给page cache了,为了不影响其他线程,我们当前可以把锁解了_spanlists[index]._mtx.unlock();// 给page cache 加锁PageCache::GetInstance()->_page_mtx.lock();PageCache::GetInstance()->ReleaseSpanToPage(span);PageCache::GetInstance()->_page_mtx.unlock();// 当前的span 还给span 后继续加锁,处理下一个对象_spanlists[index]._mtx.lock();}start = next;}// 解桶锁_spanlists[index]._mtx.unlock();
}
需要注意的是,如果当把某个span还给page cache,我们需要先将这个span从cache对应的双链表中移除,然后再将span的自由链表置空、前驱后继指针置空。但是不敢将span的起始页号以及页数清理,因为他要向page cache的哈希桶中插入,如果清空就找不到了。
这里你可能在想,这里每个span虽然内存都还回来了但是都是乱序的,这在page cach合并中没有影响嘛?没有的!因为span的起始地址(页号)知道,几页也是知道的。所以不用管内部是否乱序!
在将span 还给page cache 时需要注意锁的问题,当将span从链表中移除下来之后,应该将该位置的桶锁给解除,因为此时有可能其他线程也需要在该位置申请或者释放。然后加上page cache 的大锁,归还span,然后解除page cache的大锁。然后在进行下对一个对象进行挂到合适的span,因为在解除page cache的大锁后,要立即申请 桶锁。
三、page cache 回收内存
如果central cache 中某一个span的_useCount的值减为0了,那么central cache就需要将这个span还给page cache了。
这个思路一看很简单,只需要将span挂到page cache对应的桶中就好了,实际不然。为了缓解内存外碎片的问题,page cache还需要尝试将还回来的span与其他空闲的span进行合并成更大页的span。
• page cache 合并页的策略
page cache 的合并策略是:将当前还回来span 对应前后页的 span 尽可能的合并成更大页的span。如果当前还回来的 span 的页号是 id 该 span 管理的页数是 n。向前合并时,就需要判断 id-1 页的span是否空闲,如果空闲就将其和当前的span合并,合并完成之后,继续向前合并直到不能合并为止。向后合并时,需要判断 id + n 页的 span 是否空闲,如果空闲就合并,合并完成之后,继续向后合并,直到不能合并为止。
这里不能合并的条件是啥呢?
不能合并的条件有三个:
1、前面/后面的页号在映射的哈希表中没有(还没有向OS申请)
2、前面/后面的页号正在被使用
3、当前合并的页的大小,超过NPAGES-1以至于无法在page cache管理(也就是合并的过程中大块的内存是需要 <= NPAGES-1的)
因为上述page cache在进行合并相邻页的span时,需要通过根据页号获取相对应的span,所以这就是我们当时把页号与span之间的映射关系定义到page cache 层的原因。
这里虽然可以使用页号找到对应的span了,但是如何保证当前的 span 在page cache层呢?如果当前的span是在central cache层的那是不能够进行合并的,因为central cache的span有可能正在被线程使用了。
这里你可能想到说,可以是使用_useCount是否等于0来确定他在那一层的。这种想法是不对的,有一种情况:当central cache刚向page cache 申请了一个span此时他还没有来得及给上层thread cache了,即_useCount 是 0,此时如果page cache将其回收回去合并了,这显然是不合理的。
因此基于上面的原因,我们可以在Span中添加一个布尔类型的字段 _isUse 标记当前的span是否在被使用,page cache 层的就是 false表示未被使用,central cache就是true表示在使用。当central cache找page cache申请成功时,将 _isUse 置为 true,等central cache将span还给page cache后将 _isUse 置为 false。
// 管理多个连续页大块内存的跨度结构
struct Span
{PAGE_ID _page_id = 0; // 大内存块的起始页的页号size_t _n = 0; // 页的数量Span* _prev = nullptr;// 双链结构Span* _next = nullptr;size_t _useCount = 0; // 切好的内存块分配给 thread cache 的计数void* _freeList = nullptr; // 切好小内存块的自由链表bool _isUse = false; // 标识当前的span 是否被使用 ,默认是未使用的
};
由于在合并page cache的span的过程中,需要它通过页号找到其对应的span,但是一个span在被分配给central cache时,才会建立各个页与span之间的映射关系,而剩余挂在page cache 的那个span是没有进行页号和span的映射关系的建立的,因此在page cache之间的span也需要和其页号进行映射关系的建立。
与central cache中span不同的是,在page cache中,只需要建立一个span的首尾页号与span的映射关系即可。因为当一个span在进行尝试合并时,如果是向前合并,只需要通过一个span的尾页号即可找到该span,同理如果是向后合并,只需要通过一个span的首页号即可找到该span。因此,在page cache中的映射只需要span与该span管理的 起始页号 和 尾页号即可。
所以,当我们申请k页的span时,如果是将n页的span切成了k页 和 n - k 页的span,我们除了需要将k页span的每一页与kspan进行映射外,还需要将 n- k页的n-kspan进行首尾页号与n-kspan进行映射。
Span* PageCache::NewSpan(size_t k)
{assert(k > 0 && k < NPAGES);// 1、page cache 的第 k 号 桶中存在 非空的 k页 span,直接头删拿下来返回if (!_spanlists[k].Empty()){Span* kSpan = _spanlists[k].PopFront();// 建立页号与span的映射,方便central cache回收小块内存时查找对应的spanfor (PAGE_ID i = 0; i < kSpan->_n; i++){_idSpanMap[kSpan->_page_id + i] = kSpan;}// 将分配给central cache 的 span 设置为 ture 表示 使用kSpan->_isUse = true;return kSpan;}// 2、当前的 第 k 号 桶中 不 存在 非空的 k页 span,到[k+1, NPAGES - 1] 中找for (int i = k + 1; i < NPAGES; i++){if (!_spanlists[i].Empty()){Span* nSpan = _spanlists[i].PopFront();Span* kSpan = new Span;// 将 nSpan 分割成 k 页 和 n - k页kSpan->_page_id = nSpan->_page_id;kSpan->_n = k;nSpan->_page_id += k;nSpan->_n -= k;// 将 n - k 页插入到 第 n - k 个桶中,将 k 页返回_spanlists[nSpan->_n].PushFront(nSpan);// 将n - k 页的 span 与其 首页号 与 尾页号 进行映射_idSpanMap[nSpan->_page_id] = nSpan;_idSpanMap[nSpan->_page_id + nSpan->_n - 1] = nSpan;// 这里需要减 1 例如:页号100 5页,那尾页号就是104 ==》100 + 5 - 1// 建立页号与span的映射,方便central cache回收小块内存时查找对应的spanfor (PAGE_ID i = 0; i < kSpan->_n; i++){_idSpanMap[kSpan->_page_id + i] = kSpan;}// 将分配给central cache 的 span 设置为 ture 表示 使用kSpan->_isUse = true;// 将 kSpan返回即可return kSpan;}}// 3、走到这,说明[k+1,NPAGES-1]个桶中都没有大块内存,此时就需要向 OS 申请 NAGES-1 页的大块内存了Span* bigSpan = new Span;void* ptr = SystemAlloc(NPAGES - 1);bigSpan->_page_id = (PAGE_ID)ptr >> PAGE_SHIFT;// 将起始地址除以每一页的大小,就是页号bigSpan->_n = NPAGES - 1;// 将 NPAGES-1 页插入到 Page cache 对应哈希桶的位置_spanlists[NPAGES - 1].PushFront(bigSpan);// 4、直接递归,复用上面的逻辑,分割小对象return NewSpan(k);
}
• page cache 进行合并 span 的实现
此时page cache中的span都与他的首页号和尾页号都建立了映射,我们就可以根据上述的合并策略进行合并了。
// 将central cache 还回来的 span 进行合并 并 挂到对应的 桶中
void PageCache::ReleaseSpanToPage(Span* span)
{// 对spand的前后页进行尝试合并缓解内存外碎片化的问题// 1、向前合并while (1){// 获取前一个span 的尾页号PAGE_ID prevId = span->_page_id - 1;// 查找该页号对应的span是否在映射的哈希表中auto ret = _idSpanMap.find(prevId);// 1、如果前一个span 的尾页号不存在(还未向OS申请),直接停止合并if (ret == _idSpanMap.end()){break;}// 2、前面页号对应的span正在被使用,停止合并Span* prevSpan = ret->second;if (prevSpan->_isUse == true){break;}// 3、加上前面页合并出的span超过了 NPAGES - 1无法管理,停止合并if (span->_n + prevSpan->_n > NPAGES - 1){break;}// 进行合并span->_page_id = prevSpan->_page_id;span->_n += prevSpan->_n;// 将 prevSpan 从双链表中 移除_spanlists[prevSpan->_n].Erase(prevSpan);delete prevSpan;}// 2、向后合并while (1){// 获取后一个span 的首页号PAGE_ID nextId = span->_page_id + span->_n;// 判断 该页号 对应的 span 是否 和他进行了映射auto ret = _idSpanMap.find(nextId);// 1、如果没有建立映射(还未向OS申请),停止合并if (ret == _idSpanMap.end()){break;}// 2、如果后一个span正在被使用,停止合并Span* nextSpan = ret->second;if (nextSpan->_isUse == true){break;}// 3、如过加上后面合并出的span超出了 NPAGES - 1无法管理,停止合并if (span->_n + nextSpan->_n > NPAGES - 1){break;}// 合并span->_n += nextSpan->_n;// 将 nextSpan 从链表中移除_spanlists[nextSpan->_n].Erase(nextSpan);delete nextSpan;}// 将该合并号的 span 挂到 对应的双链表中_spanlists[span->_n].PushFront(span);// 将合并的span进行 与 首尾页号的映射_idSpanMap[span->_page_id] = span;_idSpanMap[span->_page_id + span->_n - 1] = span;// 将合并的span设置为未被使用的状态span->_isUse = false;
}
注意:在合并结束后,除了将合并后的span挂到page cache对应哈希桶的双链表当中,还需要建立该span与其首位页之间的映射关系,便于此后合并出更大的span。
四、内存回收过程的整体联调
相关文章:

【项目日记(八)】内存回收与联调
前言 我们前面实现了三层缓存申请的过程,并完成了三层缓存申请过程的联调!本期我们来介绍三层的缓存的回收机制以及三层整体联调释放的过程。 目录 前言 一、thread cache 回收内存 二、central cache 回收内存 • 如何确定一个对象对应的span • …...

性能测试监控工具jmeter+grafana
1、什么是性能测试监控体系? 为什么要有监控体系? 原因: 1、项目-日益复杂(内部除了代码外,还有中间件,数据库) 2、一个系统,背后可能有多个软/硬件组合支撑,影响性能的因…...

016.3月夏令营:数理类
016.3月夏令营:数理类: 中国人民大学统计学院: http://www.eeban.com/forum.php?modviewthread&tid386109 北京大学化学学院第一轮: http://www.eeban.com/forum.php?m ... 6026&extrapage%3D1 香港大学化学系夏令营&a…...

CS144 Lab Checkpoint 0: networking warm up
Set up GNU/Linux on your computer 我用的是Ubuntu,按照指导书上写的输入如下命令安装所需的软件包: sudo apt update && sudo apt install git cmake gdb build-essential clang \ clang-tidy clang-format gcc-doc pkg-config glibc-doc tc…...

靶场之路-VulnHub-DC-6 nmap提权、kali爆破、shell反连
靶场之路-VulnHub-DC-6 一、信息收集 1、扫描靶机ip 2、指纹扫描 这里扫的我有点懵,这里只有两个端口,感觉是要扫扫目录了 nmap -sS -sV 192.168.122.128 PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.4p1 Debian 10deb9u6 (protoc…...
给没有登录认证的web应用添加登录认证(openresty lua实现)
这阵子不是deepseek火么?我也折腾了下本地部署,ollama、vllm、llama.cpp都弄了下,webui也用了几个,发现nextjs-ollama-llm-ui小巧方便,挺适合个人使用的。如果放在网上供多人使用的话,得接入登录认证才好&a…...

3月5日作业
代码作业: #!/bin/bash# 清空目录函数 safe_clear_dir() {local dir"$1"local name"$2"if [ -d "$dir" ]; thenwhile true; doread -p "检测到 $name 目录已存在,请选择操作: 1) 清空目录内容 2) 保留目…...

【MySQL】增删改查
目录 一、新增(Create) 单行数据 全列插入 多行数据 指定列插入 插入时间 二、查询(Retrieve) 全列查询 指定列查询 查询字段为表达式 别名 去重:DISTINCT 排序:ORDER BY 条件查询࿱…...

【三维生成】StarGen:基于视频扩散模型的可扩展的时空自回归场景生成
标题:《StarGen: A Spatiotemporal Autoregression Framework with Video Diffusion Model for Scalable and Controllable Scene Generation》 项目:https://zju3dv.github.io/StarGen 来源:商汤科技、浙大CAD、Tetras.AI 文章目录 摘要一、…...

线反转法实现矩形键盘按键识别
由于行、列线为多键共用,各按键彼此将相互发 生影响,必须将行、列线信号配合起来并作适当的处 理,才能确定闭合键的位置。 线反转法 第1步:列线输出为全低电平,则行线中电平由高变低 的所在行为按键所在行。 第2步&…...

在 Element Plus 的 <el-select> 组件中,如果需要将 <el-option> 的默认值设置为 null。 用于枚举传值
文章目录 引言轻松实现 `<el-option>` 的默认值为 `null`I 实现方式监听清空事件 【推荐】使用 v-model 绑定 null添加一个值为 null 的选项处理 null 值的显示引言 背景:接口签名规则要求空串参与,空对象不参与签名计算 // 空字符串“” 参与签名组串,null不参与签…...
大白话面试中应对自我介绍
在面试中,自我介绍是开场的关键环节,它就像你递给面试官的一张“个人名片”,要让面试官快速了解你并对你产生兴趣。下面详细讲讲应对自我介绍的要点及回答范例。 一、自我介绍的时间把控 一般面试中的自我介绍控制在1 - 3分钟比较合适。时间…...
Pytorch构建LeNet进行MNIST识别 #自用
LeNet是一种经典的卷积神经网络(CNN)结构,由Yann LeCun等人在1998年提出,主要用于手写数字识别(如MNIST数据集)。作为最早的实用化卷积神经网络,LeNet为现代深度学习模型奠定了基础,…...

元宇宙崛起:区块链与金融科技共绘数字新世界
文章目录 一、引言二、元宇宙与区块链的深度融合三、区块链在元宇宙金融中的应用四、金融科技在元宇宙中的创新应用五、面临的挑战与机遇《区块链与金融科技》亮点内容简介获取方式 一、引言 随着科技的飞速发展,元宇宙概念逐渐走进人们的视野,成为数字…...

React Native 实现滑一点点内容区块指示器也滑一点点
效果图如上,内容滑一点点,指示器也按比例话一点点,列表宽度跟数据有关。 实现思路如下: 1.监听列表滑动事件,获取列表横向滑动距离,假设为A; 2.获取列表的宽度,及列表可滑动的宽度…...

怎么写C#命令行参数程序,及控制台带参数案例(程序完整源码)下载
C#命令行参数解析控制台带参数编写案例(程序完整源码)下载链接 https://download.csdn.net/download/luckyext/90434790 在CMD命令窗口,输入ping 、ipconfig等这样的命令,大家应该都知道,但很多同学可能不知道怎么写…...

全国青少年航天创新大赛各项目对比分析
全国青少年航天创新大赛各项目对比分析 一、比赛场地对比 项目名称场地尺寸场地特点组别差异筑梦天宫虚拟三维场景动态布局,小学组3停泊处,初高中组6停泊处;涉及传送带、机械臂、传感器等虚拟设备。初中/高中组任务复杂度更高,运…...

基于RAG的法律条文智能助手
文章目录 前言一、 项目背景与需求设计二、 数据收集与整理三、 核心实现流程1. 配置与模型初始化1. 配置区2. 模型初始化(init_models 函数)3. 数据加载与验证(load_and_validate_json_files 函数)4. 节点生成(create…...

智能对讲机:5G+AI赋能下的石油工业新“声”态
在浩瀚的能源版图上,中国正以非凡的“深度”探索着石油资源的奥秘。随着5G技术的不断成熟与普及,曾经“满山遍野找信号”的石油工人,如今已步入了一个全新的通信时代。在这个时代里,智能对讲机成为了连接指挥中心与一线工人的桥梁…...

leetcode日记(77)子集Ⅱ
不知道为什么看到这道题就很头痛…… 其实只要掌握nums不包含重复元素的情况下的代码就行了。 若nums不能包含重复元素,那么使用回溯很容易就能写出来: class Solution {void hs(vector<int> v,int x,vector<int> r,vector<vector<…...

linux之kylin系统nginx的安装
一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源(HTML/CSS/图片等),响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址,提高安全性 3.负载均衡服务器 支持多种策略分发流量…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
DeepSeek 技术赋能无人农场协同作业:用 AI 重构农田管理 “神经网”
目录 一、引言二、DeepSeek 技术大揭秘2.1 核心架构解析2.2 关键技术剖析 三、智能农业无人农场协同作业现状3.1 发展现状概述3.2 协同作业模式介绍 四、DeepSeek 的 “农场奇妙游”4.1 数据处理与分析4.2 作物生长监测与预测4.3 病虫害防治4.4 农机协同作业调度 五、实际案例大…...
使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度
文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...
Qt 事件处理中 return 的深入解析
Qt 事件处理中 return 的深入解析 在 Qt 事件处理中,return 语句的使用是另一个关键概念,它与 event->accept()/event->ignore() 密切相关但作用不同。让我们详细分析一下它们之间的关系和工作原理。 核心区别:不同层级的事件处理 方…...
LLaMA-Factory 微调 Qwen2-VL 进行人脸情感识别(二)
在上一篇文章中,我们详细介绍了如何使用LLaMA-Factory框架对Qwen2-VL大模型进行微调,以实现人脸情感识别的功能。本篇文章将聚焦于微调完成后,如何调用这个模型进行人脸情感识别的具体代码实现,包括详细的步骤和注释。 模型调用步骤 环境准备:确保安装了必要的Python库。…...

uni-app学习笔记三十五--扩展组件的安装和使用
由于内置组件不能满足日常开发需要,uniapp官方也提供了众多的扩展组件供我们使用。由于不是内置组件,需要安装才能使用。 一、安装扩展插件 安装方法: 1.访问uniapp官方文档组件部分:组件使用的入门教程 | uni-app官网 点击左侧…...
Python常用模块:time、os、shutil与flask初探
一、Flask初探 & PyCharm终端配置 目的: 快速搭建小型Web服务器以提供数据。 工具: 第三方Web框架 Flask (需 pip install flask 安装)。 安装 Flask: 建议: 使用 PyCharm 内置的 Terminal (模拟命令行) 进行安装,避免频繁切换。 PyCharm Terminal 配置建议: 打开 Py…...

高端性能封装正在突破性能壁垒,其芯片集成技术助力人工智能革命。
2024 年,高端封装市场规模为 80 亿美元,预计到 2030 年将超过 280 亿美元,2024-2030 年复合年增长率为 23%。 细分到各个终端市场,最大的高端性能封装市场是“电信和基础设施”,2024 年该市场创造了超过 67% 的收入。…...

职坐标物联网全栈开发全流程解析
物联网全栈开发涵盖从物理设备到上层应用的完整技术链路,其核心流程可归纳为四大模块:感知层数据采集、网络层协议交互、平台层资源管理及应用层功能实现。每个模块的技术选型与实现方式直接影响系统性能与扩展性,例如传感器选型需平衡精度与…...