Wwise SDK
AkMemoryArena 的配置和调整
|
Wwise SDK 2024.1.10
|
AkMemoryArena 是 Wwise 声音引擎使用的自定义内存分配器。您可以利用它的各种设置来管理其预留的内存。声音引擎会使用多个 AkMemoryArena。为此,可根据项目需要对其进行适当调整,来大幅提升声音引擎整体的内存利用效率。
AkMemoryArena 概述
作为通用的内存分配器,AkMemoryArena 会根据内存分配大小将其归入不同的内存堆(Small、Medium、Large 或 Huge)。
Small 分配是指 256 字节以内的分配。此阈值不可配置。这些分配会归入 Small-Block Allocator (SBA) 内存堆。SBA 基于由固定大小的内存块构成的跨度管理各项分配。这样方便对内存碎片做统一的处理。各个跨度的大小可自行配置。这些跨度的分配基于父级 Medium 或 Large 内存堆。在初始化 SBA 时,会为其设定初始内存区域。这样可降低 SBA 内存堆中的分配所用的开销。通常,AkMemoryArena 中各种规模的内存分配都需要 16 字节的开销(用于存储分配元数据)。不过,初始区域内的 SBA 分配并不需要这样的开销。比如,若普通 SBA 跨度为 16 KiB (Kibibyte),并被配置为处理 64 字节的分配,则该跨度可处理约 200 个 80 字节的内存块。不过,若在初始区域内分配同样的 16 KiB 跨度,则其可处理约 250 个 64 字节的内存块。
Medium 分配是指 256 ~ AkMemoryArenaSettings::uAllocSizeLarge 字节的分配;Large 分配是指 AkMemoryArenaSettings::uAllocSizeLarge ~ AkMemoryArenaSettings::uAllocSizeHuge 字节的分配。Medium 和 Large 分配所用的内存堆采用基于 Two-level segregated fit (TLSF) 的算法。如此一来,这些内存堆便可以相当于 O(1) 的性能进行分配和释放。这些内存堆采用 Good-fit 分配策略来决定要将分配放在哪里。不过,跟原始的 TLSF 算法不同,AkMemoryArena 所用的 TLSF 内存堆还允许随着内存需求的增加请求获取新的内存跨度,而不要求新的内存跨度跟其他内存跨度是连续的。另外,在释放所有相关内存后,TLSF 内存堆也会释放内存。在请求执行 Medium 或 Large 分配时,TLSF 内存堆会先尝试查找初始 Base 内存跨度中可用的内存块;若无可用内存块,则尝试查找已映射的 Medium 或 Large 跨度中可用的内存块。若仍未找到可用的内存块,则请求在 Huge 分配堆中获取新的跨度,并添加到 Medium 或 Large 跨度列表。同样地,在跨度没有任何分配时,其将被视为 Unused。在 Unused 跨度达到一定数量后,它们会被释放给系统。
Huge 分配是指大到无法归入 Medium 或 Large 内存堆的分配。这些分配放在单独的跨度中,并且不与其他分配共用内存。另外,Huge 跨度的内存不会被重复使用或缓存。也就是说,一旦相关内存被释放,跨度就会马上被释放。
AkMemoryArenaSettings
AkMemoryMgr 包含一系列初始化设置,方便配置声音引擎所用的不同 AkMemoryArena。除非另有说明,否则下述值的大小不需要是 2 的幂。
AkMemoryArenaSettings::bEnableSba– 决定是否要激活 SBA。若未激活,则将所有 Small 分配视为 Medium 分配,并发送到 TLSF 内存堆。在默认情况下,Media AkMemoryArena 会禁用此选项,因为 Media 分配对 SBA 来说通常太大。AkMemoryArenaSettings::uSbaInitSize– SBA 内存堆的初始内存区域的大小(字节)。因为减少了内存分配开销,所以此初始区域中的分配占用空间较少。AkMemoryArenaSettings::uSbaSpanSize– SBA 内存堆所用各个内存跨度的大小(字节)。您可以将其设为较大的值来略微提升系统性能,但可能会因内存未被使用而造成更多内存浪费。注意,该值必须是 2 的幂。AkMemoryArenaSettings::uSbaMaximumUnusedSpans– 决定在释放之前最多可有多少个在 SBA 内存堆初始内存区域以外分配的 SBA 跨度处于未使用状态。AkMemoryArenaSettings::uTlsfInitSize– TLSF 内存堆所用初始 Base 内存跨度的大小(字节)。AkMemoryArenaSettings::uTlsfSpanSize– 为 Medium 分配创建的每个辅助内存跨度的大小(字节)。若请求执行的分配(需要获取新的跨度)大于该值,则将请求分配的大小舍入为该值的下一倍数。AkMemoryArenaSettings::uTlsfLargeSpanSize– 为 Large 分配创建的每个辅助内存跨度的大小(字节)。若请求执行的分配(需要获取新的跨度)大于该值,则将请求分配的大小舍入为该值的下一倍数。AkMemoryArenaSettings::uTlsfSpanOverhead– 在为 TLSF 请求获取新的跨度时从请求执行的 Huge 分配减去的字节数。AkMemoryArenaSettings::uTlsfMaximumUnusedMediumSpans– 决定在释放之前最多可有多少个用于 Medium 分配的跨度处于未使用状态。AkMemoryArenaSettings::uTlsfMaximumUnusedLargeSpans– 决定在释放之前最多可有多少个用于 Large 分配的跨度处于未使用状态。AkMemoryArenaSettings::uAllocSizeLarge– 决定分配要达到多少字节才会被归为 Large 分配。所有小于该值但大于 256 字节的分配都将被视为 Medium 分配。注意,若该值大于 uAllocSizeHuge,则不会将任何分配归为 Large 分配。AkMemoryArenaSettings::uAllocSizeHuge– 决定分配要达到多少字节才会被归为 Huge 分配。AkMemoryArenaSettings::uMemReservedLimit– 决定此 AkMemoryArena 最多可预留多少内存(字节)。若 Huge 内存堆尝试请求获取的内存超过该限值,则返回 nullptr,且fnMemAllocSpan不会被调用。AkMemoryArenaSettings::fnMemAllocSpan– 在 Huge 内存堆要实施新的分配时执行的用户提供的回调(包括请求从 TLSF 内存堆获取新的跨度)。AkMemoryArenaSettings::fnMemFreeSpan– 在 Huge 内存堆释放分配时执行的用户提供的回调(包括从 TLSF 内存堆释放跨度)。
以下示例代码展示了在默认情况下会如何初始化 AkMemoryMgr 的 AkMemoryArenaSettings。您可以在此基础上根据自身需要对 AkMemoryArenaSettings 做进一步的设置:
AkMemoryArenaSettings 分配回调
对于 AkMemoryArenaSettings::fnMemAllocSpan 和 AkMemoryArenaSettings::fnMemFreeSpan ,回调具有以下要求和行为:
fnMemAllocSpan只需返回指向所请求大小的内存分配的指针。返回的分配对所返回内存的对齐没有任何要求,也不需要跟之前的其他内存分配是连续的。fnMemFreeSpan只需释放给定地址的内存。- 只使用之前通过
fnMemAllocSpan返回的地址对fnMemFreeSpan进行调用。size 参数跟最初请求分配时所用的值相同。 - 在执行
fnMemAllocSpan的过程中,可将任意值写入到out_userData中。该值将作为in_userData参数提供给与fnMemFreeSpan对等的调用。
回调的实现可以非常灵活。比如,可使用以下代码来实现:
Advanced Profiler 中的 Memory Arenas 选项卡会列出当前分配的所有跨度以及与回调返回值匹配的各个跨度的地址和 userData 值。这样尤其方便在结合使用其他调试和性能分析工具的时候进行调试。
有关 AkMemoryArena 配置的建议
我们已对 AkMemoryMgr 管理的 AkMemoryArena 的默认设置做了优化,以便将初始预留内存控制在较低水平,并避免出现其他一些重大错误。不过,还是建议根据自身项目的需要对其中的大部分设置加以调节。合理的调节可显著优化 Wwise 中预留的内存、游戏引擎的总计预留内存以及 Wwise 的 CPU 处理性能。 为此,不妨参考以下建议来找到最适合自己项目的设置方案:
- 为 Primary 和 Media 内存区设置
AkMemoryArenaSettings::uTlsfInitSize以使其符合典型的内存用量或整体内存预算。通常,较大的 Base 跨度可让 TLSF 算法实现更好的内存碎片处理性能。另外,还可尝试使用单个跨度来监控是否超出了内存预算(通过检查有没有发出获取辅助内存跨度的请求)。这样还有助于制定整个应用程序的内存预算,因为项目启动时的系统总计预留内存能更好地反映长时间运行后的系统总计预留内存。 - 为 Primary AkMemoryArena 设置
AkMemoryArenaSettings::uSbaInitSize以使其符合游戏当中的一般 SBA 预留要求。借助 Advanced Profiler 中的 Memory Arenas 选项卡,可评估游戏当中的一般 SBA Reserved 需求,或确定一个较高的水位并设置uSbaInitSize以与之保持一致。通过将其设为较大的值,可将更多 SBA 内存分配放在初始内存区域中并减少各项内存分配的开销。另外,这样还可减少 TLSF 内存堆中分配的 SBA 跨度,并减少 Primary TLSF 内存堆中长时间产生的碎片。 - 或者:将
AkMemoryArenaSettings::uTlsfInitSize设为较小的值以便回收内存并用于其他系统。若有游戏场景会直接释放大量内存,而且内存碎片只会造成很小的影响,最好调低 Primary 或 Media 内存区的uTlsfInitSize以便将内存用于其他系统。 - 或者:将
AkMemoryArenaSettings::uTlsfMaximumUnusedMediumSpans设为较大的值来确定内存预留水位。在默认情况下,会将uTlsfMaximumUnusedMediumSpans和uTlsfMaximumUnusedLargeSpans设为 1 以便及时回收未使用的内存。您可以将其临时设为较高的值,确保在分配之后不会释放跨度,以此确定自己的内存预留水位。 - 使用 Large 内存分配来减少内存碎片。在默认情况下,会禁用 Large 内存分配。但在有些情况下,可能要让特定大小的分配在物理上相互独立,以减少长时间产生的系统碎片。为此,不妨做些尝试以确定合理的
uAllocSizeLarge和uTlsfLargeSpanSize值。这可能会增加前期的总计预留内存,因为有更多跨度中的内存不被使用,不过可以减少总体产生的内存碎片。注意,在有可用空间的情况下,Medium 和 Large 分配都会在 Base 跨度中执行。 - 将
AkMemoryArenaSettings::uAllocSizeHuge设为较小的值以减少内部的碎片。因为 Huge 分配是独立的,所以其不受内部内存碎片影响。若可控制因频繁调用fnMemAllocSpan和fnMemFreeSpan额外产生的 CPU 成本和外部碎片,那么最好调低 Huge 分配的认定阈值以便将更多内存归入到该类别中。 - 设置
AkMemoryArenaSettings::uTlsfSpanOverhead以使其与fnMemAllocSpan实现中的分配元数据大小保持一致。若fnMemAllocSpan实现所创建的内存分配本身需要一些元数据,请估算相应元数据的大小并调高 uTlsfSpanOverhead 以与之保持一致。该值用于略微减小请求获取的 TLSF 跨度的大小,以确保fnMemAllocSpan的分配映射预期数量的内存。比如,TLSF 跨度的大小应当为 2.00 MiB(2097152 字节),实际映射的内存可能为 2.02 MiB(2113536 字节),因为需要使用额外的内存页来存储内存分配元数据。不过,若预计需要 128 字节的元数据,则可将 uTlsfSpanOverhead 设为该值,以便从请求的 TLSF 跨度大小中减去对应量值。在本例中,请求的 TLSF 跨度大小为 2097024 字节,最终会刚好映射 2.00 MiB 的内存,从而减少物理内存的浪费。