V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
ligiggy
V2EX  ›  C++

C++动态内存管理问题求解

  •  
  •   ligiggy · 2022-06-29 11:30:17 +08:00 · 3809 次点击
    这是一个创建于 639 天前的主题,其中的信息可能已经有所发展或是发生改变。

    项目上需要处理若干组,每组 500M 左右的数据,数据组成是大概可以理解为 3 个 std::vector<float>,一个 std::vector<structA>( structA 为自定义结构体),每处理一组数据就需要释放掉。

    数据处理大概包括:插值,平移等。

    由于载入内存比较大,导致处理的时间越来越长,内存越来越碎片化。

    找了几个内存池的解决方案,好像不是很好解决我的问题。比如 boost::pool ,std::allocator ,使用起来都比较麻烦,比如 boost ,很多释放都是静态的,allocator 的话,基本上需要重新造轮子。后面发现 c++17 添加了 pmr::monotonic buffer resource ,尝试 debug 几次之后,发现在现在的机器上一次只能分配 100M 的内存,200M 和 500M ,都会在运行的时候崩溃掉,应该是没有那么多的连续内存了,想问下大佬们,有什么推荐的解决方案(轮子)吗?

    我期望中的解决方案其实与 pmr 的预期类似,就是我申请一块足够大的连续内存,让这块内存分配数据的存储空间,处理完后,直接将整块内存释放掉即可。如果没有联系的内存,也可以分配成几个 100M ,几个 50M ,几个 20M 这样子的,也会比完全碎片化的要快。

    第 1 条附言  ·  2022-06-29 12:49:34 +08:00
    忘了描述实际的现象,实际是每次处理会有 40 个 G 左右,采用多线程处理的话,算法消耗的时间在 80s 左右,但是每次都会比上一次多 0.n 秒,循环往复的跑,几个小时后,时间可能会变成 90 多 s 。
    43 条回复    2022-07-01 09:10:29 +08:00
    liuhan907
        1
    liuhan907  
       2022-06-29 11:33:39 +08:00
    std::vector 分配的内存本来不就是连续的么?
    ligiggy
        2
    ligiggy  
    OP
       2022-06-29 11:35:44 +08:00
    @liuhan907 是连续的。
    luassuns
        3
    luassuns  
       2022-06-29 11:38:06 +08:00
    每组数据就很大了,用之前用 vector.reserve 扩展一下避免分配
    ligiggy
        4
    ligiggy  
    OP
       2022-06-29 11:43:09 +08:00
    @luassuns 我读出数据之前已经 resize 过了。
    shylockhg
        5
    shylockhg  
       2022-06-29 11:44:54 +08:00
    感觉楼上是正解,reserve 够大就行,你这个需求和内存池没关系
    ipwx
        6
    ipwx  
       2022-06-29 11:55:56 +08:00
    "就是我申请一块足够大的连续内存,让这块内存分配数据的存储空间"

    其实标准库也是这么干的。如果标准库不能满足你的需求,你应该对 new / delete 之类的操作进行优化(侵入式),而不是找个新的什么内存池。
    ipwx
        7
    ipwx  
       2022-06-29 11:57:29 +08:00
    一般在算法意义上的内存池都是“只用不扔”的。比如申请一大段内存,不断切出来新的 node 做树结构之类的。哪怕要回收,也是做一个很简单的链表,把刚刚不用的节点直接串起来。只要链表上有节点就不切新的,而是用原来的。
    hackfly
        8
    hackfly  
       2022-06-29 12:01:06 +08:00
    内存碎片就是因为不断的申请、析放产生的,要减少申请,增加重复利用。
    GeruzoniAnsasu
        9
    GeruzoniAnsasu  
       2022-06-29 12:17:45 +08:00   ❤️ 3
    500m 是很大了,而且比较要命的是,如果你只用标准库而不直接调 OS API 预留内存的话,上一个 500m 的连续空间不知道什么时候就会被 glibc(ptmalloc)拆碎,拆碎了之后还不会优先合并大区块或者热区块,让 glibc 来管这些反复申请的大空间不是个好做法

    monotonic buffer 是单调递增 buffer (即指针永不回退,除非整个 buffer 释放),适用于把相对大的一整块内存分给要不停分配小空间折腾的场合,不太符合你的需求


    建议尝试 std::pmr::vector 配合 pmr::memory_resource ,然后手动 mmap 一块内存作为 memory_resource 的基底

    另外因分配内存导致的性能缓慢通常是因为分配姿势不对引起的,比如是不是用循环依次复制每个对象了?是不是调用太多其实没用的构造 /析构了? c++里如果发现分配内存占了时间大头绝大多数都是逻辑写得有问题可以优化掉的
    nightwitch
        10
    nightwitch  
       2022-06-29 12:21:49 +08:00
    可以链接上 tcmalloc 试试。

    不过按你的描述来说你是已经 reserve 过了,相当于申请和释放都是在操作大块内存,没道理会产生很多内存碎片。
    最好还是用 profiler 看一下性能瓶颈在哪里。
    ligiggy
        11
    ligiggy  
    OP
       2022-06-29 12:33:23 +08:00
    @GeruzoniAnsasu
    感谢解答,目前我是这样做的。

    std::array<std::byte, 102400> buffer;
    std::pmr::monotonic_buffer_resource resource{buffer.data(), buffer.size()};
    std::pmr::vector<Frame> vecFrame{&resource};

    后面我尝试下你说的几点问题。
    ligiggy
        12
    ligiggy  
    OP
       2022-06-29 12:35:37 +08:00
    @nightwitch 好的,感谢提的建议。
    L4Linux
        13
    L4Linux  
       2022-06-29 13:40:25 +08:00
    [un]synchronized_pool_resource with max_blocks_per_chunk
    lakehylia
        14
    lakehylia  
       2022-06-29 14:05:57 +08:00
    如果用到的内存比较平均,那你就预估一个内存块的最大值,申请好之后,用完了之后先不要释放,下次在用直接用用过的
    ipwx
        15
    ipwx  
       2022-06-29 14:11:30 +08:00
    我觉得 9L 说得对,楼主的需求适合用 mmap 手动切一整块出来用。

    mmap 的内存单位大小一般是 4K 。计算你要 12B 也会给你切出来 4K 。好处是保证没有碎片。事实上 malloc / new 很可能是低下用 mmap 切出来了这种块然后自己切着玩的。
    ipwx
        16
    ipwx  
       2022-06-29 14:12:06 +08:00
    ... 顺便 mmap 大概是肯定不会有碎片的。因为在内核中 mmap 是要写到 cpu 的页表里的。
    ipwx
        17
    ipwx  
       2022-06-29 14:13:16 +08:00
    另外也永远不需要担心 mmap 切出来的若干 4K 的东西是不连续的。逻辑地址上 mmap 永远可以是连续空间,只不过 cpu 的页表可以把逻辑连续的若干个 4K 映射到物理不连续的 4K 。这一切都发生在内核态对用户程序透明。
    boaofCHIAN
        18
    boaofCHIAN  
       2022-06-29 15:39:12 +08:00
    试试直接用 tcmalloc 直接接管内存分配和使用吧
    ligiggy
        19
    ligiggy  
    OP
       2022-06-29 15:52:51 +08:00
    @boaofCHIAN 很遗憾,我用的 msvc
    cs8425
        20
    cs8425  
       2022-06-29 16:41:10 +08:00
    不负责任丢一个内存分配库
    楼主可以试试...?
    https://github.com/microsoft/mimalloc
    GeruzoniAnsasu
        21
    GeruzoniAnsasu  
       2022-06-29 16:50:09 +08:00
    @ligiggy 啊 msvc 那有点蛋疼,windows 的 libc 好像没有自己的堆管理,直接用的 win32 api 。 可以先试试 HeapAlloc / VirtualAlloc 能不能分配出足够的空间
    ligiggy
        22
    ligiggy  
    OP
       2022-06-29 17:01:33 +08:00
    @cs8425 看起来有点牛逼啊
    mingl0280
        23
    mingl0280  
       2022-06-29 17:03:23 +08:00 via Android
    自己建内存池,启动时一次性申请。STL 容器使用自建内存池的 allocator/deallocator 避免内存被释放就完了。
    mingl0280
        24
    mingl0280  
       2022-06-29 17:09:43 +08:00 via Android
    我要是你的话我就干脆 new 几个足够大的内存区域,然后处理函数加个 size ,处理完既不释放也不清除直接开始复用(反正有 size 也越不了界),来来回回就写那几个内存区域,等到程序结束再释放这几个内存区域。
    bestwaytowait
        25
    bestwaytowait  
       2022-06-29 17:18:43 +08:00
    为啥会用到 pmr 的内容,这个不是 new 一片大内存,持续用就满足这部分性能需求了?
    ligiggy
        26
    ligiggy  
    OP
       2022-06-29 17:33:17 +08:00
    @bestwaytowait 想请教一下,怎么 new ?
    bestwaytowait
        27
    bestwaytowait  
       2022-06-29 19:16:42 +08:00
    @ligiggy
    1. 正常使用 vector ,reserve 大内存,里面元素用 variant 可以支持这个 vec 不释放,反复用
    2. 手动 new 大片内存,然后 object 自己用 placement new ,反复用这段内存不释放
    3. 正常使用 vector + custom allocator ,也可以类似实现

    你可以 godbolt 贴个 demo 出来,大家一起改改看
    mingl0280
        28
    mingl0280  
       2022-06-29 22:52:43 +08:00
    @ligiggy
    ```C++
    //分配:
    size_t buffer_size = 1024*1024*600; // assume 600M
    static float* float_buf = new float[buffer_size]{};
    //使用:
    data_process_float(size_t data_size){
    // 对 float_buf 干啥随便,反正别用 delete 删指针,别把指针指向 nullptr 就完了
    }
    // 销毁:
    on_destroy(){
    if (float_buf)
    delete[] float_buf;
    }
    ```
    pagxir
        29
    pagxir  
       2022-06-29 23:02:30 +08:00 via Android
    为啥不直接用 mmap 呢? mmap 后再用 placement new 就好了。
    jink2018us
        30
    jink2018us  
       2022-06-30 01:03:59 +08:00
    32 位的吧?最简单编译成 64 位就没这问题了。所谓内存碎片是指把 32 位地址空间嚯嚯完了,64 位能嚯嚯很久
    FrankHB
        31
    FrankHB  
       2022-06-30 08:26:06 +08:00
    monotonic buffer 是只分配不管释放,除非最后整个干掉。如果不是严格需要单调行为,一般应该用 pool resource ,而不是放任让空间更加紧张。
    不过这里 std::pmr 有个坑是不保证一定会释放,允许 pool resource 也实现成 monotonic ,算是 QoI 问题,虽然没见过实际实现这么贱的。我提过 issue 然而 WG21 那边没怎么鸟,我也懒得跟了。
    因为这个原因和 C++11 依赖我用自己实现的兼容 std 的版本(以及某些实现的细碎 bug ),加了些扩展:github.com/FrankHB/YSLib/blob/master/YBase/include/ystdex/memory_resource.h

    @ipwx 就跟智能指针不总是负责所有权一样,显然不是“只用不扔”。pool resource 就是主要改善局域性和减少碎片,不一定说的是不扔。反倒是默认不扔会有上面的可用性问题。
    mmap 滥用一样可以有虚拟地址空间碎片。

    @cs8425 mimalloc 是几个比较强的实现之一,不过多线程通常不如 snmalloc 。
    mimalloc 预期的典型负载是动态语言运行时。对充分使用 C++ allocator (其实会因为复制 allocator 吃寄存器带宽的亏,先不管了)。一般表现和 MSVC 实现中的 pmr pool resource 类似。前者的优势主要是折腾的姿势多,但是这里似乎也用不到。
    我实现的 pmr 也比较类似 MSVC ,还偷懒了一点(少了个 intrusive 结构,直接复用 vector )。不过针对负载调整( github.com/FrankHB/NPLC/blob/master/NBuilder/Interpreter.cpp#L389 )后基本能碾压 LD_PRELOAD mimalloc 了。
    opt-in 了编译器相关的优化( github.com/FrankHB/YSLib/blob/master/YBase/source/ystdex/memory_resource.cpp#L267 )还能更快一点。
    FrankHB
        32
    FrankHB  
       2022-06-30 08:29:20 +08:00
    @FrankHB 最后一个 URL 有误,应为#L647 ,指局部 optimize("Os")生成 x86_64 代码中少了 15%的指令。
    ligiggy
        33
    ligiggy  
    OP
       2022-06-30 08:51:32 +08:00
    @mingl0280 你这种做法有可能需要 n*600M 的连续内存,因为我有多线程处理,线程数量是可以设定的,然后插值等等处理,可能还会深拷贝一次,不会存在申请不到的情况吗?即使我的测试机器有 128G 的内存。
    ipwx
        34
    ipwx  
       2022-06-30 10:30:15 +08:00
    @FrankHB 咱说的场景是写一个具体的算法,函数结束以后统一清理。

    包括你说的 pool resource 之类的,用链表把暂时踢出 tree structure 的 node 串起来不也是最简单的 pool 么。而且现任是具体算法的场景下最高效的形式。

    如果不是算法的场景,那可以有另外的方法。不过我的看法仍然是,通用 allocator 必然是有缺陷的,如果这东西这么简单那为什么标准库不能完美解决碎片呢?都用 c++ 了,具体场景具体分析进行优化也不是不行。
    mingl0280
        35
    mingl0280  
       2022-06-30 11:55:36 +08:00
    @ligiggy 多线程 和 “申请多少内存”没有 任 何 关 系。
    ![代码如图]( )
    mingl0280
        36
    mingl0280  
       2022-06-30 12:47:29 +08:00
    @ligiggy ![图片]( )
    ligiggy
        37
    ligiggy  
    OP
       2022-06-30 13:05:42 +08:00
    @mingl0280 我想表达的本意是,我用多线程处理不同组的数据,所以申请的内存必须是 500M*n 的,而不是单线程,申请一个 600M 的数据,重复使用。
    mingl0280
        38
    mingl0280  
       2022-06-30 13:32:51 +08:00
    @ligiggy 你前半句和后半句没有任何关联。一个程序既可以在单线程里申请几百 G 的内存然后拿给一堆其它线程用,也可以以几百个线程申请几百个内存区域拿给单独一个线程用——线程与申请的内存可以说是毫无关系。
    你唯一要做的,就是告诉你的处理线程,这个线程的数据是 a 地址下 b 长度……然后就随便去用了,尤其是你这种定长数据,那真是好办得不得了,根本不需要申请 /释放任何内存空间。
    对于你这个需求,我可以用以下步骤来做:
    1. 申请 N*500M 整块的内存为一个 plain buffer ,该 buffer 基地址为 A 。
    2. 创建线程:
    - 对于第 N 个线程,传递 A 和 N 给线程,线程计算出 A+N*500 为可用的内存部分。我一般是直接传个 uint8_t*和 uint 进去,简单方便。
    循环处理:
    - 读入数据到 A+N*500 ,随便怎么弄
    - 处理数据。
    3. 销毁线程:
    - 无需特殊处理。
    mingl0280
        39
    mingl0280  
       2022-06-30 13:33:31 +08:00
    @ligiggy 最后还有一步,所有线程跑完了把那个整块的内存给 delete 了,完事。
    mingl0280
        40
    mingl0280  
       2022-06-30 14:14:07 +08:00
    @ligiggy ![]( )
    FrankHB
        41
    FrankHB  
       2022-07-01 03:07:58 +08:00
    @ipwx 你说的思路粒度太大。从 OP 的描述看,我不认为这没被考虑过,倒是一些坑没被说,所以提了一下。
    我说的其实也不十分细(而且不怎么对口,跟 mimalloc 的目标领域倒是强相关),不过既然 mimalloc 都被 OP 认为看上去有帮助了,所以也没多计较这个。
    要说优化,是十分有讲究的,跟具体负载关系很大,坑也各自不一样,所以没有具体的代码也没法说得很细。光是怎么踢出去的策略,实际表现的上下限距离就挺大。如果非得不限定场景,那么只能说 pmr 里的 pool resource 基本算是 C++能用的各种意义上(除了具体实现的坑)最高效的了。
    (这里还是排除复制 allocator 的问题。理论上更高效的是用 keyed static variable 之类比 thread_local 更扩展的机制避免要求有状态 allocator 的复制,但这个不是得改核心语言中很基本的规则就得魔改 ABI ,C++范围内就算了。)
    至于碎片,不是用不用 allocator 的关系,是只要算法不能确保完全静态确定分配,这总是会存在,无非是怎么把问题变小。mmap 之类用好了可能解决问题,但太不具体( OP 不给代码也没法具体),而且实际上不太容易用好,不仔细优化极可能有空间利用进一步下降的代价,照样可以加剧原始问题。

    @mingl0280 OP 既然都用上 pmr 了,表明实际代码很可能难以套用你测试程序的方式使用,“简单方便”是很可疑的。
    这种情况下,只要大的方向(最底层用什么分配)正确,随便一个输入数据和实际生成优化代码的不确定抖动都可能让这里的差距化为虚无,缺乏显著性。
    考虑到 OP 似乎只用 trivial copyable 类型的数据,你的方式不是没实际优势——对编译器优化的不确定性小,编译开销也小。但这也就是确保能改得过来,而不是一定更好。
    FrankHB
        42
    FrankHB  
       2022-07-01 03:15:57 +08:00
    我顺便提一下,pmr 用起来可能有个常识性的坑 OP 不知道有没有绕过去了——copy 容器时 pmr polymorphic_allocate 不会 propagate ,因为 pmr::polymorphic_allocator select_on_container_copy_construction 会返回默认初始化的 allocator 。
    如果没注意,复制(包括传参和返回)容器对象时没有显式使用 allocator 或者 uses_allocator allocation ,退回到默认 new_delete_resource ,不但基本是纯粹的性能回归 bug ,还可能会在 swap 等操作中凭空多出 UB 。
    一旦已经注意到这个问题并正确实现,前面一些回复的手动加 buffer 的实现改起来差距就大了。
    ligiggy
        43
    ligiggy  
    OP
       2022-07-01 09:10:29 +08:00
    @FrankHB 不给代码,是因为这部分内容太过复杂了(处理比较多),也涉及到几个跟同事的核心算法(很难避开),很难去给出 代码样例。

    针对 @mingl0280 这位老哥的分析也很好,我真的很能明白这位老哥的意思,但是我的算法代码实在是不能这样写,也不知道怎么跟他解释清楚,感谢你的解释。

    当然,也因为我没给出实例代码,可能导致诸如我忽视了某些个细节的处理,导致内存碎片。也有可能因为魔改了很多三方库导致的,比如 opencv 和 eigen 。可能我的关键问题不是用动态内存管理的库吧,仔细琢磨代码细节,找出漏洞可能才是解决问题的办法。

    总而严重,感谢你的耐心回复和解释,我继续加油了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5361 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 07:58 · PVG 15:58 · LAX 00:58 · JFK 03:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.