闪存优化
闪存用于存储游戏数据和资源,包括游戏数据、纹理、音频等。闪存的读写速度比内存慢得多,因此在游戏中,我们需要尽可能地减少闪存的读写次数,以提高游戏的性能。
闪存结构和文件操作流程
在性能优化-基础 中,介绍了相关的基础知识,为了文章的完整性,简单回顾一下。
- 闪存结构
- SOC系统中的闪存一般采用NAND Flash或NOR Flash,作为非易失性存储器,用于存储操作系统、应用程序、资源文件等。
- 闪存通过总线(如SPI、eMMC、UFS等)与SOC主控芯片连接。
- 文件读写流程
文件读取:
- CPU发起文件读取请求,操作系统通过文件系统(如FAT、EXT4等)定位文件在闪存中的物理地址。
- 文件系统驱动将读取命令通过总线发送到闪存控制器。
- 闪存控制器根据地址从闪存芯片中读取数据,经过总线传输到SOC的内存(RAM)中。
- CPU从内存中获取数据进行处理。
文件写入:
- CPU将需要写入的数据放入内存缓冲区。
- 操作系统通过文件系统分配闪存空间,并生成写入命令。
- 写入命令和数据通过总线传递给闪存控制器。
- 闪存控制器将数据写入指定的闪存地址。
- 写入完成后,文件系统更新元数据,保证数据一致性。
应用层API:
- fopen, fread, fwrite, fclose等函数封装了文件操作的底层细节,但最终都是调用到操作系统API。
- 也可以使用内存映射的方式,将文件映射到内存中,直接操作内存,避免了文件直接调用API读取的过程, 映射后可以直接操作指针的方式读取和写入内存。
内存映射(Memory Mapping)机制及其优势
内存映射是指将磁盘文件直接映射到虚拟内存空间,从而支持按需访问,避免整体加载:
- 传统 I/O 模式:调用
Read()
一次性将整个文件加载至内存;内存占用高。 - 映射模式:调用 OS 提供的
mmap()
或MapViewOfFile()
,按页访问数据;只有实际访问的数据才加载。
优点包括:
- 降低内存消耗,提升启动速度
- 支持文件级懒加载和异步加载
- 适合大型资源(如场景/AssetBundle)流式访问
Unity 的 AssetBundle 加载系统内部即使用此策略,在移动平台和主机平台中应用广泛。
序列化(Serialization)
序列化的概念
Unity 的序列化(Serialization)体系分为编辑器写盘阶段和运行时读盘阶段,它同时支持 二进制格式(Binary SerializedFile)和 文本(YAML)格式。编辑器在构建场景、AssetBundle 或玩家(Player)时,将所有 UnityEngine.Object 派生的对象及其字段按照 “Type Tree + 对象数据” 的方式写入磁盘;运行时则根据磁盘上的 Type Tree 快速定位并重构内存中的 C++ 对象,并通过隐藏指针 m_CachedPtr 将之挂载到对应的 C# 托管对象上。二进制格式在读写时采用专门的 C++ 引擎代码和内存拷贝技术,支持内存映射 (.resS/.resource) 与多线程解压,是极高效的;而文本(YAML)格式则使用文本解析器和反射,仅在编辑器中针对小规模场景或开启 “Force Text” 时使用
序列化数据结构
Unity 采用自定义的 TypeTree 类型树机制进行资源的序列化与反序列化。其核心结构如下:
1. SerializedFileHeader
用于描述 .assets
文件的头部信息。
1 | class SerializedFileHeader { |
metadataSize
:元数据(TypeTree、ObjectInfo等)大小。fileSize
:整个资源文件的总大小(包括元数据和二进制数据)。version
:Unity版本序列化的版本,不是引擎版本(如0x0D
表示某个格式版本)。dataOffset
:实际资源(如贴图、音频等二进制数据)在文件中的偏移位置。endianness
:字节序标记(0 = 小端,1 = 大端)。reserved
:保留字段(通常为16字节,填0)。
2. SerializedType
描述一个类型信息,包括该类的ID、TypeTree结构等。
1 | class SerializedType { |
classID
:Unity内部定义的类ID(如1=GameObject
,28=Texture2D
)。isStrippedType
:是否为裁剪类型(strip=True 表示此类型已裁剪,仅保留元信息)。scriptTypeIndex
:脚本索引(用于 MonoBehavior / ScriptableObject 指向具体脚本)。typeTree
:该类型的字段结构树。scriptIDHash
:用于 MonoScript 类型的 GUID 哈希值。typeHash
:用于校验类型树是否匹配。
3. TypeTree
表示一个复杂类型的字段结构信息。
1 | class TypeTree { |
nodes
:字段树结构的节点数组(深度优先遍历展开)。className
:类型的名称(如MonoBehaviour
,GameObject
)。
4. TypeTreeNode
字段节点的定义,描述一个字段的名称、类型及其在内存中的表现。
1 | class TypeTreeNode { |
type
:字段的类型名称(如int
,float
,Vector3
,string
)。name
:字段名称。byteSize
:该字段在内存中的大小。index
:字段在TypeTree中的顺序索引。metaFlag
:元标志位(控制序列化行为,例如是否为Align16等)。version
:该字段引入的版本(可用于版本判断)。depth
:字段在嵌套结构中的深度(用于还原结构树)。isArray
:是否是数组类型字段。
5. ObjectInfo
表示 .assets
文件中的某个对象的基本信息。
1 | class ObjectInfo { |
byteStart
:该对象数据在文件中的偏移。byteSize
:数据块的大小。typeID
:序列化类型在类型表中的索引。classID
:类型的ClassID。scriptTypeIndex
:用于脚本类的索引。pathID
:唯一标识该对象的ID(可用于引用)。
6. LocalSerializedObjectIdentifier
描述某个对象在当前 .assets
文件中的引用信息。
1 | class LocalSerializedObjectIdentifier { |
localFileID
:引用对象的pathID
。
7. FileIdentifier
用于跨文件引用对象时的文件信息。
1 | class FileIdentifier { |
assetPath
:引用文件的路径(在Editor中可显示)。assetGUID
:引用资源的GUID。localFileID
:在目标文件中的对象标识符(pathID)。
这些结构一起组成了 Unity 序列化格式的核心结构。
8. SerializedFile文件的结构
下图展示了SerializedFile文件的结构示意图。该结构遵循 Unity
的序列化文件格式,一般用于
.assets
、.sharedAssets
、.bundle
中的资源文件:
1 | +-------------------------+ |
SerializedFileHeader:标记整个文件的版本、大小、数据偏移和字节序。
Metadata Section:所有类型信息、对象信息、引用信息等元数据。
- SerializedType[]:包含该文件中所有用到的类型的结构(包括TypeTree)。
- ObjectInfo[]:文件中所有对象的地址、大小、类型等信息。
- ScriptTypes:如果有MonoBehavior等脚本资源,会引用脚本GUID等。
- FileIdentifier[]:跨文件引用(external object)用的外部资源列表。
Data Section:资源本体,存放贴图、Mesh、声音等对象的实际数据。
可根据 ObjectInfo.byteStart
和
ObjectInfo.byteSize
从 Data Section
中读取具体对象内容,并通过 TypeTree 解析其结构。
编辑器 YAML 模式与打包二进制格式的转换关系
- 编辑器开发阶段:可启用
Force Text
模式,使资源以 YAML 格式存储,便于版本控制与协作审阅。 - 打包阶段(BuildPlayer / AssetBundle):所有 YAML 文本会被自动转化为二进制序列化形式,以提高加载性能和压缩比。
最终发布版本中的所有资源都以 Unity 自定义的二进制格式存储,并不再保留 YAML 表达。
AssetBundle文件结构
AssetBundle
是一个各种资源序列化后的集合,包括脚本、纹理、模型、音频等资源。
.unity3d
或 .bundle
等资源打包文件时,会使用
BundleFile
的结构。它遵循 Unity 的 UnityFS
或
UnityRaw
等打包格式。以下是其中关键结构体
Header
、StorageBlock
和 Node
的字段详细说明及其作用。
1. Header
Bundle 文件的开头部分,描述整个文件的基本结构和偏移信息。
1 | class BundleFileHeader { |
字段说明:
signature
:表示打包格式,常见为UnityFS
,决定了后续字段的解析方式。version
:格式版本,不同版本字段解释有细微差异。unityVersion
/unityRevision
:提供构建信息,用于兼容性判断。size
:整个 bundle 文件长度,用于验证文件完整性。compressedBlocksInfoSize
/uncompressedBlocksInfoSize
:紧随其后的 BlocksInfo 数据块大小(压缩前后)。flags
:- 位 0:BlocksInfo 是否嵌入到 header 后部(否则在文件尾部)。
- 位 1:BlocksInfo 是否经过压缩(如 LZ4, LZMA)。
2. StorageBlock
描述 bundle 文件中每一个数据块的压缩与存储方式。
1 | class StorageBlock { |
字段说明:
uncompressedSize
:该数据块在解压后的字节数。compressedSize
:实际在 bundle 文件中存储的压缩数据大小。flags
:标志当前数据块使用的压缩算法:0x00
:无压缩(None)0x01
:LZMA0x02
:LZ40x03
:LZ4HC
这些数据块会依序排列,拼接出完整的资源数据流。
3. Node
描述 bundle 中的虚拟文件结构(例如一个 .assets
文件或其他资源)。
1 | class Node { |
字段说明:
offset
:该虚拟文件在整个资源流(解压后的数据拼接体)中的偏移位置。size
:该文件的长度。flags
:通常恒为0,可忽略。path
:在 bundle 中定义的资源文件名称。例如:CAB-xxxxxx
,或sharedassets0.assets
。
4. Bundle文件结构图
1 | +------------------------------+ |
Addressables
Unity 的 Addressables 系统是 Unity 提供的一套高级资源管理与异步加载框架,它基于 AssetBundle 构建,但提供了更灵活的资源打包、定位与生命周期管理机制。以下从组件结构、实现机制和与传统 AssetBundle 的差异三个方面进行详细说明。
Addressables 的主要组件
- Addressable Asset Settings (地址配置)
- 位于
Assets/AddressableAssetsData
。 - 包含全局配置(如构建平台、资源定位方式、Profiles 设置等)。
- AddressableAssetGroup
Addressable 的资源分组单位。
每组可以设置独立的打包策略、加载路径、构建方式。
常见的 Group 类型:
- Static Content:固定内容,随包一同发布。
- Remote Content:远程 CDN 加载资源。
- AddressableAssetEntry
- 每个被标记为 addressable 的资源,对应一个 Entry。
- 包含资源路径、地址名、Label 等属性。
- Profile Settings
- 支持设置多个环境(开发、测试、发布)下的变量。
- 如:
RemoteLoadPath = http://cdn.mycompany.com/[BuildTarget]
- Content Catalog
- 构建时生成的 JSON 文件,描述所有资源的位置与依赖信息。
- 加载 Addressable 时,先加载 Catalog。
- ResourceLocator
- Catalog 解析后生成的结构,负责将地址映射为资源路径。
- Addressables.LoadAssetAsync() 内部依赖它完成地址到路径的解析。
- ResourceManager
- Addressables 底层的加载调度中心。
- 管理加载任务、依赖树、引用计数、缓存。
- IResourceProvider
- 抽象加载提供器接口。
- 可自定义如从网络、本地磁盘、WebGL 缓存中加载资源。
Addressables 与 AssetBundle 的区别
- 资源定位机制
- AssetBundle:通过资源路径或 Bundle 名称定位资源,依赖关系由开发者显式管理。
- Addressables:通过地址系统与 Catalog
文件进行统一索引,通过
IResourceLocator
+ 哈希索引管理资源映射,解耦了资源名与加载方式。
- 加载调度系统
- AssetBundle:需手动管理 AssetBundle 加载、依赖、释放、缓存。
- Addressables:封装在
ResourceManager
与AsyncOperationHandle
中,自动管理引用计数、自动卸载、自动释放依赖。
- Catalog + RuntimeData
- Addressables 构建生成的
catalog_xxx.json
是核心索引文件,记录资源地址、哈希、依赖、提供器类型。 RuntimeData
是构建期生成的内部数据结构,包括本地清单、远程路径映射、默认Provider绑定信息。
- 提供器架构(Provider)
- Addressables 使用
IResourceProvider
接口(如:BundledAssetProvider、TextDataProvider、AtlasSpriteProvider 等)支持加载多种资源类型。 - 每种资源类型可配置独立 Provider,也可扩展自定义 Provider 支持版本控制、加密等。
- 构建流程
- Addressables 构建流程为:分析分组 -> 计算依赖 -> 构建 AssetBundle -> 生成 Catalog 和链接关系 -> 可选生成 BuildLayout 文件(用于调试和分析)
- 相比之下,AssetBundle 仅构建资源和依赖 Bundle,没有结构化 Catalog 文件。
- 远程与热更新机制
- Addressables 提供
CheckForCatalogUpdates
、DownloadDependenciesAsync
等接口直接进行差异比对与远程下载。 - 核心机制是基于 Catalog 哈希比对与本地缓存标记,而传统 AssetBundle 需开发者手动构建下载/更新逻辑。
常用 API
1 | // 通过地址加载资源 |
构建与热更新使用流程示例
- 构建 Addressables 内容(在编辑器中)
1 | // 编辑器中构建 Addressables |
- 检查 Catalog 更新与远程热更新资源
1 | // 检查是否有 Catalog 更新 |
- 工程中加载远程资源的完整流程
1 | IEnumerator LoadRemoteAsset(string key) |
- 加载本地资源
1 | // 本地路径加载方式相同,Addressables 自动判断是否为本地 Bundle |
常用资源
核心资源类型划分与内存模型
Unity 支持的资源种类涵盖从图形渲染、音频播放到逻辑控制的方方面面,其常见分类及用途如下:
- Texture2D / Texture3D / Cubemap:图像纹理资源,用于 2D/3D 贴图、天光、环境映射等。
- Mesh:模型资源,包含顶点坐标、法线、UV、切线、索引等几何结构数据。
- AnimationClip:关键帧动画与曲线数据集合,驱动模型变换、骨骼动画等。
- AudioClip:用于存储原始音频数据,支持 PCM、ADPCM、Vorbis 等格式。
- Shader / Material:图形着色语言代码与其参数封装体,负责控制表面渲染效果。
- Font:字体资源,支持 TrueType、OpenType 等格式。
- TextAsset:存储任意文本或二进制文件内容的通用资源。
- VideoClip:视频数据资源,供 VideoPlayer 播放使用。
- Sprite:2D 图像切片,常用于 UI 元素或 2D 动画。
- Prefab:预制体资源,是组件与 GameObject 层级结构的序列化封装。
- Scene:场景资源,描述场景中所有对象的状态与引用。
- ScriptableObject:自定义数据容器,广泛用于游戏逻辑配置。
- AnimatorController / StateMachine:动画控制器与状态机配置资源。
这些资源在运行时均由 C++ 层的原生结构(NativeObject)表示,其 C#
封装对象中通常包含一个 m_CachedPtr
字段,指向真实的数据结构体。
资源导入器 Importer 与转换管线
Unity 的 Importer 系统负责解析外部资源格式,并将其转换为统一的中间表示与持久化形式。不同类型的资源使用不同的 Importer 进行处理。以下是典型资源的转换路径:
Importer | 支持的原始格式 | 配置结构(来自 .meta) | 中间资源结构(Library 存储) |
---|---|---|---|
TextureImporter | .png, .jpg, .tga, .dds | TextureImporterSettings + TextureSettings | Texture2D + StreamingInfo |
ModelImporter | .fbx, .obj, .dae | RigImportSettings + MeshImportSettings | Mesh / Avatar / AnimationClip |
AudioImporter | .mp3, .wav, .ogg | AudioImporterSettings | AudioClip |
ShaderImporter | .shader | ShaderImportSettings | Shader + ShaderSubProgram |
AnimationClipImporter | .anim | AnimationClipSettings + CurveMappings | AnimationClip |
VideoClipImporter | .mp4, .mov | VideoImporterSettings | VideoClip |
ScriptImporter | .cs | MonoImporterSettings | MonoScript |
导入完成后的资源对象会被序列化并缓存至 Library/
目录,以优化后续构建和编辑流程。
NativeObject 内存结构分析
所有 Unity 的 C# 资源对象(如 Texture2D)底层都是对原生 C++
结构体的托管包装,其核心字段 m_CachedPtr
指向内存中的实际数据。
以 Texture2D 为例,其底层结构类似如下:
1 | struct Texture2D : Texture { |
运行时,Unity 的对象系统维护一张对象表,用于管理所有原生对象的生命周期、引用关系及其与托管对象的映射。
内存
PC 内存架构与管理机制
架构特性
- 分离内存结构:PC 通常配备独立 CPU 主内存(DDR4/DDR5)与 GPU 专用显存(GDDR),资源在 CPU 与 GPU 间通过 PCIe 总线传输,需注意显存带宽和 Host-to-Device 的数据拷贝开销。
- 地址空间更大:64 位操作系统提供大于 4GB 的虚拟地址空间。现代 Windows/Linux 提供每进程高达 TB 级别的虚拟空间。
- NUMA(非一致性内存访问):多核 CPU 常见于服务器/桌面端,存在多个物理内存节点,跨 NUMA 节点访问将增加延迟。
操作系统内存管理
- 分页系统:操作系统使用分页机制管理内存,常用 4KB 页,并支持大页(Huge Page)。通过页表(Page Table)实现虚拟地址到物理地址的映射。
- 页面换出(Swapping):在内存不足时,系统将部分内存页写入磁盘(Swap File/Swap Partition),释放物理内存用于高优先级任务。Unity 应用在 Swap 下运行可能出现显著卡顿或帧率下降。
- 内存保护机制:通过页属性标识读/写/执行权限,防止非法访问,如访问空指针或释放后的内存会触发“段错误”(Segfault)。
手机内存硬件与系统层
操作系统的内存申请、使用与释放流程
- 内存申请(Allocation):操作系统通过页分配器(如
vm_allocate
on iOS,mmap
on Linux/Android)按页为应用分配虚拟地址空间,通常以 4KB 页为单位。分配时可能并未实际占用物理内存,只有在访问该页时(写入)才会触发“页错误”并分配真实物理页(按需分配策略)。 - 内存使用(Access):当应用访问虚拟内存时,操作系统通过页表(Page Table)和 TLB(Translation Lookaside Buffer)将虚拟地址映射到物理地址。部分内存页可能被标记为只读或共享页,以提高安全性与效率。
- 内存释放(Deallocation):当进程释放内存(如通过
free()
、munmap()
或系统层回收机制),对应虚拟页会被从页表中移除,物理页被标记为“可回收”。在某些平台上,这些页可能仍暂时保留(作为缓存),直到系统主动回收。 - 后台回收与内存压力响应:在内存压力高时,系统会回收不活跃页(Inactive Pages)、清除 Cache、或者直接终止后台进程(如 Android 的 LMK,iOS 的 Jetsam)。内存释放过程是懒惰式的,尽可能避免频繁回收和再次分配。
iOS 内存架构与管理机制
- 统一内存架构:iOS设备通常采用Unified Memory Architecture (UMA),CPU 和 GPU 共享 LPDDR 系统内存,无需显式拷贝资源至独立显存,有利于资源访问效率,但也意味着 GPU 消耗的内存直接挤占 CPU 可用内存。
- 无Swap机制:iOS 虽使用虚拟内存地址系统,但不允许 swap 到磁盘,所有内存申请都必须在物理RAM范围内完成。
- 内存警告机制:应用若占用过多内存,系统会触发
UIApplicationDidReceiveMemoryWarningNotification
或 SwiftUI 的@Environment(\.memoryWarning)
。若未及时响应释放资源,系统可能直接终止应用。 - 后台内存淘汰策略:在后台,iOS 会依据 memory pressure 使用 Jetsam 策略驱逐后台进程。Jetsam 是 Darwin 内核中 OOM 终结者,对内存压力进行 PID 层级逐步回收。
Android 内存架构与管理机制
- 设备差异化严重:Android 设备从低端 512MB 到高端 16GB RAM 均有,需针对低端设备优化路径分离。
- Zygote预加载机制:Android 使用 Zygote 机制共享系统框架库,在进程fork时减少初始化成本和内存消耗。
- GC管理机制:ART VM 默认采用并发分代垃圾回收(Concurrent Generational GC),分为 Eden、Survivor、Old 区,GC Pause Time 控制至 10ms 以内。大对象(>256K)直接进入老年代。
- 内存回收接口:应用生命周期中可通过
onTrimMemory()
钩子处理如 TRIM_MEMORY_BACKGROUND / RUNNING_LOW 等状态,及时释放缓存资源。 - 堆限制查询:可通过
ActivityManager.getMemoryClass()
或getLargeMemoryClass()
获取堆上限(如 128MB, 256MB 等)。
Unity引擎内存结构与使用方式
Unity 内存划分详解
- 托管内存(Managed):C# 层对象,如 GameObject、MonoBehaviour、ScriptableObject 等,受 GC 控制。GC 不可预期,建议使用对象池避免频繁触发。
- 原生内存(Native):Unity 内核层 C++ 数据结构:渲染、粒子系统、动画状态机、NavMesh 等。无法被 C# 的 GC 控制,需手动释放资源(如 Mesh、Texture、ComputeBuffer)。
- 图形内存(Graphics/GPU):上传到 GPU 的资源,包括贴图、Mesh、ShadowMap、RenderTexture 等。在 OpenGL ES / Metal / Vulkan 下共享系统内存,容易与其他内存抢占资源。
- 临时内存(Temp Allocator):帧内临时使用,如命中测试、物理检测、Render Loop中间态等,大小固定(4MB),超出将退化为 Heap Alloc,导致 GC Alloc 增加。
Unity 移动端内存行为
- Memory Profiler 分类:分为 Total Reserved、Total Used、Mono、Gfx、Other,便于定位问题。
- IL2CPP 与 Mono 差异:IL2CPP 性能更好、GC 控制更紧,代码大小增加,Native 内存增加;Mono 适合编辑器或开发测试环境。
- 默认内存策略:资源尽量异步加载、使用 Addressables 精细控制生命周期,避免热区资源常驻内存。
Unity 开发中的内存优化技巧
纹理优化
压缩格式选择:
- iOS:推荐使用 ASTC(质量/大小可控);老设备兼容使用 PVRTC(压缩质量低)。
- Android:优先 ASTC,其次 ETC2,最低 ETC1(无 Alpha)。Unity 可通过平台导入设置区分纹理压缩格式。
MipMap 与 StreamingMip:开启 Mipmap 可节省 GPU 带宽;结合
Texture Streaming
可按需加载低分辨率贴图,显著降低内存占用。Atlas 图集管理:将小纹理合并为大图集可减少材质切换和 Draw Call,但需控制尺寸,避免合图过大导致 StreamingMip 效率低下。
网格与渲染优化
Mesh Compression:在导入设置中开启 Mesh Compression(如 Medium/High)减少顶点数据占用。
静态合批与动态合批:
- Static Batching:占用更多内存,但极大减少 Draw Call。
- Dynamic Batching:限制顶点数 < 900;小物体适合。
GPU Instancing:对重复物体使用 Instancing 替代合批,节省 CPU → GPU 的通信开销。
遮挡剔除/Frustum Culling:关闭不可见对象渲染,降低 GPU 运算压力与带宽使用。
动画系统优化
- Animator Culling Mode:设置为
Cull Update Transforms
可在角色不在视野时停止动画更新。 - Bone 限制:控制单角色骨骼数,限制每顶点绑定骨骼数量(<4)。
- 动画压缩设置:启用
Optimal
压缩方式,移除冗余关键帧,减少曲线数据。 - Bake 动画:将运行时动画 Bake 到 Transform 减少实时计算。
Addressables 使用技巧
生命周期管理:
- 每次
LoadAssetAsync
后应配对Addressables.Release
;否则引用计数未清零,导致内存泄漏。
- 每次
异步加载场景与资源:使用
Addressables.LoadSceneAsync
/InstantiateAsync
异步加载,减少卡顿;场景切换完成后手动UnloadSceneAsync
和Release
。多平台资源分包:资源按平台/分辨率拆包(如 HD/SD),避免高配资源在低端设备加载。
GC与堆内存优化
减少GC Alloc:
- 避免每帧字符串拼接、LINQ。
- 使用对象池代替频繁 Instantiate/Destroy。
- 使用
Struct
替代 Class 避免堆分配。
增量GC(Incremental GC):Unity 2019+ 支持,分帧执行 GC,避免卡顿尖峰。
Memory Profiler:定期生成快照,比较引用链定位泄漏路径(如 ScriptableObject 没有正确释放)。
工具链
- Unity Profiler:观察 GC Alloc、Native Mem、Gfx Mem 曲线,分析每帧内存使用。
- Memory Profiler(Package):快照对比、引用路径分析。
- Android Profiler / Xcode Instruments:分析 Native 层使用、Java 层 GC/Leaks。
- ADB shell:结合 lowmem-killer/stressapptest 模拟 OOM 场景。
平台差异与特别注意事项
Android 特有限制
- 部分 Android Go 设备堆上限仅 128MB,务必支持资源降级。
- 部分 GPU(如旧 Mali)不支持 ASTC,需动态切换纹理格式。
- Android 的 RenderTexture 默认内存常驻,需主动销毁。
iOS 特性优化
- 使用
RenderTextureMemoryless.Depth
减少 Tile Memory 压力。 - iOS Metal 统一内存访问快,但若分辨率过高,GPU 资源争抢更激烈。
- 使用
OnLowMemory()
钩子及时清理缓存资源,防止 Jetsam Kill。
代码优化(CPU时间)
明白了。我将整理一份针对 Unity 中使用 C# 和 XLua 的 CPU 优化方案,适用于 3D SLG 游戏,覆盖 PC、iOS 和 Android 平台,并对比 Mono 与 IL2CPP 的差异,同时包括 Unity Profiler 工具在优化过程中的使用建议。 请稍等片刻,我会尽快为你准备好详细内容。
常见C#脚本瓶颈及优化建议
GetComponent/查找:频繁调用
GetComponent
、Find
、Camera.main
等会遍历对象池,非常耗时,应在Awake
/Start
中缓存引用。如下例所示,将transform
缓存到字段中避免重复查找:1
2
3
4private Transform _transform;
void Awake() {
_transform = transform; // 缓存 Transform
}对于
Camera.main
、标签查找等,同样应缓存或在 Inspector 里引用。Update 调用开销:每个挂
Update
的MonoBehaviour
都会产生管理开销,数量过多时影响极大。当游戏中数百或上千个物体需要每帧更新时,建议使用全局管理器统一调度,而不是每个对象独立Update
。如 Unity 官方建议,将需要更新的对象注册到单例管理器中,由管理器在Update
中遍历调用:1
2
3
4
5
6
7
8
9
10
11
12
13
14public class UnitManager : MonoBehaviour {
private static readonly List<Unit> _units = new List<Unit>();
public static void Register(Unit u) { _units.Add(u); }
public static void Unregister(Unit u) { _units.Remove(u); }
void Update() {
float dt = Time.deltaTime;
foreach (var u in _units) u.CustomUpdate(dt);
}
}
public class Unit : MonoBehaviour {
void OnEnable() { UnitManager.Register(this); }
void OnDisable() { UnitManager.Unregister(this); }
public void CustomUpdate(float dt) { /* 单位逻辑 */ }
}这样能避免大量原生到托管回调切换的开销。
委托和事件:C#的委托在添加/移除回调时会复制内部列表,频繁的订阅/取消订阅会造成大量开销。对于高频率的事件管理,尽量避免直接使用
delegate
;可以改用自定义的可快速插入/删除的数据结构(如List<Action>
或用户管理的事件分发机制)。在更新管理器中使用委托订阅时要注意,如果每帧动态添加/移除,性能会急剧下降。反射:反射调用极慢,比直接调用慢数百到上千倍。优化思路是绕开反射:可预先缓存
MethodInfo
/FieldInfo
,或使用Delegate.CreateDelegate
、Expression
树等生成委托来调用。注意 IL2CPP 模式下不支持IL.Emit
和动态表达式,最好改用委托或指针操作。总之,能用常规代码解决的场景尽量不要使用反射,若必须使用,要做缓存处理。结构体与装箱:频繁使用值类型(struct)或属性访问可能带来隐性开销。例如反复使用
Vector3
自带字段可能会装箱。优化建议是将多个值型参数打平成静态方法,避免在Lua或 C# 侧创建额外对象。例如:1
2
3
4
5
6public static class TransformUtil {
// C#静态方法一次性设置坐标,比Lua逐字段赋值快得多
public static void SetXYZ(this Transform t, float x, float y, float z) {
t.position = new Vector3(x, y, z);
}
}XLua调用时
transform:setXYZ(x,y,z)
的效率比transform.position = {x=..}
高数倍。内存分配与GC:运行时大量分配会触发 GC,影响帧率。应尽量避免在
Update
或热路径中新建对象。例如,不要每帧new List<>()
或拼接字符串,而应复用容器或使用对象池。如下示例,将列表提前创建并清空:1
2
3
4
5
6
7private static readonly int listCapacity = 100;
private readonly List<int> _list = new List<int>(listCapacity);
void Update() {
_list.Clear();
for(int i=0; i<listCapacity; i++) _list.Add(i);
// … 使用_list
}这样避免了每帧构造新列表的
GC.Alloc
。另外,应小心使用foreach
遍历(旧版本Unity会因 Enumerator 装箱导致 GC),常规可改用for
循环或缓存迭代器。Lambda与闭包:在 Lambda 表达式中捕获局部变量或实例字段会产生闭包对象,触发额外分配。尽量避免频繁在回调中使用捕获局部变量,如不需要可用静态变量替代。
其他注意点:避免每帧生成大量字符串(可用
StringBuilder
)、禁用未使用的组件或脚本、合理拆分场景避免运行时加载过多对象等。
XLua脚本性能优化
C#⇆Lua 交互开销:XLua 等桥接方案每次从Lua侧访问C#对象时,都会经过
ObjectTranslator
的查表、推栈/取值等过程,开销极大。例如gameobj.transform.position = pos
这样的调用在Lua中会经历多次翻译和分配,导致大量 CPU 时间和 GC 分配。优化方法是减少跨语言调用次数:- 缓存C#对象:将常用 C# 对象保存在Lua局部变量中,避免重复查找。
- 静态函数替代成员:尽可能将逻辑封装为静态方法导出,避免在Lua中每次访问实例成员都要做对象查找。比如写成
LuaUtil.SetPos(obj, x,y,z)
用原生C#方法设置位置,可省去transform中间对象的反复创建,提升明显。 - 减少参数/返回类型复杂度:尽量使用基本类型作为参数和返回,避免在Lua和C#之间频繁传递
Unity 特有的值类型(如
Vector3
、Quaternion
)或数组,因为它们需要多次栈操作和内存分配。例如,把void SetPos(GameObject obj, Vector3 pos)
拆成void SetPos(GameObject obj, float x, float y, float z)
,从测试来看会快得多。
值类型与GC优化:XLua支持在C#代码中为值类型加
[GCOptimize]
属性,用来优化 C#<–>Lua 之间的值类型传递。带GCOptimize
的普通值类型(纯值域的 struct、枚举及其数组)在传递时可以避免 GC 分配。建议对需要频繁在Lua中传递的自定义 struct 添加[GCOptimize]
和[LuaCallCSharp]
标记。Lua脚本本身优化:在Lua侧也要尽量减少中间创建。使用局部变量而不是全局,每帧循环中避免用
table.insert
等高开销操作;对于大量运算可考虑在C#侧提前实现为函数,Lua只作函数调用。避免在Lua里频繁创建临时表或字符串。全局变量与引用:注意Lua持有的C#对象引用会阻止C#垃圾回收。应及时将不再使用的Lua全局置为
nil
,或者手动调用xlua.hotfix(GO,"OnDestroy")
断开引用,防止内存泄漏。
语言特性对性能的影响
- 委托与事件:如上所述,C# 委托在增删回调时会复制列表,事件订阅过多会拖慢性能。对于热路径或大量订阅,考虑采用自定义事件系统或静态回调列表以避免频繁复制。
- 反射:反射是一项非常昂贵的操作,要在性能敏感场合尽量避免。必要时可缓存反射得到的
MethodInfo
/PropertyInfo
,或者使用Delegate
、Expression
等方式预编译调用。记住 IL2CPP 不支持运行时动态生成代码(如Expression.Compile()
、IL.Emit
),所以优化时优先考虑委托或原生写法。 - GC 与内存分配:Unity
的垃圾收集分两代,需要关注分配量。经常会在 Profiler 中看到的
GC.Alloc
源头包括:循环中新建对象、字符串连接、LINQ、闭包、装箱以及 UI
动态生成等。优化要点是减少分配次数:使用对象池、复用集合(
List.Clear()
循环复用)、避免频繁string
拼接(可用StringBuilder
)、慎用 LINQ(会产生临时对象)。例如,每帧产生的垃圾越少,GC 越不频繁,帧率越稳定。 - LINQ 和 lambda:LINQ 查询和
new
表达式常创建新对象;Lambda 捕获也会分配闭包实例。对 闭包 来说,如果引用的是局部或实例变量,则会分配;引用静态变量则不会。避免高频场景下使用这些特性。 - 循环遍历:在 Unity 旧版本中,
foreach
可能会因生成枚举器而装箱导致 GC。推荐对简单集合使用for(int i=0; i<list.Count; i++)
,并将Count
缓存到局部变量,降低开销。 - 方法调用开销:C#
的虚方法调用比静态调用开销略高。对少量但频繁调用的简单方法,可考虑使用
sealed
类、static
方法或[MethodImpl(MethodImplOptions.AggressiveInlining)]
(在 IL2CPP 中可启用 C++ 级内联)来减少调用开销。此外,上述列表遍历示例也显示,多层属性和方法调用会带来多重函数调度成本。 - 数据结构:少用二维或多维 C#
数组(
type[,]
),因为其内部访问需要额外函数开销,性能远低于交错数组(type[][]
)。对性能敏感的大量数据使用,应尽量使用一维数组或简单结构。
Mono 与 IL2CPP 的差异
- 编译方式:Mono 后端使用JIT(即时编译)在运行时将IL编译成本机码,灵活支持热更新和反射;IL2CPP 则是 AOT(提前编译)将 C# 转换为 C++ 再编译,生成本地机器码。IL2CPP 去除了运行时的JIT开销,对多线程和移动平台(尤其是iOS)具有性能和兼容优势。在移动端实践中,IL2CPP 通常比 Mono 运行更快(但编译时间长、包体更大),因此生产环境建议使用 IL2CPP,开发阶段可用 Mono 加快迭代。iOS 平台强制要求使用 IL2CPP(禁止 JIT),Android 平台支持 Mono 32 位和 IL2CPP 64 位。
- 性能差异:总体上,IL2CPP
经预编译优化后执行效率更高。一些论坛和测评也发现 IL2CPP
在低端机型上表现更好,但实际性能还需根据具体场景基准测试(部分特殊情况中
IL2CPP 可能略慢)。需要注意,IL2CPP 的 AOT
特性限制了某些动态功能:如动态生成代码(
IL.Emit
、Expression.Compile
)不可用,反射性能与 Mono 相近但缺少 JIT 优化。 - 调优技巧:在 IL2CPP
下,虚调用去虚拟化可以带来性能提升。将没有继承需求的类或方法标记为
sealed
,可让编译器使用直接调用代替虚表调用。从 Unity 2020.2 起,还可以用[MethodImpl(MethodImplOptions.AggressiveInlining)]
强制 C++ 端内联,以减少函数调用和参数复制成本。对于性能敏感的数学函数,可考虑内联或手写高效版本。相比之下,Mono 模式下可用 Hot Reload、原生调试,但无法在 iOS 上运行,仅建议用于快速迭代测试。
性能分析工具与方法
Unity Profiler:在编辑器或真机(必须为开发版)上运行性能分析。使用 CPU Usage 模块查看各部分开销,按类别(渲染、脚本、物理、GC等)归纳每帧时间消耗。选择某一帧后,在 Profiler 的 “详细信息” 面板里切换 Timeline 和 Hierarchy 视图:Timeline 显示该帧内各线程的时间轴,可对比主线程与作业线程的并行关系;Hierarchy 则以层次结构列出所有函数调用及耗时,默认按耗时降序,有助于快速定位耗时热点。
深度性能剖析:启用 Deep Profiling Support 后,Profiler 会对所有脚本函数进行采样,而非仅限 ProfilerMarker 标记的代码。这使得启动等阶段的分析更全面,但会带来额外开销。一般先在核心流程确认大致瓶颈,再在必要时对某几个函数启用深度剖析。分析时可在 Profiler 视图中点击 GC Alloc 等条目,查看调用堆栈,找出垃圾产生的源头。
Timeline视图:在 Profiler CPU 模块的 Timeline 视图中,可以观察每帧各线程的活动分布。比如可直观看到主线程在等待 VSync 或 GPU 时间的情况,也可在作业系统场景下查看 Worker 线程何时被调度。熟悉常见的 Profiler 标签(如
Scripts.Update
,Physics.Simulate
,VSync
等)有助于判断开销归属。ProfilerMarker:在关键代码段使用
Unity.Profiling.ProfilerMarker
标记,可以在 Profiler 的 Timeline/Hierarchy 中显示自定义名称,方便细粒度分析。例如:1
2
3
4
5
6static ProfilerMarker myMarker = new ProfilerMarker("CustomLogic");
void CustomLogic() {
using (myMarker.Auto()) {
// 性能热点代码
}
}其他工具:结合 Timeline Editor(帧捕获工具)分析渲染、GPU 等开销。使用 Memory Profiler 查看内存泄漏。对于网络或 IO,也可使用专门的分析工具。但总体原则是:先用 Profile 找出瓶颈所在,再针对性优化。
渲染优化(CPU端)
总体架构概览
Unity 使用基于 Component 的实体系统(GameObject + Component)组织场景中的渲染对象。典型的渲染对象由以下组件组成:
Transform
:提供位置、旋转、缩放。MeshFilter
:持有渲染的Mesh
数据。MeshRenderer
:将网格通过材质渲染到屏幕上。
从 2018.1 起,Unity 支持 Scriptable Render Pipeline(SRP),替代
Built-in 渲染流程。SRP 将渲染控制权下放给开发者(通过 C# 实现的
RenderPipeline
和
RenderPipelineAsset
),但底层提交仍由 C++ 实现完成(如
GfxDevice)。
渲染对象的分类与处理规则
Unity 中渲染对象的处理分为几个关键分类维度:
1. 静态 vs 动态
- 通过
GameObject.isStatic
标志决定是否为静态物体。 - 静态物体可参与 静态合批(Static Batching),在构建或运行时将多个对象合并为一个网格,大幅减少 Draw Call。
- 动态物体可使用 动态合批(Dynamic Batching)(受限条件:顶点属性总数 < 900,不能使用多 Pass)。
底层静态合批由 C++ 层的 StaticBatchingUtility
负责,在构建时生成合并网格,并打包成 StaticBatchRoot
节点,运行时通过 StaticBatchRenderer
实现直接渲染。
2. 渲染队列(RenderQueue)
决定绘制顺序,影响透明度处理。
通常划分为:
- 0–2500:不透明(Opaque)
- 2501–3000:AlphaTest
- 3001–5000:透明(Transparent)
底层渲染队列由材质的 shader.renderQueue
决定,在
Material::ComputeRenderQueueWithOffset()
中与
SubShader
的默认值合并处理。
3. 渲染层(SortingLayer + OrderInLayer)
- 主要用于 2D 渲染排序控制。
对应的底层实现见 Renderer::m_SortingLayerID
和
m_SortingOrder
,在渲染排序中通过
GetFinalSortOrder()
函数与摄像机参数一起参与排序逻辑。
4. SRP 分类流程(以 URP 为例)
在 URP 中分类逻辑主要集中在 ForwardRenderer
的
Setup()
和 Render()
中:
- 使用
ScriptableCullingParameters
对可见对象进行剔除(Frustum Culling + Occlusion Culling)。 - 结果是
CullingResults.visibleRenderersList
。 - 在
DrawRenderers
时,传入FilteringSettings
控制 Layer、Queue 等筛选。 - 使用
SortingSettings
指定排序方式(如 opaque 前向,transparent 后向)。
渲染前的处理流程
Unity 渲染对象在通过图形 API(如 DirectX/Metal/Vulkan)提交前经历如下处理流程:
1. Culling 剔除
- 通过
CullResults.Cull()
进行视锥体剔除(Frustum Culling)。 - 可启用遮挡剔除(Occlusion Culling),使用预计算的遮挡体(PVS)或动态 GPU 遮挡(如 Umbra)。
- 剔除逻辑底层通过
SceneCulling.cpp
和Cull.cpp
中的 SIMD 加速处理 BoundingBox 与 Frustum 的包围盒交集检测。
2. Filtering 筛选
- 使用
FilteringSettings
控制:LayerMask、RenderQueueRange、ShaderTag、MotionVector。 - 对应底层结构为
FilterResults
,通过Renderer::Passes
与ShaderTagId
做匹配。
3. Sorting 排序
使用
SortingSettings
控制排序规则:CommonOpaque
:按材质和状态排序,减少状态切换。CommonTransparent
:按摄像机距离排序(后向前避免混合错误)。
实现上使用
DrawObjectSortFunctions.cpp
中的排序函数对 RenderList 排序。
4. State Setup 渲染状态准备
- 包括:设置 Camera matrices、Lighting、Lightmap、Global Shader Constants。
- 若开启 SRP Batcher:所有材质 Uniform 常量使用统一大 Buffer 上传,减少 CBuffer 切换。
- 渲染状态在 C++ 中使用
SetGlobalConstantBuffer
和SetPassGlobalState()
设置,最终提交DrawCallCommand
。
5. 合批与实例化(Batching & Instancing)
- 静态合批:提前合并网格,减少 Draw Call,由
StaticBatchRenderer
提交索引偏移进行绘制。 - 动态合批:CPU 合并顶点后提交。由
DynamicBatching.cpp
实现,运行时构建临时 VBO/IBO。 - GPU Instancing:相同材质的多个对象通过
Material.enableInstancing
一次性绘制。 - SRP Batcher:使用固定格式统一结构的 UniformBuffer 进行批处理。
底层由 BatchRendererGroup.cpp
调用
DrawMeshInstanced()
或
DrawInstancedProcedural()
实现。
6. CommandBuffer 构建与提交
- 所有绘制命令封装为
CommandBuffer
(C#)或ScriptableRenderContext
。 - 最终调用
context.Submit()
,封装为RenderCommandBuffer
结构,提交给 C++ GfxDevice。 - GfxDevice(如 GfxDeviceD3D11/GfxDeviceMetal)最终翻译为底层 API DrawCall 并提交 GPU Command Queue。
优化目标与性能考虑
阶段 | 目的 | 优化方法 |
---|---|---|
剔除 | 减少不必要渲染对象 | 利用 Job System 多线程剔除,使用 GPU Occlusion Culling |
筛选 | 精准筛选目标对象 | 使用 FilteringSettings 限制 Layer/Queue |
排序 | 降低 GPU Pipeline flush | Opaque 优先按材质排序,Transparent 后向前避免混合错误 |
合批 | 减少 Draw Call 数 | 静态合批/动态合批/GPU Instancing/SRP Batcher |
状态 | 减少 CBuffer 绑定 | 启用 SRP Batcher(每材质使用统一 buffer) |
提交 | 提高 CPU-GPU 并发效率 | 使用 CommandBuffer 封装所有命令后统一提交 |
关键源码位置与类
功能 | 源码/类 |
---|---|
Renderer 渲染组件 | UnityEngine.MeshRenderer 、SkinnedMeshRenderer |
剔除 | CullResults 、ScriptableCullingParameters ,底层见
Cull.cpp |
渲染指令 | ScriptableRenderContext.DrawRenderers() ,底层翻译为
RenderCommandBuffer |
批处理 | BatchRendererGroup 、SRP
Batcher(RenderGraph ) |
GPU Instancing | Material.enableInstancing = true ;Shader 中使用
UNITY_INSTANCING_CBUFFER_START/END 宏 |
SRP 管线主类 | RenderPipeline ,
RenderPipelineAsset ,内部走
RenderPipelineManager.DoRenderLoop_Internal() 调度 |
网络优化
网络通信整体架构概览
在大型多人联网游戏中,通常采用如下架构:
- 客户端:Unity + C# + Xlua
- 服务端:C++/Java/Golang 等高性能语言
- 通信协议:TCP 为主,部分 UDP 或 WebSocket 用于推送、心跳等
- 数据格式:ProtoBuf / FlatBuffers / 自定义二进制协议 / JSON(调试)
客户端通信模块的职责包括:
- 创建 socket 并维护连接
- 数据收发与缓冲管理
- 协议解析、打包、粘包处理
- 异常重连与心跳保活
Socket 通信底层流程(以 TCP 为例)
Socket 创建与连接建立
1 | Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); |
AddressFamily.InterNetwork
: 使用 IPv4 协议SocketType.Stream
: 流式 socket,TCPProtocolType.Tcp
: 明确协议
设置 Socket 参数(可优化性能)
1 | socket.NoDelay = true; // 关闭 Nagle 算法,降低延迟 |
SendBufferSize
和ReceiveBufferSize
:设置的是 操作系统内核的 socket 缓冲区大小,而不是应用层 buffer。这影响了 socket 层对数据包的缓存能力。
NoDelay = true
用于关闭 Nagle 算法(详见后文),避免发送小包延迟。
建立连接过程(TCP 三次握手)
- 客户端发送 SYN
- 服务端返回 SYN + ACK
- 客户端发送 ACK 确认
网络数据发送流程
发送数据(Send)
1 | socket.Send(buffer, offset, length, SocketFlags.None); |
- 底层实际调用 OS 的
send
函数,将数据写入 socket 的 发送缓冲区。 - 若缓冲区满,会阻塞或返回未写完的字节数(异步 IO 会挂起等待)。
数据组织
- 数据结构通常为:
[协议头(长度+类型)] + [消息体]
- 避免频繁申请大数组,使用
ArrayPool<byte>
或环形缓冲池复用 - 批量打包小数据,避免碎片化,提高发送效率
Nagle 算法说明
Nagle 算法的目的:减少网络中小包数量,提高带宽利用率。
原理:
- 当 socket 中存在未确认的数据包时,暂不发送新的小包,等待 ACK 后再发。
缺点:
- 会引起 40~200ms 的发送延迟,尤其在 turn-based 游戏中尤为明显。
建议:
- 默认关闭(
socket.NoDelay = true
)提升实时性
网络数据接收流程
接收数据(Receive)
1 | socket.Receive(buffer, offset, size, SocketFlags.None); |
- 数据从 OS 的内核 接收缓冲区 拷贝到应用层 buffer。
- 应用层通常使用独立线程 / 异步回调持续监听数据。
粘包 / 拆包问题
TCP 是面向字节流的协议,没有消息边界,需自己处理粘包/半包:
- 协议设计中加
Length
字段(前 4 字节等)来解包 - 自己维护缓存区,按 length 解析完整消息
- 协议设计中加
性能优化建议
ArrayPool 的底层实现详解
System.Buffers.ArrayPool<T>
是 .NET
为了避免频繁创建和销毁大数组、减少 GC 压力而提供的一种数组租赁机制。
其默认实现为:System.Buffers.DefaultArrayPool<T>
,核心结构如下:
内部结构:
- 通过
T[][] _buckets
管理多个“桶”,每个桶存储特定大小的数组。 - 每个桶对应一种标准容量(例如 16, 32, 64, ..., 最大支持到 1024*1024 级别)。
- 每个桶由一个
LockedStack<T[]>
(或ConcurrentBag
)管理,支持线程安全的数组归还与租赁。
- 通过
关键逻辑:
Rent(int minimumLength)
:- 查找大于等于
minimumLength
的最近标准桶;若有可用数组直接返回,否则新建一个。 - 若请求长度大于
MaximumBufferSize
,直接分配新数组。
- 查找大于等于
Return(T[] array, bool clearArray = false)
:- 根据数组长度判断归还到哪个桶;若桶已满,则该数组被抛弃(等待 GC)。
clearArray = true
会调用Array.Clear()
清空内容,避免引用保留导致的内存泄漏。
性能特性:
- 大量减少
byte[]
,int[]
等大数组频繁分配导致的 LOH(大对象堆)GC 开销。 - 使用线程本地栈优化热路径性能,避免锁竞争。
- 分桶策略有效避免数组碎片和大小不均。
- 大量减少
使用建议:
- 配合
Span<T>
或Memory<T>
使用更高效。 - 网络通信中,推荐作为接收缓冲区、序列化缓存池等。
- 配合
优势总结:复用大数组减少 GC,提升网络中数据处理性能,降低内存抖动。
Span / Memory 的底层设计结构
Span:
Span<T>
是一种轻量结构体(ref struct),用于表示托管内存中的一段连续区域(如数组、栈上内存、堆上内存的一部分)。特点:
- 无堆分配、无 GC
- 支持
Slice()
、CopyTo()
等高效操作 - 不能存储在字段中或捕获到 lambda 中,因为它可能指向栈上内存
底层结构:
- 包含:
ref T _pointer
和int _length
- JIT 会将对 Span 的访问转成指针偏移 + 边界检查
- 包含:
Memory:
Memory<T>
与Span<T>
类似,但是可存储的引用类型特点:
- 可作为字段、异步传递
- 内部包含一个引用和偏移信息
- 可通过
.Span
转换成Span<T>
使用
应用场景:
- 使用
ArrayPool
+Span<byte>
构建零 GC 的序列化系统 - 异步函数中使用
Memory<T>
传递缓存,避免数组拷贝
C# vs C/C++ 网络通信性能对比(基于 Xlua 项目)
场景说明
在 C# + Xlua 结构中,如果将 Socket 网络通信逻辑移到 C/C++ 层,有如下潜在性能提升:
提升点分析
项目 | 使用 C# 实现 | 使用 C/C++ 实现 |
---|---|---|
GC 压力 | 高(byte[]频繁回收) | 低(自行管理内存) |
字节操作性能 | 中(需借助 Span , BinaryWriter ) |
高(直接指针操作) |
多线程 | 受限于托管线程模型 | 可使用原生多线程 |
跨 Lua 调用 | Xlua 调用 C# 较频繁 | 可通过 P/Invoke 减少 Lua ↔︎ C# 跳转 |
数据结构复用 | 需使用 ArrayPool 或自建池 |
malloc/free + buffer pool 灵活控制 |
CPU 指令优化 | 不可控 | 可用 SIMD、zero-copy、batch send 等手段 |
场景适配建议
- 通信极致高频(如 20ms 一帧、RTS 推送)推荐用 C/C++
- 若瓶颈在 IO 线程与序列化,可尝试 C# 调用原生插件(如 FlatBuffers C API)
- 若逻辑复杂不宜重构,仍可在 C# 中精细优化 buffer 分配与内存布局
附加建议:高并发优化策略
使用异步 I/O (
SocketAsyncEventArgs
) 或 IOCP(Windows)提升吞吐网络协议压缩(Snappy / Zstd)降低传输成本, 但需要注意:加密后是高熵(数据重复度低)数据,压缩无意义,所有需要先压缩,再加密。否则压缩毫无意义,反而增加开销
心跳机制定时 + RTT 检测,动态调整重传超时(RTO)
使用双缓冲避免线程读写冲突
将关键协议划分为:
- 高优先级(心跳、同步帧)
- 中优先级(状态同步)
- 低优先级(日志、战报) 实现分通道异步发送(多队列)