《C++实战项目-高并发内存池》6.内存释放流程

《C++实战项目-高并发内存池》6.内存释放流程

💡Yupureki:个人主页

✨个人专栏:《C++》 《算法》《Linux系统编程》《高并发内存池》


🌸Yupureki🌸的简介:


目录

1. 准备工作

2. ThreadCache内存回收与释放

3. CentralCache内存回收与释放

4. PageCache内存回收与释放


完整项目链接https://github.com/Yupureki-code/ConcurrentMemoryPool

1. 准备工作

当ThreadCache把内存块还给CentralCache时,这些内存块挂在哪里?我们知道这些内存块之前是在一个Span下的,而也应该理所应当还给那个Span

因此我们必须用哈希表记录一个内存块对应的Span。由于每个Span也都是PageCache给CentralCache的,对应关系也应该由PageCache知道,因此我们得在PageCache内新增一个内存块地址查找Span的哈希表

class PageCache { public: static PageCache* GetInstance() { return &_sInst; } // 获取从对象到span的映射 Span* MapObjectToSpan(void* obj); // 释放空闲span回到Pagecache,并合并相邻的span void ReleaseSpanToPageCache(Span* span); // 获取一个K页的span Span* NewSpan(size_t k); std::mutex _pageMtx; private: SpanList _spanLists[NPAGES]; std::unordered_map<PAGE_ID, Span*> _idSpanMap;//哈希表:页ID到Span的映射关系 PageCache() {} PageCache(const PageCache&) = delete; static PageCache _sInst; }; 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; } }

何时更新_idSpanMap?在PageCache中,Span内的内存块是一个连续的大块的内存块,也没有被使用,还不需要被记录。直到CentralCache申请拿走后,要进行切分,此时一块块的内存就需要存储对应的Span了。此环节对应PageCache的NewSpan函数中。因此该函数需要更新

// 获取一个K页的span Span* PageCache::NewSpan(size_t k) { assert(k > 0 && k < NPAGES); // 先检查第k个桶里面有没有span if (!_spanLists[k].Empty()) { return _spanLists->PopFront(); } // 检查一下后面的桶里面有没有span,如果有可以把他它进行切分 for (size_t i = k+1; i < NPAGES; ++i) { if (!_spanLists[i].Empty()) { Span* nSpan = _spanLists[i].PopFront(); Span* kSpan = new Span; // 在nSpan的头部切一个k页下来 // k页span返回 // nSpan再挂到对应映射的位置 kSpan->_pageId = nSpan->_pageId; kSpan->_n = k; nSpan->_pageId += k; nSpan->_n -= k; _spanLists[nSpan->_n].PushFront(nSpan); // 存储nSpan的首位页号跟nSpan映射,方便page cache回收内存时 // 进行的合并查找 _idSpanMap[nSpan->_pageId] = nSpan; _idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan; // 建立id和span的映射,方便central cache回收小块内存时,查找对应的span for (PAGE_ID i = 0; i < kSpan->_n; ++i) { _idSpanMap[kSpan->_pageId + i] = kSpan; } return kSpan; } } // 走到这个位置就说明后面没有大页的span了 // 这时就去找堆要一个128页的span Span* bigSpan = new Span; void* ptr = SystemAlloc(NPAGES - 1); bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT; bigSpan->_n = NPAGES - 1; _spanLists[bigSpan->_n].PushFront(bigSpan); return NewSpan(k); }

2. ThreadCache内存回收与释放

当ThreadCache把内存收回后,会把内存块串在对应的哈希桶内

void ThreadCache::Deallocate(void* ptr, size_t size) { assert(ptr); assert(size <= MAX_BYTES); // 找对映射的自由链表桶,对象插入进入 size_t index = SizeClass::Index(size); _freeLists[index].Push(ptr); // 当链表长度大于一次批量申请的内存时就开始还一段list给central cache if (_freeLists[index].Size() >= _freeLists[index].MaxSize()) { ListTooLong(_freeLists[index], size); } }

当哈希桶内的内存块过多时,就应该还给CentralCache

void ThreadCache::ListTooLong(FreeList& list, size_t size) { void* start = nullptr; void* end = nullptr; list.PopRange(start, end, list.MaxSize());//把过长的内存块拿出 CentralCache::GetInstance()->ReleaseListToSpans(start, size);//还给CentralCache }

3. CentralCache内存回收与释放

当CentralCache收回ThreadCache还回来的内存块:

  1. 通过PageCache内的哈希表查找内存块对应的Span
  2. 串在对应的Span下
  3. 如果该Span没有任何内存块被ThreadCache拿走使用去了,直接还给PageCache
void CentralCache::ReleaseListToSpans(void* start, size_t size) { size_t index = SizeClass::Index(size); _spanLists[index]._mtx.lock(); while (start) { void* next = NextObj(start); Span* span = PageCache::GetInstance()->MapObjectToSpan(start); NextObj(start) = span->_freeList; span->_freeList = start; span->_useCount--; // 说明span的切分出去的所有小块内存都回来了 // 这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并 if (span->_useCount == 0) { _spanLists[index].Erase(span); span->_freeList = nullptr; span->_next = nullptr; span->_prev = nullptr; // 释放span给page cache时,使用page cache的锁就可以了 // 这时把桶锁解掉 _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(); }

4. PageCache内存回收与释放

当PageCache收回CentralCache还回来的内存块:

  1. 将该内存块向前和向后合并,一直到无法合并为止
  2. 合并后,串在对应的哈希桶内
  3. 合并后,对应的哈希映射关系需要清除
void PageCache::ReleaseSpanToPageCache(Span* span) { // 对span前后的页,尝试进行合并,缓解内存碎片问题 while (1) { PAGE_ID prevId = span->_pageId - 1; auto ret = _idSpanMap.find(prevId); // 前面的页号没有,不合并了 if (ret == _idSpanMap.end()) { break; } // 前面相邻页的span在使用,不合并了 Span* prevSpan = ret->second; if (prevSpan->_isUse == true) { break; } // 合并出超过128页的span没办法管理,不合并了 if (prevSpan->_n + span->_n > NPAGES-1) { break; } span->_pageId = prevSpan->_pageId; span->_n += prevSpan->_n; _spanLists[prevSpan->_n].Erase(prevSpan); delete prevSpan; } // 向后合并 while (1) { PAGE_ID nextId = span->_pageId + span->_n; auto ret = _idSpanMap.find(nextId); if (ret == _idSpanMap.end()) { break; } Span* nextSpan = ret->second; if (nextSpan->_isUse == true) { break; } if (nextSpan->_n + span->_n > NPAGES-1) { break; } span->_n += nextSpan->_n; _spanLists[nextSpan->_n].Erase(nextSpan); delete nextSpan; } _spanLists[span->_n].PushFront(span); span->_isUse = false; _idSpanMap[span->_pageId] = span; _idSpanMap[span->_pageId+span->_n-1] = span; } 

Read more

IDEA 中的 AI 编程插件怎么选?Copilot / 灵码 / TRAE 实际使用对比

IDEA 中的 AI 编程插件怎么选?Copilot / 灵码 / TRAE 实际使用对比

# 【不吹不黑】Java 开发者真实体验:IDEA 三大 AI 编程插件深度对比(Copilot / TRAE / 灵码) > 本文是一篇**技术交流与使用体验记录**,仅用于分享 Java 开发过程中使用 AI 插件的真实感受与效率提升方式,不涉及任何商业推广或广告行为。 *** ## 一、写在前面:为什么要写这篇文章 过去一年,大模型能力的跃迁,直接改变了开发者的工作方式。**AI 已经不再是“写 Demo 的玩具”,而是逐渐演变为 IDE 中的“第二大脑”** 。 本文的目的非常明确: *   记录一名 **Java 后端开发者** 在真实项目中使用 AI 插件的体验 *   对比不同插件在 **补全、对话、Agent 工作流** 等方面的差异 *   帮助开发者根据自身场景选择合适的工具,而不是盲目跟风 本文所有结论,

By Ne0inhk
Copilot、Codeium 软件开发领域的代表性工具背后的技术

Copilot、Codeium 软件开发领域的代表性工具背后的技术

早期, Claude、Copilot、Codeium新兴的AI代码助手,模型的温度、切片的效果、检索方式、提示词的约束、AI 回复的约束、最终数据处理;整个环节,任何一个地方都可能造成最终效果不理想。 旨在通过代码生成、代码补全、代码解释和调试等多种功能,帮助开发者减少重复劳动,提高开发效率。尽管Codeium已经取得了显著的成果,但在处理复杂的代码任务、跨文件的修改以及支持定制化库和框架方面仍面临一定的局限性。 2020 年,OpenAI发布的GPT-3模型使AI生成代码的能力得以广泛应用,标志着AI代码助手的转型。2021年,GitHub 推出基于OpenAI Codex的 Copilot,提供实时代码补全和生成能力,提升开发效率,支持跨文件复杂任务。 其痛点,在大规模代码生成、跨文件任务处理以及定制化框架支持方面的局限性仍然限制了其在复杂项目中的应用。 2023年,Claude 3.5等新一代大型语言模型陆续出世,有效提升了自然语言理解与代码生成的能力。这类模型集成了代码生成、调试和文档自动生成等多项功能,能够帮助开发者快速编写高质量代码、优化程序性能并自动修复错误。随着

By Ne0inhk
Git如何查看提交行数与删除行数:统计代码贡献量的完整指南

Git如何查看提交行数与删除行数:统计代码贡献量的完整指南

Git如何查看提交行数与删除行数:统计代码贡献量的完整指南 在软件开发中,代码行数统计是衡量团队协作效率和项目进度的重要指标。通过Git的命令行工具,开发者可以轻松查看提交的代码行数、删除的代码行数以及净增行数。本文将详细介绍多种方法,并结合实际案例,帮助你快速掌握这一技能。 一、为什么需要统计代码行数? 1. 评估工作量:统计个人或团队的代码贡献量,辅助绩效考核。 2. 分析代码质量:通过删除行数判断重构频率,评估代码优化效果。 3. 项目管理:监控项目整体进度,识别高频修改的模块。 二、基础命令:查看个人提交的行数 1. 统计今日提交的代码量 若想查看今天某个开发者(如xiaoming)的代码贡献,可使用以下命令: git log --author="xiaoming"--since=midnight --pretty=tformat: --numstat * --author="xiaoming":指定提交者。 * --since=

By Ne0inhk
Flutter for OpenHarmony:git 纯 Dart 实现的 Git 操作库(在应用内实现版本控制) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:git 纯 Dart 实现的 Git 操作库(在应用内实现版本控制) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter for OpenHarmony:git 纯 Dart 实现的 Git 操作库(在应用内实现版本控制) 深度解析与鸿蒙适配指南 前言 Git 通常作为命令行工具存在。但在某些特殊场景下,你可能需要在 App 内部直接操作 Git 仓库,例如: * 开发一个手机端的 Git 客户端 App。 * 使用 Git 作为笔记应用(如 Obsidian)的同步后端。 * 在应用内拉取远程配置或 CMS 内容。 git 是一个纯 Dart 实现的 Git 核心库(类似于 Java 的 JGit)。它负责直接读写

By Ne0inhk