当前位置: 首页 > news >正文

vector之内存分配详解

本文想讨论的东西

vector作为一种常见的容器,我们在日常的C++开发中,非常的常见,但是对于vector的使用过程中的内存分配情况,包括虚拟内存和物理内存的分配,感觉不是特别的清晰,作者也是想写一个demo,和大家一起来学习,详细分析vector在使用过程中的内存分配情况。

一些需要掌握的铺垫知识

vector 中的size,capacity, shrink_to_fit方法

size是反应vector中现在的元素个数
capacity是最多能存放多少个元素,如果超过这个capacity就会扩容,所以为了避免频繁的扩容,而带来的拷贝开销,我们会
使用reserve方法
shrink_to_fit 是收缩内存到我们的size的大小,也就是会减少capacity到size大小,也就是会回收物理内存

关于linux系统中的查看内存的指令

我们通常使用 pmap -X pid 指令来把某个进程的虚拟内存使用和rss(实际使用物理内存进行展示)

关于brk和mmap

这里只是简单的介绍下,glibc的这个库里有两个调用常用来进行分配和释放虚拟内存,malloc和free,malloc会进行调用brk或者是mmap来分配内存,而free会通过调用 brk和munmap来释放虚拟内存内存。
一般情况下 小内存,小于128k的时候,会brk,
大内存,大于128k的时候用 mmap.
而在malloc以后,不会马上的分配物理内存,而是当第一次使用虚拟的内存的时候,触发内核的page fault来分配物理内存。

而在free以后,如果是调用了munmap会立即的回收物理内存
而如果调用的是brk, 系统可能不会马上回收物理内存。

关于new,delete, malloc,free

这里其它区别我不再赘述,只是想说一点,是否会触发分配物理内存的情况
new:会有两步操作,第一步是 调用malloc分配虚拟内存,第二步是调用对应的构造函数,如果构造函数中有对成员变量的赋值操作,也就是有对其的内存进行实际的使用,就会触发内核的缺页中断,进行物理内存分配。
delete:会有两步操作,第一步是 调用对应的析构函数,然后第二步是调用 free. 然后至于free操作,会不会释放物理内存就要看调用的是brk还是munmap了。

关于strace指令

strace 就是systemcall trace也就是系统调用跟踪,也就是当我们进行系统调用的时候,会被这个指令跟踪下来,我们这里涉及到的系统调用主要是,brk,mmap,munmap.

关于pod类型

本文刚开始会讨论vector里存储pod类型,然后再接下来讨论vector里存储非pod类型,所以我们要介绍下什么是pod类型
所谓pod类型就是 (Plain Old Data,即“普通旧数据类型”)是一种特殊的类型分类,它表示简单的、与 C 语言兼容的数据结构。
它有两个特点:
1.平凡可复制
所以的平凡可复制就是,对象的内存布局是连续的,可以直接用 memcpy 安全地复制
满足以下条件:
默认构造函数、拷贝构造函数、拷贝赋值运算符、析构函数是 ​自动生成的​(或显式标记为 = default)。
没有虚函数或虚基类。
2.标准布局(Standard Layout)​
内存布局与 C 语言兼容,可以直接与其他语言(如 C)交互。
满足以下条件:
所有非静态成员变量具有相同的访问权限(如全部 public)。
没有基类(或所有基类都是空类且第一个成员是非静态成员)。
没有引用类型的成员。
类中最多有一个类有非静态成员变量。

代码例子(POD)

#include <cstddef>
#include <iostream>
#include <unistd.h>
#include <vector>


void pause(const char* msg){
    std::cout << "Pause " << msg << " pid of this process " << getpid() <<std::endl;
    std::cout << "Run pmap -X " << getpid() << std::endl;
    std::cin.get(); // press enter to continue.
}

int main() {

    std::vector<int> test_vector;
    std::cout<<"before reserve size:"<< test_vector.size() << "capacity:" << test_vector.capacity()<<std::endl;
    pause("before reserve 100 000 000");

    //stage 1 virtual memory
    test_vector.reserve(100000000);
    std::cout<<"after reserve size:"<< test_vector.size() << "capacity:" << test_vector.capacity()<<std::endl;
    pause("after reserve 100 000 000");

    //state 2 use virtual memory for physical memory use
    for(size_t i=0; i<100000000; i++){
        test_vector.push_back(12);
    }
    std::cout<<"after writing data size:"<< test_vector.size() << "capacity:" << test_vector.capacity()<<std::endl;
    pause("after writing data");

    //state 3 clear vector
    test_vector.clear();
    std::cout<<"after clear size:"<< test_vector.size() << "capacity:" << test_vector.capacity()<<std::endl;
    pause("after clear vector");

    //state 4 shrink to fit
    test_vector.shrink_to_fit();
    std::cout<<"after shrink_to_fit size:"<< test_vector.size() << "capacity:" << test_vector.capacity()<<std::endl;
    pause("after shrink_to_fit");

    return 0;
}

我们设计了一个代码,自定义了一个pause函数,这个函数,会暂停进程,并且给出提示信息,执行pmap 命令,然后我们可以看到不同的阶段,对应的console控制台打印,以及pmap的输出。
程序编译指令:
g++ -static -std=c++11 -O0 test1.cpp -o test1
程序执行指令,用 strace进行跟踪系统调用情况:
strace -e brk,mmap,munmap ./test1

下面我么具体讲解每一个步骤,并且给出程序的输入截图:

1.reserve之前

before reserve
在这里插入图片描述

2.after reserve

在这里插入图片描述
在这里插入图片描述

3.after writing data

在这里插入图片描述
在这里插入图片描述

4.after clear size

在这里插入图片描述
在这里插入图片描述

5.after shrink_to_fit

在这里插入图片描述
在这里插入图片描述

代码例子(非POD)

#include <cstddef>
#include <iostream>
#include <unistd.h>
#include <vector>
#include <malloc.h>


class TestClass{
    public:
    TestClass(){
        //std::cout<<"construct enter"<<std::endl;
    }
    ~TestClass(){
        // std::cout<<"desconstruct enter"<<std::endl;
    }
    private:
    int a =10;
    int b=100;
    std::string c = "1234141324dgfdsgdffdakdfjkaljfkla";
};

void pause(const char* msg){
    malloc_trim(0);
    std::cout << "Pause " << msg << " pid of this process " << getpid() <<std::endl;
    std::cout << "Run pmap -X " << getpid() << std::endl;
    std::cin.get(); // press enter to continue.
}

int main() {

    std::vector<TestClass> test_vector;
    std::cout<< "size of std::string:"<< sizeof(std::string) << std::endl;
    std::cout<< "size of TestClass:"<< sizeof(TestClass) << std::endl;
    std::cout<<"before reserve size:"<< test_vector.size() << "capacity:" << test_vector.capacity()<<std::endl;
    pause("before reserve 100 000 000");

    //stage 1 virtual memory
    test_vector.reserve(100000000);
    std::cout<<"after reserve size:"<< test_vector.size() << "capacity:" << test_vector.capacity()<<std::endl;
    pause("after reserve 100 000 000");

    //state 2 use virtual memory for physical memory use
    for(size_t i=0; i<100000000; i++){
        test_vector.push_back(std::move(TestClass()));
    }
    std::cout<<"after writing data size:"<< test_vector.size() << "capacity:" << test_vector.capacity()<<std::endl;
    pause("after writing data");

    //state 3 clear vector
    test_vector.clear();
    std::cout<<"after clear size:"<< test_vector.size() << "capacity:" << test_vector.capacity()<<std::endl;
    pause("after clear vector");

    // //state 4 after clear, continue push back data
    // for(size_t i=0; i<100000000; i++){
    //     test_vector.push_back(std::move(TestClass()));
    // }
    // std::cout<<"after second writing data size:"<< test_vector.size() << "capacity:" << test_vector.capacity()<<std::endl;
    // pause("second time after writing data");

    //state 5  shrink to fit
    test_vector.shrink_to_fit();
    std::cout<<"after shrink_to_fit size:"<< test_vector.size() << "capacity:" << test_vector.capacity()<<std::endl;
    pause("after shrink_to_fit");

    return 0;
}

编译代码:g++ -static -std=c++11 -O0 test1.cpp -o test1
执行代码:strace -e brk,mmap,munmap ./test1

1.before reserve

在这里插入图片描述
在这里插入图片描述

2.after reserve

在reserve的时候,因为数组的大小比较大,所以会触发用mmap分配到匿名区域,一会用pmap指令可以看到
这里要区分堆区域,小内存会被用brk分配到堆区域
在这里插入图片描述
在这里插入图片描述

3.after writing data

这里和平凡的相比,不单增加了物理内存,也会增加虚拟内存
在这里插入图片描述
在这里插入图片描述

4.after clear

clear以后,由于我们的类里有std::string,会触发析构函数的执行,string的析构函数里,会释放掉堆上的内存,所以这个时候,clear会减少堆上的内存,这个是和平凡类型不一样的地方。
同时也需要注意,这个非POD的内存也不一定会马上被释放,如果其他进程不用的话,可能还会继续保留,之所以我这里马上释放,是因为我在代码里加了强制回收物理内存。
void pause(const char* msg){
malloc_trim(0);
std::cout << "Pause " << msg << " pid of this process " << getpid() <<std::endl;
std::cout << "Run pmap -X " << getpid() << std::endl;
std::cin.get(); // press enter to continue.
}

在这里插入图片描述
在这里插入图片描述

5.after shrink_to_fit

在这里插入图片描述
在这里插入图片描述

总结

本文讨论的初衷是想了解vector 的整个声明过程中对于内存的使用情况,特别是物理内存,便于我们对程序代码进行优化,这里主要有两种优化。

1.优化内存使用

如果我们的代码是在内存资源比较紧张的环境,则需要再使用完vector后,进行及时的shrink_to_fit清理内存,这个时候就会导致再次使用的时候又需要重新申请虚拟内存,以及要使用的时候触发page fault来进行分配物理内存,这个就会比较耗时。

2.减少程序的执行时间

这个在我们资源比较充足的情况下,我们可以拿空间换时间,只是调用clear,然后后续使用vector 的时候,直接使用原有的内存就可以了,不必要每次都清理掉物理内存

3.非POD的情况特殊

因为非POD可能会导致clear的时候会释放物理内存,所以用clear可能就不太行了,就需要自己实现内存池

http://www.dtcms.com/a/99882.html

相关文章:

  • 23 种设计模式中的迭代器模式
  • Three.js 快速入门教程【十九】CSS2DRenderer(CSS2D渲染器)介绍,实现场景中物体或设备标注标签信息
  • QML中刷新图片的三种方法对比分析
  • [ComfyUI] 如何升级自定义节点(Custom Nodes)
  • 计算机网络和因特网
  • AGI 的概念、意义与未来展望
  • 【AI论文】挑战推理的边界:大型语言模型的数学基准测试
  • Keepass恢复明文主密码漏洞(CVE-2023-3278)复现与hashcat爆破学习
  • Array数组常用方法总结(javascript版)
  • SpringBoot的自动装配原理
  • Redis-常用命令
  • Spring 过滤器(Filter)和过滤器链(Filter Chain)完整示例,包含多个过滤器和Filter 生命周期
  • 简单介绍一下Unity中的material和sharedMaterial
  • PipeWire 音频设计与实现分析三——日志子系统
  • vxe-table 设置单元格可编辑无效问题解决
  • 网络传输优化之多路复用与解复用
  • 流动的梦境:GPT-4o 的自回归图像生成深度解析
  • 聚焦应用常用功能,提升用户体验与分发效率
  • 桥接模式_结构型_GOF23
  • day17 学习笔记
  • Gateway实战入门(四)、断言-请求头以及请求权重分流等
  • Kafka 多线程开发消费者实例
  • 第四天 文件操作(文本/CSV/JSON) - 异常处理机制 - 练习:日志文件分析器
  • 【Python】基于 qwen_agent 构建 AI 绘画智能助手
  • Linux 文件存储和删除原理
  • Unity编辑器功能及拓展(2) —Gizmos编辑器绘制功能
  • Kafka消息丢失全解析!原因、预防与解决方案
  • 如何查看 SQL Server 的兼容性级别
  • 基于ruoyi快速开发平台搭建----超市仓库管理(修改记录1)
  • 《C++11:通过thread类编写C++多线程程序》