返回资讯中心
外部精选
软件工程
#社区热议

开发者分享用索引句柄替代C/C++原始指针和智能指针的动态内存管理经验

来源标题:Handles are the better pointers (2018)

一篇技术博客介绍了用索引句柄(index-handles)替代C/C++原始指针和智能指针的动态内存管理方法。作者基于15年以上维护0.5到100万行大型C++代码库的经验,指出智能指针管理的对象蜘蛛网式代码虽能减少内存损坏,但存在缓存未命中导致的严重性能问题,以及内存碎片和假内存泄漏。文末还补充了用每槽位代际计数器防止句柄碰撞的方法。

Hacker Newsvinhnx2026/11/3 · 4 天前30 分钟阅读

以下正文由 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 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →