开发者分享用索引句柄替代C/C++裸指针和智能指针的内存管理经验
来源标题:Handles are the better pointers (2018)
一位开发者基于15年以上大型C++代码库经验,提出用索引句柄(index-handles)替代C/C++中的裸指针和智能指针进行动态内存管理。他指出智能指针构建的"对象蛛网"代码虽能避免内存损坏,却因大量缓存未命中而极慢,且存在内存碎片和"假内存泄漏"问题。文末还补充了用每槽位世代计数器防止句柄冲突的方法。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
2018年11月28日:我在文末补充了一个小更新,介绍如何通过每个槽位的代际计数器来防止“句柄冲突”。
……在这里我要聊一聊我最近在 C 和 C++ 中如何进行动态内存管理,这套做法基本上是用“索引句柄”取代了裸指针和智能指针。
在上一篇博客中我提到了无指针、无分配编程,但跳过了细节。下面这篇博客讲的就是这个。
这些内容全部基于我 15 年以上与规模相当大的 C++ 代码库(50 万到 100 万行左右)搏斗的(有时是痛苦的)经验,这些代码库中内存通常通过智能指针管理。最糟糕的情况是数十万甚至上十万个小 C++ 对象,每个对象各自占一次堆分配,彼此通过智能指针相互指向。虽然这类代码在内存损坏方面相当健壮(段错误和内存损坏很少发生,因为大多数错误在解引用智能指针时会被断言捕获),但这种“对象蛛网代码”也慢得离谱,而且没有明显的优化切入点,因为整个代码到处都是缓存未命中。其他典型问题包括内存碎片和“假内存泄漏”——一个被遗忘的智能指针阻止了底层内存的释放(我称之为“假泄漏”,因为这类泄漏无法被内存调试工具捕获)。
这里介绍的内容没有什么是特别新颖或巧妙的,只是一些简单想法的集合,它们合在一起在较大的代码库中相当有效,可以防止(或至少尽早发现)C 和 C++ 中许多常见的内存相关问题,甚至在更高级的垃圾回收语言中也可能有用,用来减轻垃圾回收器的压力。然而,底层设计哲学并不太适合经典的 OOP 世界——在那种世界里,应用由一个个小的自治对象相互交互构成。这也是为什么在现有的大型 OOP 代码库中实现这些想法相当棘手,因为对象的创建和销毁在整个代码中“分散”发生。
不过,这里描述的方法对于面向数据的架构非常有效——在这种架构中,中央系统作用于紧密打包在内存中的数据项数组。
下面这篇博客的大部分内容是从游戏开发者的视角写的,但也适用于其他领域:程序需要在内存中管理几百到几百万个对象(或一般意义上的“数据项”),并且这些项会被频繁创建和销毁。
下面我会详细解释每一点。但基本思路是:普通的“用户层”代码不直接调用内存分配函数(如 malloc、new 或 make_shared/make_unique),并将指针的使用降到绝对最低(仅当确实需要直接内存访问某个项时,作为短生命周期的引用使用)。最重要的是,指针永远不是某个项底层内存的“所有者”。
相反,直接的内存操作尽可能发生在少数几个集中式系统内部,在那里内存相关问题更容易调试和优化。
在这篇博客中,“系统”指的是代码库中一个(通常相当大的)部分,负责一组相关任务,比如“渲染”、“物理”、“AI”、“角色动画”等等。这样的系统通过定义清晰的函数 API 与其他系统和“用户代码”隔离,系统数据上的工作在线程紧凑的中央循环中执行,而不是散布在整个代码库中。
系统通常作用于由用户代码控制创建和销毁的项(但请注意,项的创建和销毁不同于分配和释放这些项所使用的内存!)。例如,渲染系统可能处理顶点缓冲区、纹理、着色器和管线状态对象。物理系统处理刚体、关节和碰撞图元,动画系统处理动画关键帧和曲线。
把这类项的内存管理移入系统自身是合理的,因为通用内存分配器不具备关于数据项如何被处理以及数据项之间关系的系统特定“领域知识”。这使得系统可以优化内存分配,在创建和销毁项时执行额外的验证检查,并在内存中排列项以最佳利用 CPU 的数据缓存。
这种“系统领域知识”的一个好例子是使用现代 3D API 销毁渲染资源对象:当用户代码要求销毁某个资源对象时,不能简单地立即销毁它,因为该资源可能仍被引用在一个等待 GPU 消费的命令列表中。相反,渲染系统只会在用户代码请求销毁时将该资源对象标记为待销毁,而实际销毁发生在稍后 GPU 不再使用该资源的时候。
一旦所有内存管理都移入系统,系统就可以利用关于项如何使用的额外知识来优化内存分配和内存布局。一个明显的优化是将同类型的项分组到数组中,并在系统启动时分配这些数组,从而减少通用内存分配的次数。
系统不再在每次创建新项时执行一次内存分配,而是跟踪空闲的数组槽位,并选取下一个空闲槽位。当用户代码不再需要该项时,系统只需将该槽位重新标记为空闲,而不执行释放(这与典型的池分配器没有区别)。
这种池分配很可能比每项执行一次内存分配要快一些,但这甚至不是将项保存在数组中的主要原因(现代通用分配器对于小分配也相当快)。
将系统项保存在数组中而不是各自独立分配,其好处是
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。