找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3777

积分

0

好友

520

主题
发表于 昨天 03:41 | 查看: 4| 回复: 0

因为那样写出来的程序,连三个月都活不过。

我带团队十几年,最烦两种人:一种是动不动就说“malloc够快了,别搞什么分配器”;另一种就是上来就重载全局 operator new,结果把整个系统搞崩溃。

内存碎片问题示意图

在我看来,C++ 的内存分配器不是为了复杂,而是为了让软件在资源有限的环境下活得更久

一、C 和 C++ 内存分配的本质区别

C 的 malloc 像个简单的搬运工:你要 100 字节,它就给你 100 字节的空间。它不关心这块内存用来做什么,也不负责管理对象的生死。在编写脚本或小工具时,这种方式完全够用。

但 C++ 的核心是对象。new MyClass() 所做的不仅仅是分配内存,它还必须要调用构造函数来初始化对象;相应的,delete p 也不仅仅是回收内存,它必须得先调用析构函数来清理资源。

如果这个“构造-析构”的生命周期被打断了——比如文件没关闭、锁没释放,或者加密上下文泄漏了——系统很可能在半夜就挂掉。

C++ 必须保证对象生命周期的完整性,这是 RAII 原则的根基

而标准库中的 std::allocator 所做的事情,其实只有一件:提供原始的、未经初始化的内存。它不负责构造对象,不负责析构对象,也不操心你的对象逻辑。把内存管理和对象生命周期解耦,这正是 C++ 设计的高明之处。

二、那为什么不能每次都直接问操作系统要内存

因为 malloc 在长期运行的程序中,会悄无声息地把你埋进坑里

很多人以为 malloc(128) 就是向操作系统申请 128 字节,其实远非如此。以 glibc 的实现为例,它可能会为了对齐和存放管理元数据,把你的请求塞进一个 192 字节的内存块(bin)里,这就产生了 接近 50% 的内部碎片。如果你的程序频繁地 new/delete 不同大小的对象,堆上的空闲内存就会逐渐变成一堆“空洞”,这就是外部碎片。

最终导致的结果是:即使系统总的空闲内存还有 50MB,当程序需要一块连续的 512KB 来处理一个大请求时,malloc 可能因为找不到足够大的连续空间而失败,返回 nullptr,程序随之崩溃。而此时用 free 命令查看,系统内存明明还很充裕,问题极其隐蔽,难以定位。

我在做边缘设备开发时就吃过这个大亏。一台只有 256MB RAM 的终端设备,运行 QR 码解析服务,前三周稳如磐石,第四周开始随机崩溃。日志里只有孤零零的一行 std::bad_alloc。排查了整个通宵,最终才发现是 malloc 产生的外部碎片把堆空间彻底“割裂”了。

三、解决方案:不动全局,只动热点

我们的 QR 码解析模块中,有一个 DecodedSymbol 结构体,大小固定为 128 字节,且在同一会话中的数量有限(最多 32 个)。于是,我们为它量身定制了一个固定大小的内存池分配器:

class SymbolAlloc {
    static constexpr size_t SZ = 128;
    struct Node { Node* next; };
    Node* free_ = nullptr;
    std::vector<char> pool_;
public:
    SymbolAlloc() : pool_(4096) {
        int n = 4096 / SZ;
        free_ = (Node*)pool_.data();
        for (int i = 1; i < n; ++i)
            ((Node*)(pool_.data() + i*SZ))->next = (Node*)(pool_.data() + (i+1)*SZ);
        ((Node*)(pool_.data() + (n-1)*SZ))->next = nullptr;
    }
    void* alloc(){
        if (!free_) return nullptr; // 已知上限,不扩容
        auto p = free_;
        free_ = free_->next;
        return p;
    }
    void free(void* p){
        ((Node*)p)->next = free_;
        free_ = (Node*)p;
    }
};

我们的策略是:每个扫码会话独立创建这样一个分配器,会话结束就将其销毁。所有 DecodedSymbol 对象都从这个内存池中分配和释放,完全不触及全局堆,也就不会产生跨会话的内存碎片

这个方案上线后,设备稳定运行了超过 18 个月,没有再出现因内存问题导致的崩溃,完全满足了企业级软件对稳定性的苛刻要求。而对比之前的版本,平均每两个月就会崩溃一次。

当然,在动手写自定义分配器之前,我们也尝试过 tcmalloc 这类通用的高性能分配器。它确实比 glibc 的 malloc 优秀很多,能显著改善碎片问题。但在对象模式高度集中、大小完全固定的特定场景下,手写的专用分配器在空间利用率和速度上依然更具优势。

我们团队在开发时遵循一个核心原则:先用性能分析工具定位真正的内存热点,再针对这些热点进行局部的分配器替换,绝不轻易重载全局的 operator new

说到底,操作系统 并非不做内存管理,而是它无从知晓你的对象具体多大、会存活多久、哪些对象需要被放在一起以提高效率。它的策略是通用且公平的,而你的高性能程序需要的,往往是精准而高效的控制。

你可以选择不用自定义内存分配器。但当你的服务在客户现场深夜崩溃,日志里只有一句 bad_alloc,而所有监控都显示内存依然充裕时,你就会深刻地体会到:当初为了省事而少写的那几行代码,最终需要用十倍甚至百倍的人力去填坑

而你,作为代码的负责人,永远是那个无法逃避责任的对象。在云栈社区,我们经常探讨类似的高性能底层问题,因为只有理解这些,才能写出真正健壮、可靠的系统。




上一篇:从VGG到ResNet:CNN架构演进与五大训练实用技巧详解
下一篇:大模型时代的技术护城河:别在融化的冰面上盖房子
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-25 09:10 , Processed in 2.215990 second(s), 45 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表