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

2022-06-29 11:30:17 +08:00
 ligiggy

项目上需要处理若干组,每组 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 这样子的,也会比完全碎片化的要快。

3894 次点击
所在节点    C++
43 条回复
GeruzoniAnsasu
2022-06-29 16:50:09 +08:00
@ligiggy 啊 msvc 那有点蛋疼,windows 的 libc 好像没有自己的堆管理,直接用的 win32 api 。 可以先试试 HeapAlloc / VirtualAlloc 能不能分配出足够的空间
ligiggy
2022-06-29 17:01:33 +08:00
@cs8425 看起来有点牛逼啊
mingl0280
2022-06-29 17:03:23 +08:00
自己建内存池,启动时一次性申请。STL 容器使用自建内存池的 allocator/deallocator 避免内存被释放就完了。
mingl0280
2022-06-29 17:09:43 +08:00
我要是你的话我就干脆 new 几个足够大的内存区域,然后处理函数加个 size ,处理完既不释放也不清除直接开始复用(反正有 size 也越不了界),来来回回就写那几个内存区域,等到程序结束再释放这几个内存区域。
bestwaytowait
2022-06-29 17:18:43 +08:00
为啥会用到 pmr 的内容,这个不是 new 一片大内存,持续用就满足这部分性能需求了?
ligiggy
2022-06-29 17:33:17 +08:00
@bestwaytowait 想请教一下,怎么 new ?
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
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
2022-06-29 23:02:30 +08:00
为啥不直接用 mmap 呢? mmap 后再用 placement new 就好了。
jink2018us
2022-06-30 01:03:59 +08:00
32 位的吧?最简单编译成 64 位就没这问题了。所谓内存碎片是指把 32 位地址空间嚯嚯完了,64 位能嚯嚯很久
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
2022-06-30 08:29:20 +08:00
@FrankHB 最后一个 URL 有误,应为#L647 ,指局部 optimize("Os")生成 x86_64 代码中少了 15%的指令。
ligiggy
2022-06-30 08:51:32 +08:00
@mingl0280 你这种做法有可能需要 n*600M 的连续内存,因为我有多线程处理,线程数量是可以设定的,然后插值等等处理,可能还会深拷贝一次,不会存在申请不到的情况吗?即使我的测试机器有 128G 的内存。
ipwx
2022-06-30 10:30:15 +08:00
@FrankHB 咱说的场景是写一个具体的算法,函数结束以后统一清理。

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

如果不是算法的场景,那可以有另外的方法。不过我的看法仍然是,通用 allocator 必然是有缺陷的,如果这东西这么简单那为什么标准库不能完美解决碎片呢?都用 c++ 了,具体场景具体分析进行优化也不是不行。
mingl0280
2022-06-30 11:55:36 +08:00
@ligiggy 多线程 和 “申请多少内存”没有 任 何 关 系。
![代码如图]( )
mingl0280
2022-06-30 12:47:29 +08:00
@ligiggy ![图片]( )
ligiggy
2022-06-30 13:05:42 +08:00
@mingl0280 我想表达的本意是,我用多线程处理不同组的数据,所以申请的内存必须是 500M*n 的,而不是单线程,申请一个 600M 的数据,重复使用。
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
2022-06-30 13:33:31 +08:00
@ligiggy 最后还有一步,所有线程跑完了把那个整块的内存给 delete 了,完事。
mingl0280
2022-06-30 14:14:07 +08:00
@ligiggy ![]( )

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/862885

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX