返回

文章详情

双头栈分配器

Hacker News2026年10月15日 02:06

在编写高性能应用程序(例如视频游戏)时,您会学习编写快速代码的巧妙技巧,以充分利用有限的资源。内存分配是一个关键性能主题,涉及需要可变数量内存的代码。当您听到“栈分配器”时,您可能会想到“alloca”,它允许您从实际程序栈中分配内存。这在紧急情况下非常有用,但今天的帖子不是关于这个的。我们今天讨论的这类型栈分配器的另一个名称是“递增分配器”。大约在2010年,我在Surreal Software(Midway Seattle)工作,于一款名为“This is Vegas”的开放世界游戏上工作,不幸的是当Midway倒闭时,该项目被取消。在那个项目中,我的职责之一是动画编程。动画是通过为一组骨骼创建状态机来工作的,每个状态都是一个动画混合树。当状态变化时,它们会从旧状态过渡到新状态以保持连续性。这里有大量的混合过程,并且在CPU上评估和混合这些动画时需要大量的动态内存分配。这些分配都是瞬态的,仅需存在于帧的部分时间。此外,这些分配必须每帧进行,因为在不同帧之间可能会发生剧烈变化,例如基于相机视锥体来决定当帧中动画哪些角色。分配器在对该代码段进行性能分析时显示为热点。为了解决这个问题,我们实现了“栈分配器”,在这个分配器中,分配仅仅是整数相加,释放则更加便宜。为此,您分配并保留一些内存,这是您将需要的最大内存,例如1MB。这就是您的栈。您还保留一个整数“tos”变量以知道栈的顶部在哪里。当您想要分配内存时,您得到的内存就是栈顶部的内存,并且您将想要分配的字节数加到tos变量上,以确保下一次分配在您的分配之后:在实际实现中,您还需要知道内存的大小,以便处理tos达到内存末尾的情况——通过返回null,或断言,或其他方法。当您完成内存使用后,只需将tos变量设置为0。在我们的情况下,我们在动画混合完成并将结果复制到即将发送到顶点着色器用于蒙皮的缓冲区后将tos设置为0。如果您一般使用栈分配器处理游戏帧期间所有瞬态内存分配,您可能会在帧开始时将其设置回0。结果是巨大的变化。在性能分析中,分配不再需要任何可测量的时间,因为分配是整数加法,而释放所有内存只是一个整数赋值。没有比这更快的了。有一点需要注意的是,按此方式编写的分配器不会调用析构函数。使用放置式new创建构造函数会很简单。调用析构函数则要难得多,因为这必须是一个手动过程,容易出错。您可能能够使用某种RAII模式,但栈分配器最适合更简单的类型。栈分配器的另一个好处是它们可以在多线程环境中工作。如果tos变量使用原子加法,则多个线程可以从同一个栈分配器中进行分配。这在CPU的分叉/合并场景中或在GPU上运行复杂算法时可能会很方便。您还可以使用栈分配器进行“部分释放”。例如,如果您有某个递归过程,每个级别需要动态分配,您可以记住该级别开始时tos的位置,并在该级别处理完成时将其设置回该值。双头栈分配器几年后,我在暴雪的《星际争霸2》团队中找到了自己。我的老板詹姆斯·安哈特(James Anhalt)是一个非常聪明的人,他向我介绍了着色器玩具,并分享了一些之前博客文章中提到的很酷的技术。他告诉我,双头栈分配器是他过去遇到的内存碎片问题的解决方案。设想你有一个流动世界游戏,玩家在其中四处奔跑,旧的关卡几何体会被释放,而新的关卡几何体会从磁盘上分配并流入。同时,设想玩家角色可以变形——每次他们改变形态时,旧形态会被释放,新形态会从磁盘分配并流入。当您有这两个系统在各自的时间线上分配和释放随机数量的内存时,您的内存就会变得碎片化。让我们来看一个简单的例子。假设您有100MB的总内存用于关卡(红色)和角色(绿色)。关卡分配了20MB,然后角色分配了30MB……

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

☕请我喝杯咖啡