Unity3D:内存分配器定制

Unity3D:内存分配器定制
在线工具推荐:3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器

内存分配器定制

优化应用程序性能时,要考虑的一个重要因素是内存分配。使用此页面了解有关 Unity 本机内存分配器类型的信息,并了解可以自定义分配器以提高性能的方案。Unity 建议您大致了解分配器。

有关分配器类型及其默认值的完整参考,请参阅自定义分配器。

注意:并非所有平台都支持此功能。有关详细信息,请参阅特定于平台的文档。

应用程序使用内存分配器来平衡性能和可用内存空间。如果应用程序具有大量备用内存,则在加载场景和帧时,它可以优先使用速度更快、内存密集型的分配器。但是,如果应用程序内存有限,则需要有效地使用该内存,即使这意味着使用较慢的分配器。为了帮助您为不同的项目获得最佳性能,您可以自定义 Unity 的分配器以适应每个应用程序的大小和要求。

Unity 有五种分配器类型。每种类型都有不同的算法,用于将分配拟合到内存块中,因此对于不同的分配很有用。分配之间的重要区别通常是持久性或分配生存期,它决定了分配应该去哪里。例如,长期(持久)分配将转到堆和存储桶分配器,而短期分配将转到线程安全线性和 TLS 分配器。

下表列出了每种分配器类型的算法和用法:

分配器类型算法用于
动态堆两级隔离拟合 (TLSF)• 主分配器• Gfx 分配器• 类型树分配器• 文件缓存分配器• 探查器分配器• 编辑器探查器分配器




(仅适用于编辑器)
固定尺寸无锁分配器作为小型分配的共享分配
器:• 主分配器• Gfx 分配器• 类型树分配器
• 文件缓存分配器


双线程根据大小和线程 ID 重定向分配• 主分配器• Gfx 分配器• 类型树分配器• 文件缓存分配器


线程本地存储 (TLS) 堆栈后进先出电堆临时拨款
线程安全线性循环先进先出用于将数据传递到作业的缓冲区

注意:本文档中的示例使用关闭播放器或编辑器时写入日志的内存使用情况报告。若要查找日志文件,请按照日志文件页上的说明进行操作。

动态堆、存储桶和双线程分配器

本节介绍动态堆、存储桶和双线程分配器的功能和自定义方案。

概述

双线程分配器是结合了动态存储桶分配器的包装器。更具体地说,它结合了:

  • 两个动态堆分配器:主线程的无锁分配器,以及所有其他线程共享的分配器,后者锁定分配和解除分配。Unity 将这些分配器用于对于存储桶分配器来说太大的分配。 动态堆分配器使用内存块。等于或大于半个块的分配将转到虚拟内存系统,而不是动态堆分配器。
  • 用于小型分配的存储桶分配器。 如果存储桶分配器已满,则分配将溢出到动态堆分配器中。

动态堆分配器

主堆分配器是动态堆分配器。它将算法两级隔离拟合 (TLSF) 应用于内存块。

每个平台都有一个默认块大小,您可以自定义该大小。分配必须小于半个块。如果分配等于或大于半个块,则对于动态堆分配器来说太大;相反,Unity 使用虚拟内存 API 进行分配。

动态堆分配器的示例使用情况报告:

[ALLOC_DEFAULT_MAIN]
Peak usage frame count: [16.0 MB-32.0 MB]: 497 frames, [32.0 MB-64.0 MB]: 1 frames
Requested Block Size 16.0 MB
Peak Block count 2
Peak Allocated memory 54.2 MB
Peak Large allocation bytes 40.2 MB

在此示例中,TLSF 块大小设置为 16 MB,Unity 已分配两个块。分配器的峰值使用量为 54.2MB。在这 52.4MB 中,有 40.2MB 没有在 TLSF 块中分配,而是回退到虚拟内存。大多数帧有 16-32MB 的分配内存,而一帧(可能是加载帧)的峰值为 32-64MB 内存。

如果增加块大小,则大型分配将保留在动态堆中,而不是回退到虚拟内存中。但是,该块大小可能会导致内存浪费,因为这些块可能无法完全使用。

若要避免使用类型树和缓存分配器,请将其大小设置为 0。本来使用类型树和缓存的分配将回退到主分配器。这可能会导致更多的碎片,但会节省这些分配器的内存块的内存。

存储桶分配器

存储桶分配器是一种快速无锁分配器,可执行少量分配。通常,存储桶分配器用作加快小型分配的第一步,然后再将其转到堆分配器。

分配器保留内存块用于分配。每个块分为 16KB 的子部分。这是不可配置的,也不会显示在用户界面中。每个小节都分为多个分配。分配大小是配置的固定大小的倍数,称为粒度

以下示例设置演示了为分配保留块的过程:

适用于 Windows、Mac 和 Linux 播放器的共享存储桶分配器
适用于 Windows、Mac 和 Linux 播放器的共享存储桶分配器

在此设置中,总块大小(存储桶分配器块大小)为 4MB,分配粒度(存储桶分配器粒度)为 16B。第一个分配是 16B,第二个是 32B (2*16),然后是 48B、64B、80B、96B、112B 和 128B,总共有八个存储桶(存储桶分配器存储桶计数)。

每个子部分包含不同数量的存储桶。要计算子部分中的存储桶数,请将子部分大小 (16KB) 除以粒度大小。例如:

  • 当分配粒度为 64B 时,子节可容纳 256 个存储桶。
  • 当分配粒度为 16B 时,子节中可容纳 1,024 个存储桶。

存储桶分配器为开发版本和发布版本生成不同的使用情况报告,因为开发版本,每个分配都有一个额外的 40B 标头。下图演示了 16B 和 64B 分配的开发版本和发布版本之间的差异:

开发和发布版本比较
开发和发布版本比较

标头也是分配器在仅分配其 4MB 中的 2MB 后报告它已满的原因:

[ALLOC_BUCKET]
      Large Block size 4.0 MB
      Used Block count 1
      Peak Allocated bytes 2.0 MB
      Failed Allocations. Bucket layout:
        16B: 64 Subsections = 18724 buckets. Failed count: 3889
        32B: 17 Subsections = 3868 buckets. Failed count: 169583
        48B: 31 Subsections = 5771 buckets. Failed count: 39674
        64B: 28 Subsections = 4411 buckets. Failed count: 9981
        80B: 17 Subsections = 2321 buckets. Failed count: 14299
        96B: 6 Subsections = 722 buckets. Failed count: 9384
        112B: 44 Subsections = 4742 buckets. Failed count: 5909
        128B: 49 Subsections = 4778 buckets. Failed count: 8715

在同一项目的发布版本中,分配器块大小就足够了:

[ALLOC_BUCKET]
      Large Block size 4.0 MB
      Used Block count 1
      Peak Allocated bytes 3.3 MB

如果存储桶分配器已满,则分配将回退到另一个分配器。使用情况报告显示使用情况统计信息,包括失败的分配数。如果报告显示的失败计数呈线性增加,则很可能是在计算帧而不是负载时发生失败的分配。回退分配对于场景负载来说不是问题,但如果在计算帧时发生回退分配,它们可能会影响性能。

要防止这些回退分配,请增加块大小,并限制新的块大小以匹配帧的峰值使用量,而不是场景负载峰值使用率。这可以防止块变得太大,以至于它保留了大量在运行时不可用的内存。

提示:探查器分配器共享存储桶分配器的实例。您可以在共享探查器存储桶分配器中自定义此共享实例。

双线程分配器

双线程分配器包装了一个用于小型分配的共享存储桶分配器,以及动态堆分配器的两个实例:一个用于主线程的无锁分配器,以及一个由所有其他线程共享但锁定分配和解除分配的分配器。

您可以自定义两个动态堆分配器的块大小:

主分配器,具有共享线程块大小的自定义值
主分配器,具有共享线程块大小的自定义值

使用情况报告包含分配器所有三个部分的信息。例如:

[ALLOC_DEFAULT] Dual Thread Allocator
  Peak main deferred allocation count 135
    [ALLOC_BUCKET]
      Large Block size 4.0 MB
      Used Block count 1
      Peak Allocated bytes 3.3 MB
    [ALLOC_DEFAULT_MAIN]
      Peak usage frame count: [16.0 MB-32.0 MB]: 8283 frames, [32.0 MB-64.0 MB]: 1 frames
      Requested Block Size 16.0 MB
      Peak Block count 2
      Peak Allocated memory 53.3 MB
      Peak Large allocation bytes 40.2 MB
    [ALLOC_DEFAULT_THREAD]
      Peak usage frame count: [64.0 MB-128.0 MB]: 8284 frames
      Requested Block Size 16.0 MB
      Peak Block count 2
      Peak Allocated memory 78.3 MB
      Peak Large allocation bytes 47.3 MB

注意峰值主要延迟分配计数是删除队列中的项目数。主线程必须删除它所做的任何分配。如果另一个线程删除了分配,则该分配将添加到队列中。分配在队列中等待主线程将其删除。然后计为递延分配。

TLS 和线程安全线性分配器

本节介绍线程本地存储 (TLS) 和线程安全线性分配器的功能和自定义方案。

概述

Unity 有两个分配器在双线程分配器之外工作:

  • 线程本地存储 (TLS):用于快速临时分配的基于堆栈的分配器。这是最快的分配器,因为它几乎没有开销。它还可以防止碎片化。它基于后进先出 (LIFO)。
  • 线程安全线性:一个先进先出 (FIFO) 轮循机制分配器,临时作业分配用于在工作线程之间传递短期内存)。

线程本地存储 (TLS) 堆栈分配器

每个线程都使用自己的快速堆栈分配器进行临时分配。这些分配非常快,生命周期不到一帧。

临时分配器的默认块大小对于平台为 4MB,对于 Unity 编辑器为 16MB。您可以自定义这些值。

注意:如果分配器的使用量超过配置的块大小,Unity 将增加块大小。此增加的限制是原始大小的两倍。

每线程快速临时分配器中的主线程块大小自定义值
每线程快速临时分配器中的主线程块大小自定义值

如果线程的堆栈分配器已满,则分配将回退到线程安全的线性作业分配器。一些溢出分配是可以的:一帧中有 1 到 10 个,或者在加载期间有几百个溢出分配。但是,如果数字在每一帧上都增长,则可以增加块大小。

使用情况报告中的信息可帮助您选择适合您的应用程序的块大小。例如,在以下主线程使用情况报告中,负载峰值为 2.7MB,但其余帧低于 64KB。您可以将块大小从 4MB 减小到 64KB,并允许加载帧溢出到分配:

[ALLOC_TEMP_TLS] TLS Allocator
  StackAllocators :
    [ALLOC_TEMP_MAIN]
      Peak usage frame count: [16.0 KB-32.0 KB]: 802 frames, [32.0 KB-64.0 KB]: 424 frames, [2.0 MB-4.0 MB]: 1 frames
      Initial Block Size 4.0 MB
      Current Block Size 4.0 MB
      Peak Allocated Bytes 2.7 MB
      Overflow Count 0
    [ALLOC_TEMP_Job.Worker 18]

在第二个示例中,工作线程不用于大型临时分配。为了节省内存,您可以将工作线程的块大小减小到 32KB。这在多核机器上特别有用,其中每个工作线程都有自己的堆栈:

[ALLOC_TEMP_Job.Worker 14]
      Initial Block Size 256.0 KB
      Current Block Size 256.0 KB
      Peak Allocated Bytes 18.6 KB
      Overflow Count 0

线程安全线性分配器

Unity 中的工作线程使用循环先进先出(FIFO)算法,为作业快速、无锁定地分配工作缓冲区。作业完成后会释放缓冲区。

此分配器分配内存块,然后在这些块内线性分配内存。可用块保存在池中。当一个块已满时,分配器会从池中获取一个新块。当分配器不再需要块中的内存时,它会清除该块,并且该块返回到可用块池中。快速清除分配以使块再次可用非常重要,因此作业不应保持分配超过几个帧。

您可以自定义块大小。分配器根据需要最多分配64个块。

编辑器的快速线程共享临时分配器的默认值
编辑器的快速线程共享临时分配器的默认值

如果所有块都在使用中,或者某个块的分配太大,则分配将回退到主堆分配器,这比作业分配器慢得多。一些溢出分配是可以的:一帧中有1 到 10个,或者几百个,尤其是在加载期间。如果溢出计数随每一帧而增长,则可以增加块大小以避免回退分配。但是,如果将块大小增加太多(例如,为了匹配场景加载等事件中的峰值使用),则可能会在播放过程中留下大量内存不可用。

例如:

[ALLOC_TEMP_JOB_4_FRAMES (JobTemp)]
  Initial Block Size 0.5 MB
  Used Block Count 64
  Overflow Count (too large) 0
  Overflow Count (full) 50408

在此示例使用情况报告中,0.5MB 块大小太小,无法容纳应用程序所需的作业内存,并且完整分配器导致大量分配溢出。

若要检查生成的帧溢出是否足够,请运行短时间,然后运行较长时间。如果溢出计数保持稳定,则溢出是加载期间发生的高水位线。如果溢出计数随着较长时间的运行而增加,则生成正在处理每帧溢出。在这两种情况下,您都可以增加块大小以减少溢出,但溢出在加载期间的重要性不如每帧。

自定义分配器

要自定义分配器设置,请执行以下操作之一:

  • 使用编辑器:
  1. 选择“项目设置”>“内存设置”。
  2. 选择要编辑的值旁边的锁定图标。
项目设置>内存设置,显示一系列播放器内存设置
项目设置>内存设置,显示一系列播放器内存设置
  • 使用命令行参数。 要查找要更改的分配器参数的名称,请检查编辑器和玩家在启动时打印的分配器设置列表。 例如,要更改主堆分配器的块大小,请使用-memorysetup-main-allocator-block-size=<new_value>

分配器参数名称及其默认值:

分配器描述参数名称默认值
主要分配器Unity 用于大多数分配的分配器。
主分配器Unity 用于大多数分配的主分配器。
主螺纹块尺寸专用主线程分配器的块大小。memorysetup-main-allocator-block-size16777216
共享线程块大小共享线程分配器的块大小。memorysetup-thread-allocator-block-size16777216
Gfx 分配器Unity 用于与 Gfx 系统相关的 CPU 分配的分配器。
主螺纹块尺寸专用主线程 Gfx 分配器的块大小。memorysetup-gfx-main-allocator-block-size16777216
共享线程块大小共享线程 Gfx 分配器的块大小。memorysetup-gfx-thread-allocator-block-size16777216
其他分配器
文件缓存块大小文件缓存有自己的分配器以避免碎片。这是它的块大小。memorysetup-cache-allocator-block-size4194304
类型树块大小类型树有自己的分配器,以避免由于许多小分配而导致碎片化。这是它的块大小。memorysetup-typetree-allocator-block-size2097152
共享存储桶分配器在主分配器之间共享的存储桶分配器。
存储桶分配器粒度共享分配器中存储桶的步长。memorysetup-bucket-allocator-granularity16
存储桶分配器存储桶计数存储桶大小的数量。memorysetup-bucket-allocator-bucket-count8
存储桶分配器块大小用于存储桶的内存块的大小。memorysetup-bucket-allocator-block-sizeEditor: 8388608
Player: 4194304
存储桶分配器块计数要分配的最大块数。memorysetup-bucket-allocator-block-countEditor: 8
Player: 1
快速每线程临时分配器线程本地存储 (TLS) 分配器,用于处理非常短暂的分配。
主螺纹块尺寸主线程堆栈的初始大小。memorysetup-temp-allocator-size-mainEditor: 16777216
Player: 4194304
作业辅助角色块大小Unity 作业系统中每个作业辅助角色的大小。memorysetup-temp-allocator-size-job-workerE262144
后台作业工作线程块大小每个后台辅助角色的大小。memorysetup-temp-allocator-size-background-worker32768
预载块大小预加载管理器堆栈大小。memorysetup-temp-allocator-size-preload-managerEditor: 33554432
Player: 262144
音频工作线程块大小每个音频工作线程的堆栈大小。memorysetup-temp-allocator-size-audio-worker65536
云辅助角色块大小云工作线程堆栈大小。memorysetup-temp-allocator-size-cloud-worker32768
Gfx 线程块大小主渲染线程堆栈大小。memorysetup-temp-allocator-size-gfx262144
地理标志烘焙块尺寸每个 GI 工作线程的堆栈大小。memorysetup-temp-allocator-size-gi-baking-worker262144
导航网格工作线程块大小导航网格工作线程堆栈大小。memorysetup-temp-allocator-size-nav-mesh-worker65536
快速线程共享临时分配器快速线性分配器,用于线程之间共享的短期分配。
作业分配器块大小Unity 主要用于作业工作线程的循环线性线程分配器。memorysetup-job-temp-allocator-block-size2097152
后台作业分配器块大小后台辅助角色的线性分配器,允许更长的生存期分配。memorysetup-job-temp-allocator-block-size-background21048576
低内存平台上的作业分配器块大小内存小于 2GB 的平台将此大小用于作业辅助角色和后台作业。memorysetup-job-temp-allocator-reduction-small-platforms262144
探查器分配器Unity 专门用于性能分析器的分配器,以便它们不会干扰应用程序的分配模式。
探查器块大小探查器主要部分的块大小。memorysetup-profiler-allocator-block-size16777216
编辑器性能分析器块大小性能分析器的编辑器部分的块大小。这在玩家身上不存在。memorysetup-profiler-editor-allocator-block-size1048576
共享探查器存储桶分配器探查器和编辑器性能分析器分配器的共享存储桶分配器。

内存不足的平台上不存在。
存储桶分配器粒度共享分配器中存储桶的步长。memorysetup-profiler-bucket-allocator-granularity16
存储桶分配器存储桶计数存储桶大小的数量。例如,如果值为 4,则大小为 16、32、48 和 64。memorysetup-profiler-bucket-allocator-bucket-count8
存储桶分配器块大小用于存储桶的内存块的大小。memorysetup-profiler-bucket-allocator-block-sizeEditor: 33554432
Player: 4194304
存储桶分配器块计数要分配的最大块数。memorysetup-profiler-bucket-allocator-block-countEditor: 8
Player: 1

提示:为确保您的设置提高性能,请在进行更改之前和之后分析应用程序。有关详细信息,请参阅探查器概述页面。您还可以检查内存使用情况报告。当您关闭播放器或编辑器时,它们在日志中可用。若要查找日志文件,请按照日志文件页上的说明进行操作。

存储和读取设置

Unity 将分配器设置存储在 中,这会在构建时使用修改后的设置填充文件。这意味着新设置在每次生成时都会生效。MemorySettings.assetboot.config

在编辑器中,位于文件夹中。每次 Unity 导入或更改时都会更新。编辑器的新值仅在下次编辑器启动时生效。boot.configProjectSettingsMemorySettings.asset

此文由3D建模学习工作室整理翻译,转载请注明出处!

上一篇:Unity3D:内存托管 (mvrlink.com)

下一篇:Unity3D:增量垃圾回收 (mvrlink.com)

NSDT场景编辑器 | NSDT 数字孪生 | GLTF在线编辑器 | 3D模型在线转换 | UnrealSynth虚幻合成数据生成器 | 3D模型自动纹理化工具
2023 power by nsdt©鄂ICP备2023000829号