内存池(C++)v3 | 简历写法 | 相关面试题
文章目录
- 项目优化
- ThreadCache.cpp
- CentralCache.cpp
- 测试结果
- 简历写法
- 相关面试题
- 一、基础原理相关
- 1、为什么需要设计内存池?与系统默认的内存分配器相比,有哪些优势?
- 2、内存对齐的意义是什么?在项目中是如何实现内存对齐的?
- 3、如何避免内存池内的内存碎片化?
- 二、并发与线程安全相关
- 1、如何实现线程安全的内存池?在项目中使用了哪些同步机制?
- 2、无锁队列的实现原理是什么?项目中的无锁数据结构如何工作?
- 3、你在项目中提到自旋锁和 std::this_thread::yield,请解释它们的作用及适用场景。
- 4、伪共享问题是什么?如何避免伪共享?
- 三、项目设计相关
- 1、为什么选择三层架构(ThreadCache、CentralCache 和 PageCache)?每一层的职责是什么?
- 2、动态批量分配策略是如何设计的?它是如何根据使用模式调整的?
- 3、智能预取机制的实现原理是什么?如何优化内存分配延迟?
- 四、性能优化相关
- 1、如何测试内存池的性能?测试中发现了哪些瓶颈,又是如何优化的?
- 2、高并发场景下,如何确保内存池的性能优于系统内存分配器?
- 五、C++语言相关
- 1、在项目中用到了哪些 C++ 特性?为什么选择这些特性?
- 2、原子操作(std::atomic)与互斥锁的区别是什么?项目中什么时候使用原子操作,什么时候使用锁?
- 3、在高并发场景中,compare_exchange_weak 和 compare_exchange_strong 有什么区别?为什么选择 compare_exchange_weak?
- 六、开源与扩展相关
- 1、你的内存池如何适配不同大小的对象?是否支持大对象分配?
- 2、CAS操作是什么
- 相关纯八股面试题
- 操作系统相关
- 1、什么是死锁,如何预防死锁?
- 2、介绍一下几种典型的锁?
- 3、解释一下用户态和核心态?
- C++语言相关
- 1、vector容器如何进行动态内存的分配和管理?
- 2、什么是内存池?它如何帮助优化内存使用?
- 3、解释C++中的内存碎片及其影响。
- 内部碎片与外部碎片的定义
- 4、请解释C++中的内存对齐和填充。
- 5、什么是内存池分配器?它如何工作?
- 6、如何在C++中实现一个简单的内存池?
项目优化
ThreadCache在向CentralCache申请内存时是批量申请的,而不是向版本2一样内存按需申请的
项目完整代码
ThreadCache.cpp
void* ThreadCache::fetchFromCentralCache(size_t index) {size_t size = (index + 1) * ALIGNMENT; // 计算实际大小// 根据对象内存大小计算批量获取的数量size_t batchNum = getBatchNum(size);// 从中心缓存批量获取内存void* start = CentralCache::getInstance().fetchRange(index, batchNum);if(!start) {return nullptr; // 中心缓存没有可用内存}// 更新自由链表大小// 增加对应大小类的自由链表大小freeListSize_[index] += batchNum;// 取一个返回,其余放入线程本地自由链表void* result = start;if(batchNum > 1) {// 将start的下一个节点地址存入freeList_[index]freeList_[index] = *reinterpret_cast<void**>(start);}return result;}// 计算批量获取内存块的数量
size_t ThreadCache::getBatchNum(size_t size) {// 基准:每次批量获取不超过4KB内存constexpr size_t MAX_BATCH_SIZE = 4 * 1024; // 4KB// 根据对象大小设置合理的基准批量数size_t baseNum;if(size <= 32) {baseNum = 64; // 64 * 32 = 2KB} else if(size <= 64) {baseNum = 32; // 32 * 64 = 2KB} else if(size <= 128) {baseNum = 16; // 16 * 128 = 2KB} else if(size <= 256) {baseNum = 8; // 8 * 256 = 2KB} else if(size <= 512) {baseNum = 4; // 4 * 512 = 2KB} else if(size <= 1024) {baseNum = 2; // 2 * 1024 = 2KB} else {baseNum = 1; // 大于1024直接获取一个}// 计算最大批量数size_t maxNum = std::max(size_t(1), MAX_BATCH_SIZE / size);// 取最小值,但确保至少返回1return std::max(size_t(1), std::min(baseNum, maxNum));
}
CentralCache.cpp
// 当线程缓存(ThreadCache)不足时,会调用此函数从中心缓存(CentralCache)批量获取内存。
// 如果中心缓存没有可用内存,则进一步从底层的页缓存(PageCache)获取大块内存并切分为小块。
void* CentralCache::fetchRange(size_t index, size_t batchNum) {// 索引检查,当索引大于等于FREE_LIST_SIZE时,说明申请内存过大应直接向系统申请if(index >= FREE_LIST_SIZE || batchNum == 0) {return nullptr; // 索引越界,无法获取内存}// 自旋锁保护// 线程A: 获取锁成功 → 临界区执行 → 释放锁// 线程B: 获取锁失败 → 忙等待自旋 → 锁释放 → 获取锁成功// 线程C: 获取锁失败 → 忙等待自旋 → 锁释放 → 获取锁成功// test_and_set() 是 std::atomic_flag 唯一提供的修改方法:// 原子地将标志位设置为 true(表示“已锁定”)// 如果test_and_set()返回true(表示锁被其他线程占有),则循环继续,进入自旋等待状态。// 如果返回false(表示锁未被占有),则成功获取锁,退出循环,进入临界区。// Acquire 语义的作用:// 确保后续的内存操作不会被重排到锁的获取之前。// 使得当前线程在获取锁之后,能够安全地看到之前线程释放锁前的所有内存写入操作的效果。// 释放线程: [内存修改操作] → 锁释放 (release)// 获取线程: 锁获取 (acquire) → [读取释放线程的修改操作]// memory_order_acquire提供了一种内存屏障(Memory Barrier):// 确保当前线程在获取锁(或原子变量)后,后续的内存读写操作一定不会被重排到锁获取之前。// 从而保障了线程看到的内存状态和预期是一致的while(locks_[index].test_and_set(std::memory_order_acquire)) {// 添加线程让步,避免忙等待,避免过度消耗CPU// yield() 函数的作用:// 提示操作系统主动让出当前线程的CPU时间片,给其他线程使用。// 避免“纯忙等待”导致CPU占用率飙高,降低系统整体吞吐量。// 线程等待获取锁 (spin):// 尝试获取锁 → 锁被占用 → 调用yield() → 暂停线程运行// └→ 调度器调度其他线程运行// yield 的使用并不会挂起线程,只是告知调度器:// 当前线程可以暂时让步,调度其他线程运行。// 避免“无效的CPU忙等待”状态。std::this_thread::yield();}void* result = nullptr;try {// 尝试从中心缓存获取内存块result = centralFreeList_[index].load(std::memory_order_relaxed);if(!result) {// 若中心缓存为空,从底层页缓存(PageCache)获取新的内存// size 就是单个内存块大小size_t size = (index + 1) * ALIGNMENT;result = fetchFromPageCache(size);if(!result) {locks_[index].clear(std::memory_order_release);// 若页缓存也无法提供内存,释放锁并返回nullptr表示失败。return nullptr;}// 将获取的内存块切分成小块// 从更底层的PageCache成功获取到一大块连续内存(一个Span)后。// 现在要做的事情是:// 将这一大块内存切割成多个小块。// 构建链表,存入CentralCache的自由链表中,以便后续分配。// 从切割出的内存块中,取出一个返回给调用者(通常是ThreadCache),其他的存入中心缓存。// 转换为char*类型,便于后续地址运算(字节级偏移)。char* start = static_cast<char*>(result);size_t totalBlocks = (SPAN_PAGES * PageCache::PAGE_SIZE) / size; // 计算总块数size_t allocBlocks = std::min(batchNum, totalBlocks); // 实际分配的块数// 构建返回给ThreadCache的内存块链表if(allocBlocks > 1) {// 确保至少有两个块才构建链表// 构建链表for(size_t i = 1; i < allocBlocks; ++i) {void* current = start + (i - 1) * size;void* next = start + i * size;*reinterpret_cast<void**>(current) = next;}*reinterpret_cast<void**>(start + (allocBlocks - 1) * size) = nullptr; // 最后一个块指向nullptr}// 构建保留在CentralCache的链表if(totalBlocks > allocBlocks) {void* remainStart = start + allocBlocks * size;for(size_t i = allocBlocks + 1; i < totalBlocks; ++i) {void* current = start + (i - 1) * size;void* next = start + i * size;*reinterpret_cast<void**>(current) = next;}*reinterpret_cast<void**>(start + (totalBlocks - 1) * size) = nullptr; // 最后一个块指向nullptrcentralFreeList_[index].store(remainStart, std::memory_order_release); // 更新中心缓存的自由链表头}} else {// 如果中心缓存有index对应大小的内存块// 从现有链表中获取指定数量的块void* current = result;void* prev = nullptr;size_t count = 0;while(current && count < batchNum) {prev = current; // 保留前一个块current = *reinterpret_cast<void**>(current); // 获取下一个块count++;}// 当前centralFreeList_[index]链表上的内存块大于batchNum时需要用到 if(prev) {*reinterpret_cast<void**>(prev) = nullptr;}centralFreeList_[index].store(current, std::memory_order_release); // 更新中心缓存的自由链表头}} catch(...) {// 发生异常时确保释放锁locks_[index].clear(std::memory_order_release);throw; // 重新抛出异常}// 释放锁locks_[index].clear(std::memory_order_release);return result;
}
测试结果
测试结果表明内存池版本3的性能要略好于内存池版本2
简历写法
cpp-高并发内存池
项目描述:基于C++实现的一个线程安全的高并发内存池,旨在优化内存分配和释放的性能,特别是在多线程环境下。项目包括内存池的设计与实现、性能优化以及测试验证。
主要工作
● 设计三层缓存架构:实现了ThreadCache、CentralCache和PageCache三层缓存架构,通过分层管理提升内存分配效率,减少锁竞争,优化多线程访问性能。
● 实现高效同步机制:采用thread_local、自旋锁和原子操作等多种同步机制,在不同层级选择最优的同步策略,显著提升并发性能。
● 优化内存管理:设计了基于8字节对齐的内存分配策略和Span内存块追踪机制,实现延迟归还策略,有效减少内存碎片和提高内存利用率。
● 实现批量内存管理:设计动态阈值的批量分配和释放机制,优化Span分配策略,减少系统调用次数,提高内存分配效率。
● 完善性能测试:实现了小对象分配、多线程并发和混合大小内存分配等多种测试场景,验证内存池在各种负载下的性能表现。
项目难点
● 线程安全实现:在高并发环境下保证数据一致性,通过在不同缓存层使用thread_local、自旋锁和互斥锁等多种同步机制,解决了多线程访问冲突问题。
● 内存管理优化:设计并实现了SpanTracker机制和延迟归还策略,解决了内存碎片、内存利用率低和频繁系统调用等问题,显著提升了内存管理效率。
● 性能调优难点:通过实现批量内存分配、优化内存回收时机和设计多级缓存架构,解决了锁竞争频繁、系统负载过高等性能瓶颈问题。
个人收获
● 深入理解了多线程编程和线程同步机制,包括互斥锁、自旋锁和原子操作;
● 提升了对高并发系统设计的理解,特别是在缓存系统中的应用;
● 学习了如何通过性能分析和优化来提升系统在高并发场景下的表现;
● 增强了对C++语言在系统级编程中的运用能力,特别是在内存管理和并发控制方面。
相关面试题
一、基础原理相关
1、为什么需要设计内存池?与系统默认的内存分配器相比,有哪些优势?
2、内存对齐的意义是什么?在项目中是如何实现内存对齐的?
■避免未对齐访问引发性能问题。
■ 减少伪共享,优化 CPU 缓存性能。
■ 使用 ALIGNMENT 参数(如 8 字节对齐)实现对齐,通过 (bytes + ALIGNMENT - 1) / ALIGNMENT - 1或类似方式计算对齐后的大小。
3、如何避免内存池内的内存碎片化?
二、并发与线程安全相关
1、如何实现线程安全的内存池?在项目中使用了哪些同步机制?
2、无锁队列的实现原理是什么?项目中的无锁数据结构如何工作?
无锁队列(Lock-Free Queue)是一种并发数据结构,它允许多个线程或进程在没有锁的情况下进行数据的插入和删除操作。无锁数据结构的关键特点是:在多线程环境下,多个线程可以并发地访问和修改数据结构,而不会导致线程阻塞或等待锁的释放,从而避免了死锁、锁竞争等问题,减少了上下文切换的开销
3、你在项目中提到自旋锁和 std::this_thread::yield,请解释它们的作用及适用场景。
4、伪共享问题是什么?如何避免伪共享?
伪共享问题是什么?
伪共享(False Sharing) 是一种性能问题,发生在多核处理器的缓存系统中,尤其是涉及到多个线程并发操作共享内存时。它并不是一个实际的共享问题,而是因为多个线程频繁地访问并修改位于同一缓存行(cache line)中的数据,导致缓存一致性协议(如 MESI 协议)频繁地无效化缓存,从而造成性能的下降。
缓存行(Cache Line):
缓存行是处理器缓存的基本单位,通常大小为 64 字节(这个大小依赖于具体的硬件架构)。
当一个线程修改内存中的一个数据时,该数据所在的缓存行会被加载到该线程所在的核心的缓存中。如果其他线程也访问同一个缓存行的其他数据,这会导致缓存一致性协议(例如 MESI 协议)介入,强制将缓存行同步到内存或者其他缓存中。
伪共享的根本原因:
当多个线程在 同一个缓存行 中的 不同位置 频繁读写数据时,这些线程之间并不会直接共享数据,但由于它们访问的是同一个缓存行,导致缓存失效和同步操作,从而增加了处理器缓存的一致性开销。这种现象被称为“伪共享”。
伪共享会显著降低多线程程序的性能,因为每次数据写入都会使缓存行失效,迫使其他线程从主内存重新加载缓存行,从而浪费了大量的 CPU 资源。
三、项目设计相关
1、为什么选择三层架构(ThreadCache、CentralCache 和 PageCache)?每一层的职责是什么?
2、动态批量分配策略是如何设计的?它是如何根据使用模式调整的?
■ 根据分配请求的频率和内存块大小调整批量分配的数量。
■ 高频分配增大批量分配大小,低频分配减少批量分配大小
3、智能预取机制的实现原理是什么?如何优化内存分配延迟?
在分配请求前,提前准备好一批内存块,减少等待时间
四、性能优化相关
1、如何测试内存池的性能?测试中发现了哪些瓶颈,又是如何优化的?
2、高并发场景下,如何确保内存池的性能优于系统内存分配器?
五、C++语言相关
1、在项目中用到了哪些 C++ 特性?为什么选择这些特性?
RAII(Resource Acquisition Is Initialization)是C++中的一种编程惯用法,旨在通过对象的生命周期管理资源的分配与释放。RAII的核心思想是将资源的获取与对象的生命周期绑定在一起,从而确保资源在对象销毁时自动释放。
RAII的基本概念
RAII将资源(如内存、文件句柄、网络连接等)的获取和释放交给对象的构造函数和析构函数来管理。对象的构造函数负责资源的初始化,而析构函数负责资源的释放。因此,当对象的生命周期结束时(如超出作用域或被销毁),资源会自动释放,从而避免了资源泄露和未释放资源的问题。
以下是一些常见的RAII应用实例,涵盖了内存管理、锁机制和网络连接等方面:
2、原子操作(std::atomic)与互斥锁的区别是什么?项目中什么时候使用原子操作,什么时候使用锁?
3、在高并发场景中,compare_exchange_weak 和 compare_exchange_strong 有什么区别?为什么选择 compare_exchange_weak?
六、开源与扩展相关
1、你的内存池如何适配不同大小的对象?是否支持大对象分配?
2、CAS操作是什么
CAS操作(Compare and Swap 或 Compare and Exchange)是一种原子操作,用于并发编程中保证数据的一致性和避免竞态条件。CAS操作的基本思想是:比较某个内存位置的当前值是否与期望的值相同,如果相同,则交换该内存位置的值为新的值,否则不做任何修改。
CAS操作是多线程并发编程中常用的技术,尤其在高并发的环境下非常重要。它通常用于实现无锁的数据结构和算法。
相关纯八股面试题
操作系统相关
1、什么是死锁,如何预防死锁?
死锁是指在多线程或多进程程序中,多个进程或线程在执行过程中,因争夺资源而造成一种相互等待的状态,从而导致程序无法继续执行。
2、介绍一下几种典型的锁?
常见的锁有互斥锁、自旋锁和基于这两种锁实现的读写锁、悲观锁和乐观锁。
- 互斥锁:是最常用的一种锁,用于对共享资源的互斥访问,当一个线程获取锁之后,其他线程再去获取锁就会进入阻塞等待状态,将其CPU资源释放,当该锁释放的时候,系统又将阻塞等待的线程唤醒,让其尝试去获取锁。
- 自旋锁:是一种忙等待的锁,当锁被其他线程所持有的情况下,该线程并不会因为获取不到锁而释放CPU资源,而是轮询查看锁是否能被获取。相较于互斥锁,它减少了线程上下文切换带来的开销,当锁的粒度较小,加锁时间短的情况,建议使用自旋锁,因为它减少了上下文切换带来的性能开销。但是如果锁的粒度较大,加锁时间长的情况,建议使用互斥锁,因为轮询时间过长会造成CPU资源的浪费。
- 读写锁:是一种读(共享)写(排他)的锁机制,当一个线程加读锁访问共享资源时,允许其他线程加读锁读取共享资源,但是不允许加写锁。当线程加写锁访问共享资源时,不允许其他线程加任何锁访问共享资源。
- 悲观锁:悲观锁默认多线程会同时对共享资源进行修改,所以先加锁,再访问共享资源。互斥锁、自旋锁、读写锁都是悲观锁。
- 乐观锁:乐观锁默认多线程同时修改共享资源的概率较小,所以先修改共享资源,当发现共享资源被其他线程同时修改了以后则退出当前操作,重新执行修改。通常使用CAS机制,像无锁机制、无锁数据结构就是使用了乐观锁的思想。这种思想实现的无锁数据结构可能在高并发访问的情况下,性能高过悲观锁。
3、解释一下用户态和核心态?
- 用户态:用户态是CPU运行用户程序的一种模式,权限较低,不能直接访问硬件资源;用户态需要通过系统调用(System Call)请求内核态的服务。
- 内核态:内核态是CPU运行操作系统内核的一种模式,拥有最高权限,可以直接访问硬件资源;内核态负责管理系统的核心功能,如进程调度、内存管理、设备驱动等。
C++语言相关
1、vector容器如何进行动态内存的分配和管理?
创建时可指定初始大小分配内存;插入元素超容量时,按一定规则(如原容量 2 倍)分配新内存、拷贝数据、释放旧内存;可通过swap()或shrink_to_fit()释放多余内存;用reserve()预分配内存避免频繁扩容。
2、什么是内存池?它如何帮助优化内存使用?
内存池是一种内存管理技术,它预先分配一大块内存,然后按需分配小块内存给程序使用,以此来优化内存分配和回收的性能。
3、解释C++中的内存碎片及其影响。
C++中的内存碎片是指内存中存在的小块空闲空间,这些空间无法被有效利用,导致内存利用率下降,可能会影响程序性能和内存分配效率。
内存对齐可能会导致每次分配的内存都比实际请求的更多,尤其是在堆中频繁分配和释放内存时。这样,即使分配的内存块很小,也可能由于对齐要求造成内存的浪费,从而导致内存碎片的增多。
内部碎片与外部碎片的定义
内部碎片:发生在一个已分配内存块内部,指的是在分配内存时,由于内存块的大小超过了实际需要的大小而造成的浪费。它通常是由于内存分配的单位大小不完全匹配请求的内存需求,导致分配了比需要更多的内存,剩余部分未被使用。
外部碎片:发生在一块内存区域中,指的是内存中的空闲区域由于多次分配和释放内存导致的不连续的空闲空间。这些不连续的空闲区域可能无法有效地利用,因为它们的大小和位置不适合满足新的内存请求。
内存对齐通常会导致内部碎片,因为对齐要求可能导致内存块比实际需要的更大,并且这些多余的空间无法被有效利用。
外部碎片通常是由于内存分配和释放的方式造成的空闲内存区域变得不连续,与内存对齐本身的影响关系不大。
4、请解释C++中的内存对齐和填充。
没有对齐可能会增加内存访问次数
5、什么是内存池分配器?它如何工作?
预先分配一大块内存,按需从内存块上分配和回收内存 优点是减少内存碎片 提高内存分配回收的效率 减少内存分配回收时的系统调用
6、如何在C++中实现一个简单的内存池?
一次性分配一大块内存,将内存划分成小块,将这些小块内存分配给用户 将释放的内存,归还给内存池
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!