跳到正文
Qiita · Unity 开发社区· 来源· matsu_r11·· 3 小时前AI 评分55

Unity 6.6默认启用mimalloc:原理与On/Off性能对比实测

Unity 6.6でデフォルトになったmimallocとは? 仕組みを理解してON/OFFを計測してみた

正文 · AI 翻译

はじめに

  • Unity 6.6では、ネイティブメモリのメインアロケータがmimallocにデフォルトで切り替わりました(6.5ではオプションでデフォルトOFF)
  • スレッドごとにヒープを持つことで、マルチスレッドでの確保・解放の取り合いを減らす
  • C#のGCとは無関係。ゲーム側のコード変更は基本的に不要
  • iOSは対象外(もともとシステムのアロケータを使っている)

メモリアロケータとは

简单来说,

当程序请求“请给我内存”时,系统会准备并交付实际可用的内存。

例如在C/C++的世界中,

プログラム : 「100bytes欲しい」
↓
メモリーアロケーター : 「メモリ上のこの場所を使ってください」
↓
プログラム : 「100bytes GET」

发挥着这样的作用。

使用完毕后,

プログラム : 「100bytesもういらない」
↓
メモリーアロケーター : 「その領域を再利用できるようにしておきます」

也承担着相应的职责。

allocate(确保) 和 free(释放)
的管理是其主要职责。

而重要的一点是,在确保内存时,
并不是每次都要向OS提出请求
这一点。

例如,在程序执行过程中,

100 bytes欲しい
200 bytes欲しい
64 bytes欲しい
500 bytes欲しい

即使被要求,
如果每次都向OS
“请给我100 bytes”
这样请求的话,效率会非常低。

因此,内存分配器是,

OS
↓
アロケータ
┌──────────────────────────────┐
│       大きなメモリ領域        │
└──────────────────────────────┘
↓
アロケータ
┌────┬────┬────┬────┬──────────┐
│100 │200 │ 64 │500 │          │
└────┴────┴────┴────┴──────────┘

像这样由自己进行细致管理。
因此,
内存分配器 = 高效地借出和回收内存的管理者
就是这样的定位。

与C# GC的区别

Unity中大致分为
「C#侧的内存」 和 「原生侧的内存」 。

C#侧的内存 C#托管内存 GC回收
原生侧的内存 原生内存 Unity 的原生分配器群进行管理(从 6.6 开始主要使用 mimalloc)

〇 C# 侧

var list = new List<int>();

此类 C# 对象的内存基本上分配在 C# 的托管堆上。
当不再需要时,

GC(垃圾回收器)

会进行回收。

〇 原生端

另一方面,在Unity内部,

  • Texture
  • Mesh
  • NativeArray等原生容器
  • 与资产相关的内部数据
  • 引擎内部的C++对象
  • Job System等内部处理
  • 各种原生数据

等等,与C#托管内存不同,会大量使用原生内存。
负责分配和释放这些内存的是本次
Native Memory Allocator
。
因此,
mimalloc = 替代C# GC的东西

并非如此。

DynamicHeapAllocator的问题

Unity 的传统 DynamicHeapAllocator 机制,据官方帖子说明,大致具有如下结构。

対象 机制 锁
小型分配(约64字节及以下) Bucket Allocator 无(无锁)
从主线程分配 主线程专用的动态堆(TLSF) 无
从其他线程分配 所有线程共享的动态堆(TLSF) 有

〇 分配器争用

DynamicHeapAllocator 的机制是,
除主线程外的其他线程在共享堆上操作时需要获取锁。
当一个线程持有锁时,其他线程必须等待。
这种多个线程争夺同一分配器的状态被称为 allocator contention(分配器争用)。

在旧式游戏中,

Main Thread
    ↓
ほとんどの処理

这样的形式也能成立。
然而,当前 Unity 越来越多地采用多线程运行模式,导致出现

Main Thread
     │
     ├── Job System
     │     ├── Worker 1
     │     ├── Worker 2
     │     └── Worker 3
     │
     ├── Render Thread
     │
     ├── Async Loading
     │
     └── その他Native Thread

这样的情况,
这些情况会同时申请和释放原生内存。

尽管通过增加 CPU 核心实现了并行化,但在内存分配环节却出现了所有线程竞争同一个锁的现象
这一现象随之发生。

mimalloc / 每个线程的堆

〇 mimalloc这一机制

mimalloc是由微软开发的开源通用内存分配器。

Unity的官方帖子中将mimalloc介绍为“具有每线程堆(per-thread heap)和无锁高速路径的分配器”。
在常规的分配与释放操作中,线程之间不会争夺锁。

〇 什么是 per-thread heap?

顾名思义,per-thread = 为每个线程准备的堆。

Thread A
   ↓
┌─────────────┐
│ Heap A      │
└─────────────┘

Thread B
   ↓
┌─────────────┐
│ Heap B      │
└─────────────┘

Thread C
   ↓
┌─────────────┐
│ Heap C      │
└─────────────┘

A在分配内存时,
不再需要“获取共享堆的锁”

而是可以
“从自己的堆中分配”
。

这减少了典型分配路径中线程之间发生冲突的机会。
mimalloc 本身也采用了使用线程局部堆和段来减少竞争的设计。

Unity 6.6 中设为默认

mimalloc 本身在 Unity 6.5 时就已经作为可选选项存在,但从 Unity 6.6 开始,该设置默认开启。

image.png
Project Settings > Memory Settings > Main Allocator > Mimalloc enabled

实际上的ON/OFF比较

以下是在Windows 11上,使用mimalloc开启与关闭的运行文件进行的比较。

計測環境

  • Unity 6000.6.0f1
  • Windows 11、Intel Core i7-8750H(6核12线程)、内存16GB
  • Windows独立版本发布构建(Mono,Burst启用)
  • 11个作业工作器+主线程

計測方法

在通过 Burst 编译的作业中,UnsafeUtility.Malloc / Free(Allocator.Persistent)被大量重复执行,并测量了所花费的时间。
每个场景在3次预热后进行了10次测量,取其中位数(median)。
尚未在 Android 真机上进行验证。

关闭时

image.png

开启时

image.png

尽管内容难以看清,但在所有测量中,启用 mimalloc 的处理速度都更快。

名前 正在做什么
Small / Medium 确保的内存大小。Small 为较小(16〜256字节),Medium 为中等(1〜16KB)
MainThread 仅通过主线程大量进行内存分配与释放
Parallel 将相同量的处理分配到12个线程中并行执行
Cross_WorkerAlloc_MainFree 在工作线程中分配,在主线程中释放
Cross_MainAlloc_WorkerFree 在主线程中分配,在工作线程中释放
alloc / free 分配耗时 / 释放耗时
场景 关闭 开启 开启时的速度
Churn_Small_MainThread 16.735 毫秒 6.701 毫秒 約2.5倍
Churn_Small_Parallel 31.008 毫秒 1.363 毫秒 約22.7倍
Churn_Medium_MainThread 21.381 毫秒 14.026 毫秒 約1.5倍
Churn_Medium_Parallel 38.890 毫秒 2.408 毫秒 約16.2倍
Cross_WorkerAlloc_MainFree(确保) 11.381 毫秒 0.999 毫秒 約11.4倍
Cross_WorkerAlloc_MainFree(释放) 5.705 毫秒 2.631 毫秒 約2.2倍
Cross_MainAlloc_WorkerFree(确保) 6.975 毫秒 3.006 毫秒 約2.3倍
Cross_MainAlloc_WorkerFree(释放) 5.971 毫秒 0.839 毫秒 約7.1倍

从结果中可以得知

在 OFF 状态下,并行处理反而会变慢
将相同量的处理分配到 12 个线程中时,比单线程时慢了约 2 倍(即速度变为原来的 0.54 倍)。
Unity 的官方帖子也指出,当多个线程争用同一个锁时,增加核心数并不能解决问题,
相反,核心数越多情况反而越糟。
此外,还解释说等待锁的线程可能会被操作系统挂起,导致恢复所需的成本可能超过获取锁本身的成本。

在 ON 状态下,线程越多速度越快
在 ON 状态下,速度提高了约 5 到 6 倍。本次测试使用的 CPU 为物理 6 核,因此性能提升幅度接近核心数量。

主线程单独运行也变快了
Unity 的官方基准测试结果显示,在单线程和多线程模式下 mimalloc 都更快。

发布版本的测量
Unity 的官方基准测试为了观察最坏情况,使用的是 development + profiler 构建进行测量的。
本次我们使用发布版本进行了测量,并确认了相同的趋势。

※图片中的内存值(allocated/reserved)是 Profiler API 的值,并非物理内存的使用量,因此不作为比较对象。 Unity 的官方帖子中说明,通过调整 mimalloc 的设置,物理内存的使用量与以往相同或更少。

用于比较创建的脚本
https://gist.github.com/Matsuta211/1b3a001453531ef9fc83fbc337f23fce

最后

mimalloc 将从 Unity 6.6 开始默认启用,游戏侧无需更改代码。

ただし、効果の大きさはゲームによって異なります。
Job Systemや非同期ロードなど、ワーカースレッドでネイティブメモリを多く確保するゲームほど恩恵が大きく、
メインスレッド中心のゲームでは、体感できる差は小さいかもしれません。

一方で、これまで懸念だった「ワーカースレッドでの確保は、ロックの取り合いで遅くなる」というコストは大きく下がりました。
Job Systemを使った並列化が、より選びやすくなったと言えます。

※Unityをアップグレードした既存プロジェクトで、この設定がONになるかどうかは未確認です。
Project Settings > Memory Settings で確認してください。

参考

来源:Qiita · Unity 开发社区 · qiita.com