C++ 笔记3:内存碎片化


Lambert 发布于 2024-12-13 / 3 阅读 / 0 评论 /
内存碎片化 内存池 参考:《ppp2e》Chap25 1.内存碎片化 动态内存分配存在的问题,就是内存碎片化。 p275
  • 内存碎片化

  • 内存池

参考:《ppp2e》Chap25

1.内存碎片化

动态内存分配存在的问题,就是内存碎片化。

p275

截屏2024-03-28 01.47.54

为什么 new/delete 会造成内存碎片化呢?

使用newdelete操作符进行动态内存分配和释放时,会导致内存碎片化的主要原因是频繁地分配和释放不同大小的内存块,而这些内存块在内存中的分布是不连续的。

具体来说,当使用new操作符动态分配内存时,操作系统会在进程的地址空间中找到一块足够大的连续空闲内存块,并将其分配给程序使用。但是,当程序使用delete操作符释放内存时,这块内存并不会被立即回收,而是被标记为可用状态。如果后续需要再次分配一个较大的连续内存块,而恰好前面已经被释放的内存空间无法满足需求,那么就会产生一块或多块不连续的空闲内存,这些不连续的空闲内存块就是内存碎片。

由于操作系统的内存分配算法通常是首次适配(First Fit)或最佳适配(Best Fit)等,它们往往会选择找到第一个满足条件的空闲内存块来分配,而不会考虑是否存在更好的连续空闲内存块。这就导致了内存碎片化的问题,因为即使总的空闲内存量可能足够,但由于这些空闲内存是分散的,无法满足需要分配的较大连续内存块的需求。

此外,频繁地进行内存分配和释放操作也会增加操作系统的系统调用开销,因为每次分配和释放内存都需要涉及到操作系统的内存管理机制。系统调用的开销包括上下文切换、内核态与用户态之间的切换等,会影响程序的性能和效率。

简单说,new/delete 会涉及到系统调用,这样做相对有较大开销。

2.存储池

存储池(Memory Pool)是一种内存管理技术,它预先分配一块连续的内存空间,然后将其划分成多个固定大小的内存块,这些内存块可以被程序动态地分配和释放。存储池技术旨在提高内存分配和释放的效率,并减少内存碎片化问题。

可以这样简单理解,存储池就是预先申请,而 new/delete 是次次都要,这就“显得很烦”。

2.1使用场景

存储池的使用场景包括:

  • 对象池:用于管理大量相同类型的对象,例如游戏中的子弹、粒子等。通过使用存储池,可以减少动态分配和释放对象所带来的开销,提高性能。

  • 网络连接池:在服务器应用中,经常需要管理大量的网络连接。使用存储池可以有效地管理这些连接,提高系统的并发性能。

  • 内存池:一些应用程序可能需要频繁地分配和释放大量小型内存块,例如字符串、缓冲区等。通过使用存储池,可以减少内存碎片化,提高内存使用效率。

存储池的主要步骤包括:

  1. 初始化存储池:在程序启动时,预先分配一块连续的内存空间,并将其划分成多个固定大小的内存块。

  2. 动态分配内存:当程序需要分配内存时,从存储池中获取一个合适大小的内存块,并将其标记为已分配。

  3. 释放内存:当不再需要分配的内存时,将其标记为未分配,但实际上并不释放内存块本身,而是将其重新放回存储池中以供后续使用。

2.2 使用纯链表实现实现一个存储池

这段代码有个很有意思的点,结构体定义在外,然后在类中使用了结构体:


// Define a node structure for the linked list

struct  Node {

void* data; // Pointer to the data

Node* next; // Pointer to the next node

};

  

// Define a memory pool class

class  MemoryPool {

private:

// 这个类使用了结构体来创建链表

Node* head; // Pointer to the head of the linked list

  

这种设计模式的优点是,**它将数据的存储(Node结构体)和数据的操作(MemoryPool类)分离开来,使得代码更加清晰和易于理解。**此外,通过使用链表来管理内存,MemoryPool类可以有效地重用已经释放的内存,从而提高内存的使用效率。

再看main函数部分:


#include  "mempool.hpp"

  

int main() {

// Create a memory pool object

MemoryPool pool;

  

// Use the memory pool to allocate and deallocate memory blocks

int* num1 = static_cast<int*>(pool.allocate(sizeof(int)));

*num1 = 10; // Initialize the integer,将10存储在num1所指向的内存地址中

std::cout << *num1 << std::endl;

  

float* num2 = static_cast<float*>(pool.allocate(sizeof(float)));

*num2 = 3.14f;

std::cout << *num2 << std::endl;

  

pool.deallocate(num1);

pool.deallocate(num2);

  

return  0;

}

2.3可视化详解这个demo

第一,在程序运行前先给类对象和变量申请空间,而后类对象成员变量进行初始化,

截屏2024-03-28 14.09.05

第二,allocate是是指向类对象的

截屏2024-03-28 14.15.26

第三,调用销毁函数

在 pythontutor 上进行可视化操作的时候,有如下报错:

截屏2024-03-28 14.50.55

Here is the stack trace with the currently-executing function at the top:

  • MemoryPool::deallocate(void*) at line 56
  • main at line 77

ERROR: Invalid write of size 8

(Stopped running after the first error. Please fix your code.)

“Invalid write of size 8” 错误通常表示你正在尝试 写入8个字节大小的数据到一个无效的地址或者一个未分配的内存块 。这种错误通常与指针操作有关,例如尝试解引用一个空指针,或者将一个非法地址赋值给指针。

在你的代码中,错误发生在 MemoryPool::deallocate(void*) 函数的第56行。这意味着在这一行尝试写入8个字节的数据到一个无效的地址。

由此产生下面的问题:我们怎么确保 [deallocate](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)函数的ptr是有效的,并且它指向的内存块是由[MemoryPool](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)分配的? 我也不知道,这里卡住太久,现在依然不会。


void deallocate(void*  ptr) {

// Check if ptr is nullptr

if (ptr == nullptr) {

throw std::runtime_error("Attempt to deallocate a null pointer");

}

  

// Cast the pointer to Node* and add it to the linked list of free blocks

Node* block = static_cast<Node*>(ptr);

block->next = head;

head = block;

}

我在想是不是我想复杂了,qwq。明显,block和ptr指向的是同一块内存。

截屏2024-03-28 15.50.45

Invalid write of size 8

这个bug我还挺迷的,我本地反正可以跑

截屏2024-03-28 15.45.32

最开始怀疑是使用了C++17编译,后来换到C++20还是可以编译通过。

最后,这部分在本机编译通过,给出通过代码,


#include  <iostream>

  

// Define a node structure for the linked list

struct  Node {

void* data; // Pointer to the data

Node* next; // Pointer to the next node

};

  

// Define a memory pool class

class  MemoryPool {

private:

// 这个类使用了结构体来创建链表

Node* head; // Pointer to the head of the linked list

  

public:

// Constructor

// 类成员变量 head 在构造函数中初始化为 nullptr,表示链表为空

MemoryPool() {

head = nullptr;

}

  

// Destructor

~MemoryPool() {

// Free all the memory blocks in the linked list

// 遍历释放所有的内存块

Node* current = head;

while (current != nullptr) {

Node* temp = current;

current = current->next;

delete temp;

}

}

  

// Allocate a memory block

void* allocate(size_t  size) {

// Check if there is a free block available in the linked list

if (head != nullptr) {

// Use the free block,

// 如果链表中有可用的内存块,这两行代码将取出链表的头节点,并将头节点移动到下一个节点

Node* block = head; // 暂存头节点

//block is a local variable, and it points to the head of the linked list

head = head->next;

// Return the data pointer,这个指针指向的就是分配的内存

return block->data;

}

  

// If no free block is available, allocate a new block

return  new  char[size];

}

  

void deallocate(void*  ptr) {

// Check if ptr is nullptr

if (ptr == nullptr) {

throw std::runtime_error("Attempt to deallocate a null pointer");

}

  

// Cast the pointer to Node* and add it to the linked list of free blocks

Node* block = static_cast<Node*>(ptr);

block->next = head;

head = block;

}

  

};

  
  
  

int main() {

// Create a memory pool object

MemoryPool pool;

  

// Use the memory pool to allocate and deallocate memory blocks

int* num1 = static_cast<int*>(pool.allocate(sizeof(int)));

*num1 = 10; // Initialize the integer,将10存储在num1所指向的内存地址中

std::cout << *num1 << std::endl;

  

pool.deallocate(num1);

  
  

return  0;

}

2.4使用容器实现存储池

在刚刚经历完使用纯链表实现存储池后,我们知道在纯链表实现的过程中,首先需要在类外定义链表的结构体,而后在类中去使用结构体,而后又是复杂定义分配和销毁。总之,debug的我血压都上去了。

现在,我们来看看STL中最常使用的vector容器,可以用舒服来形容,


#include  <iostream>

#include  <vector>  // 包含STL向量容器头文件

  

// Define a memory pool class

class  MemoryPool {

private:

std::vector<void*> freeBlocks; // 使用std::vector存储空闲内存块的指针

  

public:

// Constructor

MemoryPool() {}

  

// Destructor

~MemoryPool() {

// 释放所有的内存块

for (void* ptr : freeBlocks) {

delete[]  static_cast<char*>(ptr);

}

}

  

// Allocate a memory block

void* allocate(size_t  size) {

// 检查是否有空闲的内存块可用

if (!freeBlocks.empty()) {

// 使用空闲的内存块

void* ptr = freeBlocks.back();

freeBlocks.pop_back();

return ptr;

}

  

// 如果没有空闲内存块,分配新的内存块

return  new  char[size];

}

  

// Deallocate a memory block

void deallocate(void*  ptr) {

// 将要释放的内存块添加到空闲内存块列表的末尾

freeBlocks.push_back(ptr);

}

};

  

int main() {

// 创建内存池对象

MemoryPool pool;

  

// 使用内存池分配和释放内存块

int* num1 = static_cast<int*>(pool.allocate(sizeof(int)));

*num1 = 10; // 初始化整数

std::cout << *num1 << std::endl;

  

pool.deallocate(num1);

  

return  0;

}

我们进入析构函数看看是如何delete的:

截屏2024-03-28 16.49.42

而freeBlocks是是类对象的成员变量,在进入析构函数当 MemoryPool 对象被销毁时,freeBlocks也随之销毁。

截屏2024-03-28 16.54.01


更新于:2023年3月29日



是否对你有帮助?

评论