目录
new一定会陷入内核态么
先说结论,new
操作符并不一定每次都会导致程序进入内核态。因为malloc会在用户态进行缓存内存块,对于小内存的申请直接从用户态拿,只有当缓存的内存块不够时才会调用brk(小于128k)或者mmap(大于128k)系统调用陷入内核态。
1. new
操作符的基本原理
在 C++ 中,new
操作符用于动态分配内存,其底层是通过调用标准库的内存分配函数(通常是 malloc
或自定义的内存分配器)来实现的。内存分配器(如 malloc
)管理内存时,通常使用两种系统调用:
brk
:调整程序数据段(堆区)大小来分配内存。mmap
:通过内存映射来分配较大的内存块。
这两种系统调用都需要进入内核态来申请或管理内存,因为它们需要操作系统进行虚拟内存管理(如修改进程地址空间、分配虚拟内存页等)。
2. 什么时候 new
会进入内核态?
- 当需要向操作系统申请新内存时,
new
会调用底层的malloc
或operator new
,而malloc
会通过brk
或mmap
系统调用来分配内存。因为brk
和mmap
都需要与内核进行交互,所以这时new
会导致进入内核态。 - 缺少足够的空闲内存时,例如当前的内存池(Heap Pool)中没有足够的内存空间时,
new
需要请求新的内存,从而进入内核态。
3. 什么时候 new
不会进入内核态?
-
内存复用时:如果
new
调用的内存分配器(如malloc
或自定义内存池)在用户态已经管理了一块空闲的内存区域,那么直接从用户态的内存池中分配内存即可,不需要额外调用brk
或mmap
,因此不会陷入内核态。 -
小块内存分配时:许多内存分配器会在用户态维护一个小内存池(如
tcmalloc
、jemalloc
、ptmalloc
等)。当分配小块内存时,如果内存池中已经有足够的内存块,则不会调用系统调用,而是直接在用户态进行分配,从而不会进入内核态。
4. 内存池管理机制避免进入内核态
在高性能场景中,用户通常使用内存池来管理动态内存。内存池通过一次性向操作系统申请较大的内存块,并在用户态进行分配和回收,减少频繁的系统调用,从而避免程序频繁进入内核态。
-
内存池的工作原理:
- 一次性向操作系统申请一大块内存(可能使用
mmap
或brk
),然后将这块内存在用户态划分成多个小块进行管理。 - 当程序调用
new
进行内存分配时,直接从内存池中取出空闲内存块,而不需要频繁进行系统调用。
- 一次性向操作系统申请一大块内存(可能使用
-
优点:
- 避免了频繁的内存分配导致的上下文切换,从而显著提高程序的执行效率。
- 减少了内存碎片化问题,因为内存池可以更加精细地管理内存分配和回收。
5. new
使用内存池的场景
如果内存分配器使用了内存池或其他优化策略(例如 tcmalloc
、jemalloc
、ptmalloc
),new
的行为会更加高效:
-
多线程分配:如
tcmalloc
和jemalloc
,在多线程环境下为每个线程分配独立的线程本地内存池。这样,在多线程同时调用new
时,每个线程可以在用户态完成内存分配,从而避免频繁的锁竞争和内核态切换。 -
小对象优化:针对小对象(如 16 字节、32 字节)的频繁分配,通常使用对象池(Object Pool)或自由链表(Free List),从而显著提升小内存块分配的效率,避免进入内核态。
6. 总结
new
并不一定会进入内核态。只有在内存池耗尽或需要申请新内存时,new
会调用底层分配器(malloc
或operator new
),从而使用系统调用(brk
或mmap
)进入内核态。- 使用用户态内存池或高效的内存分配器(如
tcmalloc
、jemalloc
等),可以避免频繁的new
调用进入内核态,从而提高程序的整体性能。
那么问题来了:
内存分配器既然有用户态缓存 那么为什么我们常常还要额外实现一个内存池呢?
内存分配器在用户态的缓存确实可以在一定程度上减少进入内核态的频率,但在某些情况下,额外实现一个用户态内存池仍然是有必要的。以下是一些原因,解释为什么在高性能应用中使用内存池仍然是一个有效的策略:
1. 性能优化
-
减少分配和释放的开销:尽管分配器有用户态缓存,但频繁地调用
new
和delete
仍然会导致分配器维护内部数据结构的开销。内存池可以通过一次性分配一大块内存并进行内部管理,避免每次小块分配时的额外开销。 -
批量分配:内存池通常会支持批量分配,可以一次性分配多个对象,这在多次分配时能够显著提高性能。
2. 控制内存分配策略
-
定制分配策略:使用自定义内存池,开发者可以根据应用的特定需求来设计分配和释放策略。例如,某些应用程序可能需要在特定情况下快速释放内存,而内存池可以优化这种需求。
-
专用内存管理:内存池可以专门用于某种类型的对象,避免与其他类型的内存分配器干扰,从而减少内存碎片化和管理开销。
3. 避免内存碎片
- 内存碎片管理:内存池可以在用户态高效地管理对象的生命周期,减少内存碎片化问题。大多数通用内存分配器在多个不同大小的对象间分配内存,可能会导致内存碎片。而使用内存池可以将相同类型的对象集中在一起,降低碎片化的风险。
4. 并发性能
-
减少锁竞争:在多线程环境下,使用默认的内存分配器可能导致频繁的锁竞争。实现一个内存池,尤其是线程本地的内存池(每个线程有自己的内存池),可以显著减少锁竞争,提高并发性能。
-
无锁内存分配:某些内存池实现使用无锁算法进行分配,从而避免锁的开销,提高多线程环境下的性能。
5. 内存池的灵活性
-
灵活的内存管理:内存池允许开发者更灵活地控制内存的分配和释放。例如,可以预分配特定数量的对象,以减少运行时内存分配的不可预测性。
-
生命周期管理:内存池可以有效管理对象的生命周期,确保对象的分配和释放遵循特定的逻辑,降低内存泄漏的风险。
6. 性能可预测性
- 更好的性能可预测性:使用内存池时,分配和释放的时间开销通常更为可预测,因为它们是针对相同大小的对象进行的。这对实时系统或对延迟敏感的应用程序非常重要。
7. 提高缓存命中率
- 优化缓存使用:在内存池中分配的对象通常是相同大小的,这可以提高 CPU 缓存的命中率,进一步提升性能。
总结
虽然标准内存分配器的用户态缓存可以减少系统调用的频率,但在高性能应用中,自定义内存池能够提供更高效的内存管理,优化性能,减少内存碎片,提高并发处理能力,和提高整体内存使用的灵活性。因此,对于性能要求较高的应用程序,实现一个额外的内存池是非常有必要的。