UWA问答 | 博客 | 游戏及VR应用性能优化记录分享 | 侑虎科技
UWA博客，UWA旗下官方博客。针对游戏及VR应用性能优化等内容记录、交流，以及案例分享。博客主要分为：厚积薄发、万象更新、博观约取、充电一刻、博物纳新等几个模块。
UE是怎么管理纹理的各向异性采样的
1）UE是怎么管理纹理的各向异性采样的 2）Unity 2022动态设置光照贴图手机端显示异常 这是第459篇UWA技术知识分享的推送，精选了UWA社区的热门话题，涵盖了UWA问答、社区帖子等技术知识点，助力大家更全面地掌握和学习。 UWA社区主页：community.uwa4d.com UWA QQ群：793972859 From 问答社区 Q：优化我们UE项目的功耗发热问题时，在移动端真机测试中，通过各种性能工具的参数发现GPU带宽很高，且其中Anisotropic Filtered各向异性过滤的比例一直很高。按之前的经验来说，各向异性对带宽的影响就是很大的，但是在编辑器里没找到设置项，请问UE是怎么管理纹理的各向异性采样的？ A：UE中纹理是否开启各向异性需要检查以下设置： Texture Group 在DeviceProfiles.ini中可以创建TextureGroup并进行设置，例如在Engine\Config\BaseDeviceProfiles.ini中（或Project\Config\DefaultDeviceProfiles.ini）： 其中，MinMagFilter是纹理拉伸时使用的过滤方式，包括point、linear、aniso，此处aniso即开启纹理的各向异性设置。如果想对该Group的所有纹理关闭各向异性设置，可以修改为point或linear。 纹理编辑器设置 打开纹理的资产编辑器，在设置中能够通过Filter设置调整纹理采样方式，可选包括Nearest，Bi-linear，Tri-linear，Default。其中，Default为使用TextureGroup的设置，大部分默认的Group中纹理都开启了各向异性（参考上一条）。此处可以选择另外三种过滤方式，选择后将关闭该纹理的各向异性。该设置适合针对单独纹理关闭各向异性时使用。 各向异性最高采样数设置 各向异性的最高采样数可以通过Cvars：r.MaxAnisotropy进行设置。例如： r.MaxAnisotropy=8，则纹理最高采样数为8。数值为0或1时关闭各向异性。该设置为全局设置，对所有开启了各向异性的纹理生效。在Scalability.ini中各纹理级别已经包含了该设置： 此外在DeviceProfile中，也可以直接在对应平台添加该Cvars的设置： 欢迎大家转至社区交流： https://answer.uwa4d.com/question/69523009244ce21ce9ec0967 From 问答社区 Q：编辑器加载显示都正常，出安卓包，光照贴图出现错乱，使用FrameDebugger抓帧查看数据unity_lightmap有值，使用工具获取LightingSetting中设置的光照贴图也是正常的，Mesh中也添加了对应的光照信息数据。 补充测试点：同一个物件，固定在场景中，光照信息正常，动态加载的物件会出现异常（使用Yooasset进行加载）即使动态重新设置了光照贴图，固定的物件光照信息仍然正常。（提供的图片为手机端的显示和FrameDebugger的抓帧数据。） 为什么Unity 2022动态设置光照贴图手机端会显示异常？ 欢迎大家转至社区交流： https://answer.uwa4d.com/question/694eab9d682c7e5cd61bfb83 无论是社区里开发者们的互助讨论，还是AI基于知识沉淀的快速反馈，核心都是为了让每一个技术难题都有解、每一次踩坑都有回响。本期分享分别来自UWA AI问答和UWA问答社区，希望这些从真实开发场景中提炼的经验，能直接帮你解决当下的技术卡点，也让你在遇到同类问题时，能更高效地找到破局方向。 封面图来源于网络 今天的分享就到这里。生有涯而知无涯，在漫漫的开发周期中，我们遇到的问题只是冰山一角，UWA社区愿伴你同行，一起探索分享。欢迎更多的开发者加入UWA社区。 UWA官网：www.uwa4d.com UWA社区：community.uwa4d.com UWA学堂：edu.uwa4d.com 官方技术QQ群：793972859
12月刊｜上新！全局筛选 + 设备标记 + 序列帧回溯，GPM 2.0 性能分析效率拉满
GPM 2.0持续聚焦游戏研发团队的性能分析效率与数据精准度需求，12月新增五大核心功能，从操作流程简化、数据干扰剔除、适配优先级优化、管控灵活性提升、玩家操作回溯五个维度，全方位升级性能监测与分析能力，助力团队更高效、更精准地解决性能问题。 全局筛选器 一次配置多页同步，告别重复操作。 以往使用时在“数据总览”、“指标分析”等页面切换，需反复设置版本、时间范围、机型等筛选条件。本次新增了的全局筛选器功能！ 多页面条件自动同步：在任一页面完成筛选配置（例如“版本V2.1+近7天+Android机型”），切换至其它分析页面时，筛选条件将自动同步生效，无需二次手动设置； 常用方案保存复用：支持将高频筛选逻辑（如“每周性能复盘方案”、“低端机专项分析方案”）保存为个人或公用方案，后续进入系统直接调用，大幅缩短数据准备时间。 真机/模拟器区分 剔除干扰数据，确保分析贴合真实玩家。 过往性能数据分析中，Android版本、内存、CPU频率等指标易受模拟器设备干扰，导致统计结果与真机环境偏差较大；而崩溃、卡顿、帧率等核心指标更易因模拟器、Root机数据混入，造成运营团队误判项目性能达标情况。本次新增的运行环境区分功能，通过精准识别逻辑解决该问题。 多维度精准识别：基于设备硬件特征与系统属性，可靠区分真机与模拟器； 筛选控制灵活可控：支持在筛选器中自主选择“包含/排除”模拟器数据，对不支持模拟器游玩的项目，可一键隐藏无效数据，确保性能指标、异常发生率等统计结果与真实玩家设备场景高度一致。 机型上市时间/价格标记 洞察设备特性，优化适配优先级。 此前合作团队仅通过机型名称与配置，难以判断设备老化程度，也无法匹配用户设备档次与性能需求，导致适配优先级难以明确。本次更新新增“机型上市时间”与“机型价格”两大维度，为适配决策提供数据支撑。 维度细化：在“数据细分”列表中，按“上市年份+季度”（如2025Q1）、“上市发售价格档”（例如4000元档、千元档）标注设备属性； 适配优化：可通过条件组合（如“2025Q1~Q4+4000元档”）快速查看旗舰机型性能表现，或聚焦“2022年前+千元机”分析老旧低端设备的内存占用情况，精准确定优先优化群体。 网页端崩溃上报开关 零开发快速管控，成本精准投放。 此前开启/关闭崩溃、ANR上报需技术团队通过集成SDK时的宏命令调整，不仅依赖开发资源，变更策略还需等待App发版，响应效率低。本次开放的网页端开关功能，实现“自主化、快响应”管控。 零开发操作：运营、测试人员可直接在网页端根据版本目标（如灰度测试、正式发布）开启/关闭上报，无需技术介入； 快速生效：变更上报策略后5-10分钟内即可生效，无需等待App发版，大幅提升问题排查灵活性与时效性，确保成本花在关键版本的问题监测上。 播放序列帧 动态回溯玩家操作，精准定位异常节点。 本次新增的“序列帧播放”功能，实现单张截帧展示的基础上，将玩家操作流程可视化回溯。 灵活播放控制：支持自定义“每秒播放帧数”、“每次跳转帧数”，按需调节播放速度； 便捷辅助功能：搭配键盘微调、当前帧下载，可完整回溯玩家操作路径，精准锁定异常发生前的关键操作（如技能释放、场景切换），为性能优化与玩家行为分析提供直观支撑。 迭代不止，性能管控更省心 GPM 2.0始终以游戏研发团队的实际需求为核心，围绕更多实用场景推进功能升级，敬请期待GPM 2.0在2026年中更多、更贴合您需求的功能！ 实操演示 立即体验！ 专属通道限时开放 ➡ 预约技术专家1V1部署指导！我们将全程支持服务搭建、数据分析与反馈，确保您能够充分体验GPM 2.0带来的价值。 邮件：sales@uwa4d.com 微信号：17502188376 了解GPM 2.0全功能，可以点击《GPM 2.0 核心功能全景指南｜重塑游戏性能主动管控能力》查看详情。 关于GPM 2.0 UWA GPM 2.0是专为上线及测试阶段游戏项目打造的高性能监测平台。它不仅能深度捕捉宏观性能数据，更创新采用性能无损截帧技术，在不影响玩家体验的前提下，助力开发者全面掌握玩家端运行关键细节，从多维度优化游戏性能，实现从“玩家投诉后救火” 到 “问题发生前预警” 的核心转变。 GPM 2.0全景指南 《UWA GPM 2.0 核心功能全景指南｜重塑游戏性能主动管控能力》 《GPM 2.0 功能更新月报：新增导出表格、崩溃反混淆等实用功能，体验再升级！》 GPM 2.0新功能 集锦 《GPM 2.0全新功能发布｜GPU精准监测 + 精细化运营，重构游戏性能管控新范式》 《GPM 2.0 SDK 热更新｜不打扰玩家的“隐形升级术”！》 《GPM 2.0功能尝新｜AI识图精准捕获材质丢失，全链路护航游戏品质！》 《UWA GPM 2.0 崩溃上报功能全面升级｜全场景覆盖+免费开放，让崩溃排查不再卡壳》 《大R流失、问题难复现？GPM 2.0「指标 - Session直连」功能，让异常定位效率翻倍！》 《从 “被动救火” 到 “主动掌控”：GPM 2.0 如何重塑游戏性能竞争力？》 《GPM 2.0 告警功能：游戏性能的智能守护者》 《无限接近真相｜GPM 2.0 功能上新》 《崩溃现场全回溯｜GPM 2.0「崩溃上报分析」上线，终结“无法复现”的噩梦》 《GPM 2.0 全新发布「卡顿帧堆栈」功能｜精准高效定位游戏玩家端性能》 《深入解析「卡顿帧堆栈」》 《解锁GPM 2.0「卡顿帧堆栈」｜代码示例与实战分析》 《如何精准打点解决卡牌、SLG、开放大世界、放置类游戏卡顿难题》 《不妥协每一毫秒的性能 不浪费每一字节的流量》 《GPM 2.0 重新定义性能监测与优化｜从未知到已知，从被动到主动》 GPM 2.0案例精讲 集锦 《我们的目标是不耗电、不发烫、不闪退》 《冒险闯关类玩家关注的四大体验》 《玩家端性能数据 一手掌握》 《造成设备功耗高的罪魁祸首》 《海外市场设备适配性优化实战》 《5000人测试背后的秘密》 GPM 2.0实操篇 集锦 《GPM 2.0 按游戏类型定制性能监控策略｜「告警」功能实战指南》 《GPM 2.0 让每一帧的流畅兑换真金白银》 《GPM 2.0 实操之Session篇之如果闪退是因为内存崩了》 《GPM 2.0 应用场景实例之解决让人头疼的问题》 《GPM 2.0 助力设备监测、行为分析与性能优化｜玩家共测新体验》 《GPM 2.0 API 功能助力定制你的性能优化》 《GPM 2.0 驱动优化与投放决策｜硬件性能一目了然》 《GPM 2.0 多维数据与实时截帧完美结合》
在游戏开发领域，性能优化始终是绕不开的核心命题：从数据获取的简便性，到性能分析的洞察深度，每一个细节都可能影响玩家端的最终体验。最近，我们正式推出新版本，围绕开发者真实需求，为优化工作提供更强大的助力！ 一、Shader变体分析 无论是对于酷炫渲染效果的追求，还是开发者制作时由于时间紧迫性而忽视了性能，都可能导致打包运行后、Shader变体数量或者游戏系统显存占用过高，这在UWA支持过的项目中已经屡见不鲜， 并且进一步伴生加载时间过长及渲染效率下降等现象。 UWA报告中对Shader内存的占用统计（一） UWA报告中对Shader内存的占用统计（二） 对于单个Shader资源而言，其内存占用高的核心原因是变体数过多，在项目迭代中可能会出现已经被弃用、或者没有被实际使用到的关键字，导致变体成倍上升；又或者Shader写的比较复杂，其中一些关键字组合永远不会被用到，导致很多变体是多余的。 在最新的UWA AssetBundle检测报告中，我们支持AB变体数据下载，直接列出了所有变体的关键字的组合，高效赋能Shader优化。通过该功能，开发者可精准定位无用变体，降低设备运行压力。 二、GPU负载预警 在拆解GPU压力时，UWA建议以GPU Clocks数值为度量标准。GPU Clocks是UWA通过硬件底层接口统计到的数值，它反映了每帧耗费在GPU计算上的时钟周期数，单位为万Cycles/帧。若将该值乘以项目的目标帧率，则得到项目运行过程中每秒所需要的GPU算力。 而每款SOC出厂时，硬件厂商会给其标定一个额定最大频率，也就是其释放全部性能时，理论上每秒钟能进行多少时钟周期的运算，我们称之为理论最大算力。若实时需求算力大于当前运行机型GPU芯片的理论最大算力，即代表硬件算力不足以满足项目渲染所需要耗费的实时算力，这为我们提供了压力判断的有效依据。 在最新版本中，我们增加“能耗压力阈值线”：GPU工作频率逼近80%即预示设备存在能耗过高风险、“帧率压力阈值线“：GPU工作频率逼近100%即预示设备存在掉帧风险，提前预警；同样地，我们也可以通过不同档位的设备测试GPU数据，来反推分档的合理性。 除此之外，我们还新增了若干性能指标或分析维度，立志于为决策提供更高效的评判依据。从精准适配到高效操作，从数据记录到趋势预测，以更智能的分级、更流畅的交互、更深度的数据，为游戏性能优化打开新可能。 以下为最新版本支持清单 欢迎体验最新版本 ，让每一次优化都更有方向！ 关于UWA UWA是一家创业十年的高新技术企业，作为游戏行业的深耕者，UWA始终专注于为使用Unity、Unreal引擎的开发者提供丰富的优化产品，帮助开发者高效解决开发问题、定位性能瓶颈、提供解决方案，已支持超过一万款游戏项目。还打造了技术博客、问答、开源库、学堂等社区产品，为开发者提供便利和高效的支持。线上培训和线下教育的新业务，满足行业对人才培育的需求。
参数GPU Write Total Bandwidth的含义是什么，导致其值过高的因素有哪些
1）参数GPU Write Total Bandwidth的含义是什么，导致其值过高的因素有哪些 2）对Unity项目来说，性能和效果可以兼顾的物理插件有哪些推荐 这是第458篇UWA技术知识分享的推送，精选了UWA社区的热门话题，涵盖了UWA问答、社区帖子等技术知识点，助力大家更全面地掌握和学习。 UWA社区主页：community.uwa4d.com UWA QQ群：793972859 From UWA AI 问答 Q：GPU Write Total Bandwidth这一参数的含义，和其过高时可能的影响因素有哪些？ A：GPU Write Total Bandwidth含义： 该指标表示GPU每帧向内存（显存或系统内存）写入的总数据量，单位通常为MB/s或GB/s。它是GPU带宽压力的关键子项，反映帧渲染过程中所有写操作的带宽消耗总和，包括：帧缓冲（Color/Depth/Stencil）、RenderTexture、计算着色器输出、顶点着色器输出（如Transform Feedback）、以及部分缓存写回等。 Write Total Bandwidth 过高的常见影响因素： ✔ 高分辨率多目标渲染（MRT）或高精度帧缓冲 同时写入多个RenderTarget（如GBuffer中Albedo+Normal+Metallic+Depth），尤其使用高精度格式（RGBA16F、RGB10A2）； 主摄像机启用HDR渲染（R11G11B10或RGBA16F），大幅增加Color Buffer写入带宽； MSAA启用后（如4xMSAA），Color/Depth Buffer写入量成倍增长（需resolve前写入+resolve后写入）。 ✔ 频繁或大尺寸RenderTexture写入 全屏后处理中反复创建/清空/Blit到高分辨率RT（如2048×1536的Bloom临时纹理）； UI渲染中大量使用CanvasRenderTarget或Camera.targetTexture； 动态阴影图（Shadow Map）分辨率过高（如4096×4096）且每帧更新。 ✔ 深度/模板缓冲频繁写入与清除 每帧Clear Depth/Stencil（尤其在多Camera、多Pass场景下重复Clear）； 启用深度预通道（Z-Prepass）但未合批或存在冗余绘制，导致两次深度写入（Prepass+Main Pass）； 不合理的ZTest/ZWrite设置（如ZWrite Off缺失导致无效深度覆盖重试）。 ✔ Compute Shader高频大块写入 使用RWTexture2D/RWBuffer执行大规模粒子更新、物理模拟或图像生成，且线程组尺寸过大或Dispatch次数过多； 未使用AppendBuffer/ConsumeBuffer而采用全量写入方式。 ✔ GPU管线冗余写入行为 多个Camera渲染同一区域且未做裁剪/层级隔离，造成重复Color/Depth写入； URP/HDRP中未关闭不必要的Pass（如ShadowCaster、DepthOnly在非阴影场景仍启用）。 快速定位建议： 查看GOT Online GPU报告中Write Total曲线与Front-end Write/Texture Write/Depth Write等子项占比（若支持）； 对比FPS下降时段是否伴随Write峰值，结合GPU Freq是否因带宽瓶颈触发降频； 检查RenderPipeline Asset中是否启用了非必要高精度RT格式、MSAA或多Pass渲染； 使用UWA资源检测报告筛查高分辨率、未压缩的RenderTexture和ShadowMap资产。 优化优先级：降低RT分辨率 → 合并MRT输出 → 关闭冗余Clear/MSAA → 替换高精度格式（如RGBA8替代RGBA16F）→ 后处理降采样。 UWA GPU分析页面中还展示了很多对性能排查分析有一定参考价值的参数和其子参数，若您对这些参数的具体含义和参考价值仍有疑问，可在社区中进行查阅、或尝试使用AI问答助手解答。如以上问题，除了了解写带宽的影响因素外，您还可以提问在不同GPU架构下多种多样的读带宽子项的含义：Front-end Read、LoadStore Read、Texture Read、Vertex Read、SP Read。 分享链接： https://www.uwa4d.com/main/uwa-shared.html?shareId=8919019e-4d69-43a2-bba1-39138791414a From UWA AI 问答 Q：对Unity项目来说，性能和效果可以兼顾的物理插件有什么推荐？ A：在Unity项目中，兼顾性能与效果的物理插件需满足三大硬性条件： ✔ 跨平台行为一致（尤其iOS/Android）； ✔ CPU占用可控（单帧<1ms，中端机）； ✔ 支持表现/逻辑分层（避免帧同步风险）。 推荐方案： 首选：Unity原生PhysX+工程化封装（非插件，但最可靠） 适用场景：角色布娃娃、关卡交互物体、简单布料、碰撞反馈。 关键优化实践： 使用Rigidbody.interpolation=Interpolate+collisionDetectionMode=ContinuousDynamic控制抖动与穿透； 所有Collider替换为Primitive Collider组合（禁用MeshCollider，见FAQ文档），复杂模型用BoxCollider+CapsuleCollider分区代理； 物理更新频率降为Time.fixedDeltaTime=0.033f（30Hz），配合Physics.autoSimulation=false手动调度。 高效替代：Obi Softbody & Obi Cloth（商业插件） 优势： 纯C#实现，完全绕过PhysX，iOS/Android行为100%一致，支持帧同步逻辑层（需定点数改造）； 布料解算支持GPU加速（Metal/Vulkan），CPU负载下降60%+； 提供ObiSolver多线程调度器，可绑定至Unity Job System。 注意：需关闭Substeps（默认3→设为1），避免低端机卡顿；布料顶点数建议≤128。 表现专用（零物理开销）：Dynamic Bone（免费开源） 定位：纯表现层附属物模拟（头发、衣摆、尾巴）。 优势： 无刚体、无物理引擎依赖，仅靠Transform插值计算； 支持LOD控制（Update When Inactive+Max Distance）； 源码开放，可无缝接入URP/HDRP。 不适用于受力反馈、碰撞交互等逻辑需求。 明确规避 Havok Physics for Unity：仅限高端主机/PC，移动端无授权支持，且无法保证Android/iOS一致性。 自研PhysX封装库：跨平台浮点误差不可控，已有多款上线项目因PhysX版本差异导致iOS/Android同步失败。 最终建议路径 所有方案均需配合UWA GOT Online的Physics模块实时监控Rigidbody Count与FixedUpdate ms，确保长期稳定。 无论是社区里开发者们的互助讨论，还是AI基于知识沉淀的快速反馈，核心都是为了让每一个技术难题都有解、每一次踩坑都有回响。本期分享分别来自UWA AI问答和UWA问答社区，希望这些从真实开发场景中提炼的经验，能直接帮你解决当下的技术卡点，也让你在遇到同类问题时，能更高效地找到破局方向。 封面图来源于网络 今天的分享就到这里。生有涯而知无涯，在漫漫的开发周期中，我们遇到的问题只是冰山一角，UWA社区愿伴你同行，一起探索分享。欢迎更多的开发者加入UWA社区。 UWA官网：www.uwa4d.com UWA社区：community.uwa4d.com UWA学堂：edu.uwa4d.com 官方技术QQ群：793972859
GPM 2.0全新功能发布｜GPU精准监测 + 精细化运营，重构游戏性能管控新范式
GPM 2.0始终以游戏研发全流程的核心痛点为迭代方向，持续拓展性能监测的深度与广度。本次升级聚焦「渲染瓶颈定位」「用户体验分级」「崩溃问题排查」「研发生态协同」四大关键场景，推出四大核心功能，通过更精准的数据支撑、更灵活的运营适配、更高效的问题解决、更顺畅的流程衔接，为游戏研发团队提供全方位升级的性能管控解决方案，助力产品实现体验与营收的双重提升。 GPU性能指标模块 告别图形渲染优化的“盲目调试”，渲染瓶颈精准锁定。 全覆盖核心指标：包含GPU Clocks、GPU Fragment Shaded、GPU带宽及图元数四大维度； 可视化数据呈现：搭配直观数据趋势图，实时掌握玩家设备GPU动态表现； 场景化关联分析：支持结合玩家画面截帧，实现指标数据与实际场景的深度联动。 应用场景 通过GPU Clocks快速判断设备是否存在GPU降频现象； 借助片元着色数量与图元数，定位多角色同屏、高画质特效等复杂场景的渲染过载； 利用带宽数据追溯显存传输瓶颈，优化资源加载逻辑。 玩家分档功能 精细化运营，优化资源分配。 多维度自定义分档：支持按用户等级、付费能力、设备配置等维度划分用户群体； 差异化策略配置：可针对不同档位设置专属监测标准与告警机制； 数据化对比分析：直观呈现不同群体的性能问题分布，助力决策优化。 应用场景 对高价值用户群体：开启高频次、高标准性能告警，保障重度场景流畅体验； 对大众用户群体：聚焦登录、主线任务等关键场景，筑牢基础体验底线； 通过分档数据，判断性能瓶颈对核心价值的影响权重，精准分配优化资源。 自定义文件上报 全维度信息支撑，崩溃排查效率倍增。 灵活配置上报文件：支持自主选择需上报的附加文件类型（自定义日志、场景配置、资源加载记录等）； 同步上报关联信息：崩溃发生时，自定义文件与堆栈、设备参数同步上传； 全场景回溯支撑：构建完整“问题回溯链条”，还原崩溃前全流程场景。 应用场景 针对偶发崩溃：通过自定义日志追溯玩家操作路径，快速锁定触发条件； 针对场景化崩溃：结合场景配置文件与资源加载记录，还原崩溃时的运行环境； 大幅提升疑难崩溃排查效率，减少因问题积压导致的用户体验下滑。 OpenAPI 打通研发流程，数据价值深度释放。 多系统兼容集成：支持与自有研发管理平台、数据分析工具、后台监控系统对接； 全维度数据同步：可同步性能指标、崩溃报告、分档数据等核心信息； 自动化场景适配：支持联动实现性能波动自动预警、数据深度挖掘等场景。 应用场景 与版本管理系统联动：新版本上线后，自动监测性能波动并触发预警； 对接数据分析平台：挖掘性能数据与玩家留存、付费转化的关联逻辑，支撑产品迭代； 集中化数据展示：避免跨平台切换操作，聚焦核心工作提升协同效率。 持续迭代，赋能无界 GPM 2.0的每一次升级，都源于对游戏研发实际需求的深度洞察。本次四大功能的推出，进一步完善了性能管控的全场景覆盖能力： 对追求性能优化效率的团队：提供精准监测与高效排查工具，减少无效调试； 对注重精细化运营的团队：实现用户体验分级保障，最大化核心价值； 对需要流程协同的团队：打通数据链路，释放数据驱动决策的核心力量。 行动号召 功能详情：点击《GPM 2.0核心功能全景指南》，查看完整操作手册 试用申请：添加官方微信（17502188376），申请免费体验新功能 定制咨询：发送需求至sales@uwa4d.com，获取专属解决方案 GPM 2.0将继续聚焦游戏研发的核心痛点，持续拓展功能边界，为游戏研发团队带来更全面、更高效的性能管控工具！ 关于GPM 2.0 GPM 2.0 是一款专为上线或测试阶段的游戏项目打造的高效性能监测工具。它不仅深入捕捉宏观性能数据，还通过其独特的性能无损截帧功能，让开发者在不影响玩家体验的前提下，全面掌握玩家运行时的关键细节，从多个维度优化游戏的性能表现，提升整体用户体验。 GPM 2.0全景指南 《UWA GPM 2.0 核心功能全景指南｜重塑游戏性能主动管控能力》 《GPM 2.0 功能更新月报：新增导出表格、崩溃反混淆等实用功能，体验再升级！》 《12月刊｜上新！全局筛选+设备标记+序列帧回溯，GPM 2.0性能分析效率拉满》 GPM 2.0新功能 集锦 《GPM 2.0 SDK 热更新｜不打扰玩家的“隐形升级术”！》 《GPM 2.0功能尝新｜AI识图精准捕获材质丢失，全链路护航游戏品质！》 《UWA GPM 2.0 崩溃上报功能全面升级｜全场景覆盖+免费开放，让崩溃排查不再卡壳》 《大R流失、问题难复现？GPM 2.0「指标 - Session直连」功能，让异常定位效率翻倍！》 《从 “被动救火” 到 “主动掌控”：GPM 2.0 如何重塑游戏性能竞争力？》 《GPM 2.0 告警功能：游戏性能的智能守护者》 《无限接近真相｜GPM 2.0 功能上新》 《崩溃现场全回溯｜GPM 2.0「崩溃上报分析」上线，终结“无法复现”的噩梦》 《GPM 2.0 全新发布「卡顿帧堆栈」功能｜精准高效定位游戏玩家端性能》 《深入解析「卡顿帧堆栈」》 《解锁GPM 2.0「卡顿帧堆栈」｜代码示例与实战分析》 《如何精准打点解决卡牌、SLG、开放大世界、放置类游戏卡顿难题》 《不妥协每一毫秒的性能 不浪费每一字节的流量》 《GPM 2.0 重新定义性能监测与优化｜从未知到已知，从被动到主动》 GPM 2.0案例精讲 集锦 《我们的目标是不耗电、不发烫、不闪退》 《冒险闯关类玩家关注的四大体验》 《玩家端性能数据 一手掌握》 《造成设备功耗高的罪魁祸首》 《海外市场设备适配性优化实战》 《5000人测试背后的秘密》 GPM 2.0实操篇 集锦 《GPM 2.0 按游戏类型定制性能监控策略｜「告警」功能实战指南》 《GPM 2.0 让每一帧的流畅兑换真金白银》 《GPM 2.0 实操之Session篇之如果闪退是因为内存崩了》 《GPM 2.0 应用场景实例之解决让人头疼的问题》 《GPM 2.0 助力设备监测、行为分析与性能优化｜玩家共测新体验》 《GPM 2.0 API 功能助力定制你的性能优化》 《GPM 2.0 驱动优化与投放决策｜硬件性能一目了然》 《GPM 2.0 多维数据与实时截帧完美结合》
【USparkle专栏】如果你深怀绝技，爱“搞点研究”，乐于分享也博采众长，我们期待你的加入，让智慧的火花碰撞交织，让知识的传递生生不息！ 背景：前段时间在项目内做了关于Mono内存（堆内存）的优化。从结果上将Mono内存从220MB降低到130MB，优化过程中唤起了部分关于GC的消失的回忆，虽然实际的优化工作中也许并用不到，但是更明确底层实现机制总归是一件迭代自我的过程，在这里就来回顾一下。 一、什么是垃圾回收 - GC（Garbage Collector） 在游戏运行的时候，数据主要存储在内存中，当游戏的数据在不需要的时候，存储当前数据的内存就可以被回收以再次使用。内存垃圾是指当前废弃数据所占用的内存，垃圾回收（GC）是指将废弃的内存重新回收再次使用的过程。 1. 什么时候触发垃圾回收 有三个操作会触发垃圾回收： 在堆内存上进行内存分配操作而内存不够的时候都会触发垃圾回收来利用闲置的内存。 GC会自动触发，不同平台运行频率不一样。 GC被代码强制执行。 2. GC操作带来的问题 直白点就两个问题：一个是Stop-the-world导致的“卡”；一个是内存碎片导致的“堆内存太大”。 GC操作会需要大量的时间来运行，如果堆内存上有大量的变量或者引用需要检查，则检查的操作会十分缓慢，这就会使得游戏运行缓慢。 GC可能会在关键时候运行，例如在CPU处于游戏的性能运行关键时刻，此时任何一个额外的操作都可能会带来极大的影响，使得游戏帧率下降。 另外一个GC带来的问题是堆内存的碎片。当一个内存单元从堆内存上分配出来，其大小取决于其存储的变量的大小。当该内存被回收到堆内存上的时候，有可能使得堆内存被分割成碎片化的单元。也就是说堆内存总体可以使用的内存单元较大，但是单独的内存单元较小，在下次内存分配的时候不能找到合适大小的存储单元，这也会触发GC操作或者堆内存扩展操作。 堆内存碎片会造成两个结果：一个是游戏占用的内存会越来越大；一个是GC会更加频繁地被触发。 特别是在堆内存上进行内存分配时内存单元不足够的时候，GC会被频繁触发，这就意味着频繁在堆内存上进行内存分配和回收会触发频繁的GC操作。 二、Unity托管堆 在讲具体的Unity GC机制之前再回顾一下Unity托管堆。 1. 托管堆的工作原理及其扩展原因 “托管堆”是由项目脚本运行时（Mono或IL2CPP）的内存管理器自动管理的一段内存。必须在托管堆上分配托管代码中创建的所有对象。 Unity官方文档图 在上图中，白框表示分配给托管堆的内存量，而其中的彩色框表示存储在托管堆的内存空间中的数据值。当需要更多值时，将从托管堆中分配更多空间。 GC定期运行将扫描堆上的所有对象，将任何不再引用的对象标记为删除。然后会删除未引用的对象，从而释放内存。 至关重要的是，Unity的垃圾收集是非分代的，也是非压缩的。“非分代”意味着GC在执行每遍收集时必须扫描整个堆，因此随着堆的扩展，其性能会下降。“非压缩”意味着不会为内存中的对象重新分配内存地址来消除对象之间的间隙。 内存空隙 上图为内存碎片化示例。释放对象时，将释放其内存。但是，释放的空间不会整合成为整个“可用内存”池的一部分。位于释放的对象两侧的对象可能仍在使用中。因此，释放的空间成为其他内存段之间的“间隙”（该间隙由上图中的红色圆圈指示）。因此，新释放的空间仅可用于存储与释放相同大小或更小的对象的数据。 这导致了内存碎片化这个核心问题：虽然堆中的可用空间总量可能很大，但是可能其中的部分或全部的可分配空间对象之间存在小的“间隙”。这种情况下，即使可用空间总量高于要分配的空间量，托管堆可能也找不到足够大的连续内存块来满足该分配需求。 如果分配了大型对象又没有足够的连续空间提供使用则： 运行垃圾回收器，尝试释放空间来满足分配请求。 如果在GC运行后，仍然没有足够的连续空间来满足请求的内存量，则必须扩展堆。堆的具体扩展量视平台而定。 2. Unity托管堆的问题 Unity在扩展托管堆后不会经常释放分配给托管堆的内存页, 防止再次发生大量分配时需要重新扩展堆。 在大多数平台上，Unity最终会将托管堆的空置部分使用的页面释放回操作系统。发生此行为的间隔时间是不确定的，不要指望靠这种方法释放内存。 频繁分配临时数据给托管堆，这种情况通常对项目的性能极为不利。 如果每帧分配1KB的临时内存，并且以60帧的速率运行，那么它必须每秒分配60KB的临时内存。在一分钟内，这会在内存中增加3.6MB的垃圾。对内存不足的设备而言每分钟3.6MB的垃圾也无法接受。 三、Unity的GC机制 -- Boehm GC 以前看过Unity使用的GC方案但最近才惊觉现在使用的Unity都是IL2CPP的版本了，所谓的Mono GC本来就已经不存在了。于是来看下现在的IL2CPP的GC机制： Boehm GC（贝姆垃圾收集器）。 1. IL2CPP - Boehm GC 贝姆垃圾收集器是计算机应用在C/C++语言上的一个保守的垃圾回收器（Garbage Collector），可应用于许多经由C/C++开发的程序中。 摘录一段定义： Boehm-Demers-Weiser garbage collector，适用于其它执行环境的各类编程语言，包括了GNU版Java编译器执行环境，以及Mono的Microsoft .NET移植平台。同时支援许多的作业平台，如各种Unix操作系统，微软的操作系统（Microsoft Windows），以及麦金塔上的操作系统（Mac OS X），还有更进一步的功能，例如：渐进式收集（Incremental Collection），平行收集（Parallel Collection）以及终结语意的变化（Variety Offinalizersemantics）。 在Unity中我们可以看到关于Boehm GC的算法部分： BoehmGC.cpp内部调用的就是这个第三方库，他是Stop-the-world类型的垃圾收集器，这表明了在执行垃圾回收的时候，将会停止正在运行的程序，而停止时间的只有在完成工作后才会恢复，所以这就导致了GC引起的程序卡顿峰值，很显然这对游戏的平滑体验造成了较大的负面影响。 通常，解决这个问题的常规方案是尽可能地“减少”运行时垃圾回收（后续用GC代替），亦或者将GC放在不那么操作敏感的场景中，比如回城、死亡后等。但完全避免运行时垃圾回收在大部分时间是不现实的。 接下来我们来看看Boehm GC的背后机制。 2. Boehm GC算法思路 Boehm GC是一种Mark-Sweep（标记-清扫）算法，大致思路包含了四个阶段： 准备阶段：每个托管堆内存对象在创建出来的时候会有一个关联的标记位，来表示当前对象是否被引用，默认为0。 标记阶段：从根内存节点（静态变量；栈；寄存器）出发，遍历扫描托管堆的内存节点，将被引用的内存节点标记为1。 清扫阶段：遍历所有节点，将没有被标记的节点的内存数据清空，并且基于一定条件释放。 结束阶段：触发注册过的回调逻辑。 3. 渐进式GC 使用渐进式GC允许把GC工作分成多个片，因此为了不让GC工作长时间的“阻塞”主线程，将其拆分成了多个更短的中断。需要明确的是这并不会使GC总体上变得更快，但是却可以将工作负载分配到多帧来平缓单次GC峰值带来的卡顿影响。 注: Unity在高版本已经默认是渐进式GC了，大概是Unity 19.1a10版本。 [Unity 活动]-浅谈Unity内存管理_哔哩哔哩_bilibili 4. GC中的内存分配 Boehm GC的使用方法非常简单，只需要将malloc替换为GC_malloc即可，在此之后便无需关心free的问题。 void * GC_malloc(size_t lb) { return GC_malloc_kind(lb, NORMAL); } void * GC_malloc_kind(size_t lb, int k) { return GC_malloc_kind_global(lb, k); } 在整个内存分配链的最底部，Boehm GC通过平台相关接口来向操作系统申请内存。为了提高申请的效率，每次批量申请4KB的倍数大小。 分配器的核心是一个分级的结构，Boehm GC把每次申请根据内存大小归类成小内存对象和大内存对象。 小内存对象：不超过PageSize/2，小于2048字节的对象。 大内存对象：大于PageSize/2的对象。 对于大内存对象，向上取整到4KB的倍数大小，以整数的内存块形式给出。而小内存对象则会先申请一个内存块出来，而后在这块内存上进一步细分为Small Objects，形成free-list。 下面会分别说下大内存对象和小内存对象，参考网上的资料整理，确实有点点干，但是配图我重新做了一下，大概可以辅助消化。 四、IL2CPP - Boehm GC：小内存分配 1. 粒度对齐 实现思路是，提出粒度（GRANULES）的概念，即一个GRANULE的大小是16字节。实际分配内存的时候按照GRANULE为基本单位来分配。分配过程中，按照原始需要的大小，计算并映射得到实际需要分配的GRANULE个数，代码如下： //lb是原始的分配大小，lg是GRANULE（1～128）。 size_t lg = GC_size_map[lb]; 例如需要18字节的内存，则lg=2，即实际分配2个GRANULE（32字节），如果需要1字节的内存，则lg=1，即实际分配1个GRANULE（16字节）。 GC_size_map是一个“GRANULE索引映射表”，用来维护原始分配的内存大小和内存索引之间的关系。最多可以返回128个GRANULE，所以小内存的大小上限是128*16=2048。GC_size_map数组本身会不断加载根据需要不断扩容。 示意 2. 空闲链表 - ok_freelist 决定了GRANULE的大小之后，在申请内存时刻首先会从“空闲链表”中查看是否有空闲内存块，如果有则直接返回这块内存，完成分配，其算法维护了一个数据结构obj_kind： struct obj_kind { void **ok_freelist; struct hblk **ok_reclaim_list; ... } GC_obj_kinds[3]; GC_obj_kinds[3]对应了3种内存类型，分别是PTRFREE、NORMAL和UNCOLLECTABLE，每种类型都有一个obj_kind结构体信息。 PTRFREE：无指针内存分配，明确的告诉GC，该对象内无任何的指针信息，在GC时候无需查找该对象是否引用了其他对象。 NORMAL：无类型的内存分配，因为无法得到对象的类型元数据，所以在GC时会按照只针对其的方式扫描内存块，如果通过了指针校验，就会认为该对象引用了该指针地址指向的对象。 UNCOLLECTABLE：为BOEHM自己分配的内存，这些不需要标记和回收。 每一个obj_kind的结构体都维护了一个ok_freelist的二维指针链表用来存放空闲的内存块。ok_freelist维护了0~127个链表索引。而每一个尺寸的freelist就是对应大小的GRANULE池子，其结构示意如图： freelist示意 于是，根据要申请的内存大小计算得到GRANULE在freelist的索引，然后去查询对应索引的freelist，如果存在空闲看空间ok_freelist[index][ 0]，则将其返回并从链上移除。 ok_freelist链表最初为空，如果ok_freelist中没有相应的空闲内存块，则调用GC_allocobj(lg, k)去底层查找可用的内存。 GC_allocobj的核心逻辑是调用GC_new_hblk(gran, kind)去底层内存池获取内存，并且查看底层内存池中是否分配了空闲的内存块，如果没有则通过系统函数例如malloc分配内存给底层内存池，如果内存池有，直接取出一块返回。GC_new_hblk的代码逻辑如下： GC_INNER void GC_new_hblk(size_t gran, int kind) { struct hblk *h; /* the new heap block */ GC_bool clear = GC_obj_kinds[kind].ok_init; /* Allocate a new heap block */ h = GC_allochblk(GRANULES_TO_BYTES(gran), kind, 0); if (h == 0) return; /* Build the free list */ GC_obj_kinds[kind].ok_freelist[gran] = GC_build_fl(h, GRANULES_TO_WORDS(gran), clear,(ptr_t)GC_obj_kinds[kind].ok_freelist[gran]); } GC_new_hblk的主要逻辑有2步： 调用GC_allochblk方法进一步获取内存池中可用的内存块； 调用GC_build_fl方法，利用内存池中返回的内存块构建ok_freelist，供上层使用。 3. 核心内存块链表GC_hblkfreelist 底层内存池的实现逻辑和ok_freelist类似，维护了一个空闲内存块链表的指针链表GC_hblkfreeelist，但是和ok_freelist不同的是，这个链表中的内存块的基本单位是4KB，也就是一个内存页（page_size）的大小。GC_hblkfreelist一个有60个元素，每一个元素都是一个链表。 4. 内存块 - hblk、头信息 - hblkhdr 链表中的每一个内存块都以大小4096（4KB）为一基本单位，一个大小为4096的内存块被称为hblk，数据定义如下： struct hblk { char hb_body[HBLKSIZE]; //HBLKSIZE=4096 }; 每个hblk拥有一个相应的header信息，用来描述这个内存快的情况，数据的定义如下： //头部信息 struct hblkhdr { struct hblk * hb_next; //指向下一个hblk struct hblk * hb_prev; //指向上一个hblk struct hblk * hb_block; //对应的hblk unsigned char hb_obj_kind; //kink类型 unsigned char hb_flags; //标记位 word hb_sz; //如果给上层使用，则表示实际分配的单位，如果空闲，则表示内存块的大小 word hb_descr; size_t hb_n_marks;//标记位个数，用于GC word hb_marks[MARK_BITS_SZ]; //标记为，用于GC } 5. hblk内存块查询 structh blk *GC_allochblk(size_t sz, int kind, unsigned flags/* IGNORE_OFF_PAGE or 0 */) { ... //1.计算需要的内存块大小 blocks_needed = OBJ_SZ_TO_BLOCKS_CHECKED(sz); start_list = GC_hblk_fl_from_blocks(blocks_needed); //2.查找精确的hblk内存块 result = GC_allochblk_nth(sz, kind, flags, start_list, FALSE); if (0 != result) return result; may_split = TRUE; ... if (start_list < UNIQUE_THRESHOLD) { ++start_list; } //3.从更大的内存块链表中找 for (; start_list <= split_limit; ++start_list) { result = GC_allochblk_nth(sz, kind, flags, start_list, may_split); if (0 != result) break; } return result; } STATIC int GC_hblk_fl_from_blocks(word blocks_needed) { if (blocks_needed <= 32) return blocks_needed; if (blocks_needed >= 256) return (256-32)/8+32; return (blocks_needed-32)/8+32; } 先根据上层需要分配的内存大小计算出需要的内存块大小，如果申请的大小小于4096字节，则结果是1，对于小对象内存块的个数就是1。 根据实际需要的内存块数，判断并决定从哪一个GC_hblkfreelist链表查找，start_list是开始查找的链表index，即从GC_hblkfreelist[start_list]开始查找。并不是需要blocks，就一定会从GC_hblkfreelist[blocks]的链表中查找，遵循转换规则（小内存索引是连续的，中内存索引是32+8的步长，大点的内存索引都是60）。 如果blocks_needed小于32，则startlist=blocks_needed，直接去GC_hblkfreelist[blocks_needed]中查找。 如果blocks_needed位于32～256，则startlist=(blocks_needed-32)/8+32，即blocks_needed每增加8个，对应GC_hblkfreelist[index]的index增加1。 如果blocks_needed大于256，则都从GC_hblkfreelist[60]链表中查找。 决定从哪个链表开始查找之后，首先进行精确查找，如果直接找到，则直接返回找到的内存块。 如果精准查找失败，则逐渐增大start_list，从更大的内存块链表中查找。 STATIC struct hblk *GC_allochblk_nth(size_t sz, int kind, unsigned flags, int n, int may_split) { struct hblk *hbp; hdr * hhdr; struct hblk *thishbp; hdr * thishdr;/* Header corr. to thishbp */ //计算需要分配的内存块大小 signed_word size_needed = HBLKSIZE * OBJ_SZ_TO_BLOCKS_CHECKED(sz); //从链表中查找合适的内存块 for (hbp = GC_hblkfreelist[n];; hbp = hhdr -> hb_next) { signed_word size_avail; if (NULL == hbp) return NULL; //获取内存块的header信息 GET_HDR(hbp, hhdr); //内存块大小 size_avail = (signed_word)hhdr->hb_sz; if (size_avail < size_needed) continue; //可用内存大于需要的分配的大小 if (size_avail != size_needed) { //要求精准不分割，退出循环，返回空 if (!may_split) continue; ... if( size_avail >= size_needed ) { ... //分割内存块，修改链表 hbp = GC_get_first_part(hbp, hhdr, size_needed, n); break; } } } if (0 == hbp) return0; ... //修改header信息 setup_header(hhdr, hbp, sz, kind, flags) ... return hbp; } 当分配字节的时候先通过精确查找如果发现有精确内存，则会返回相应的内存块，如果没有发现精确内存则会去查找更大的内存块并进行分割，一半返回使用，一半放到池子里。 拆分示意 如上图示例，如果要申请1KB，则会先找4KB，如果没有4KB则去找8KB，找到了8KB就进行两个4KB的拆分，然后移除8KB出池子，再把拆分过的另一半4KB内存块加入到池子里： STATIC struct hblk *GC_get_first_part(struct hblk *h, hdr *hhdr, size_t bytes, int index) { word total_size = hhdr -> hb_sz; struct hblk * rest; hdr * rest_hdr; //从空闲链表删除 GC_remove_from_fl_at(hhdr, index); if (total_size == bytes) return h; //后半部分 rest = (struct hblk *)((word)h + bytes); //生成header信息 rest_hdr = GC_install_header(rest); //内存块大小 rest_hdr -> hb_sz = total_size - bytes; rest_hdr -> hb_flags = 0; ... //加入相应的空闲链表 GC_add_to_fl(rest, rest_hdr); } 6. 内存块分配 如果GC_hblkfreelist空闲链表中找不到合适的内存块，则考虑从系统开辟一段新的内存，并添加到GC_hblkfreelist链表中。在GC_expand_hp_inner方法中实现： GC_INNER GC_bool GC_expand_hp_inner(word n) { ... //调用系统方式开辟内存 space = GET_MEM(bytes); //记录内存地址和大小 GC_add_to_our_memory((ptr_t)space, bytes); ... //添加到GC_hblkfreelist链表中 GC_add_to_heap(space, bytes); ... } GC_add_to_heap方法将创建出来的内存块加入相应的GC_hblkfreelist链表中。同时加入一个全局的存放堆内存信息的数组中。 其中如果发现内存连续的前后内存块存在且空闲，则合并前后的内存块，生成一个更大的内存块。 7. ok_freeList 在GC_new_hblk中调用GC_build_fl方法构建链表，就是这个GC系统的缓存池核心数据结构。 //构建ok_freelist[gran] GC_obj_kinds[kind].ok_freelist[gran] = GC_build_fl(h, GRANULES_TO_WORDS(gran), clear,(ptr_t)GC_obj_kinds[kind].ok_freelist[gran]); GC_INNER ptr_t GC_build_fl(struct hblk *h, size_t sz, GC_bool clear, ptr_t list) { word *p, *prev; word *last_object;/* points to last object in new hblk*/ ... //构建链表 p = (word *)(h -> hb_body) + sz;/* second object in *h*/ prev = (word *)(h -> hb_body);/* One object behind p*/ last_object = (word *)((char *)h + HBLKSIZE); last_object -= sz; while ((word)p <= (word)last_object) { /* current object's link points to last object */ obj_link(p) = (ptr_t)prev; prev = p; p += sz; } p -= sz; //拼接之前的链表 *(ptr_t *)h = list; //返回入口地址 return ((ptr_t)p); } 以4096字节的内存块划分为16字节单元的freeList为例，步骤如下： 4096字节按照16字节分配，划分为256个小内存块，编号是0～255，将最后一个内存块（255）作为新链表的首节点。 内存地址向前遍历，建立链表，即255的下一个节点是254，尾节点是0。 将尾节点的下一个节点指向原链表的首地址。 将新链表的首节点地址作为ok_freelist[N]，N是上文提到的GRANULE，例如16字节对应1。 重建好的freeList，并将首节点提供给上层使用。 五、Boehm GC：大内存分配 分配大内存对象是指分配的内存大于2048字节。 OBJ_SZ_TO_BLOCKS用于计算需要的hblk内存块的个数，对于大内存，需要的个数大于等于1。例如需要分配9000字节的内存，则需要3个hblk内存块，然后调用GC_alloc_large分配内存。 GC_INNER ptr_t GC_alloc_large(size_t lb, int k, unsigned flags) { struct hblk * h; word n_blocks; ptr_t result; ... n_blocks = OBJ_SZ_TO_BLOCKS_CHECKED(lb); ... //分配内存 h = GC_allochblk(lb, k, flags); ... //分配失败，系统分配内存块后继续尝试分配 while (0 == h && GC_collect_or_expand(n_blocks, flags != 0, retry)) { h = GC_allochblk(lb, k, flags); retry = TRUE; } //记录大内存创建大小 size_t total_bytes = n_blocks * HBLKSIZE; ... GC_large_allocd_bytes += total_bytes; ... result = h -> hb_body; //返回内存地址 return result; } 大内存分配的内存查找和小对象方式一样，会不断增加start_list。从更大的链表中查找是否有空闲内存，不同的是，如果查找到了空闲内存不会分裂构建ok_freeList链表而是直接返回大内存块的地址提供使用。 六、Boehm GC：内存分配流程图 示意 七、额外：SGen GC Simple Generational Garbage Collection简称SGen GC，是相比Boehm GC（贝姆GC）更为先进的一种GC方式。官方Mono在2.8版本中增加了SGen GC，但默认的仍是Boehm GC。3.2版本之后，Mono正式将SGen GC作为默认GC方式。 SGen GC将堆内存分为初生代（Nursery）和旧生代（Old Generation）两代进行管理，并包含两个GC过程：Minor GC对初生代进行清理；Major GC对初生代和旧生代同时进行清理。 1. 内存分配策略 - 初代 在SGen GC中，初生代是一块固定大小的连续内存，默认为4MB，可以通过配置修改。这一点与G1不同，在G1中同一代的Region在物理上是不要求连续的。 为了支持多线程工作，新对象的内存分配依然在每个线程的TLAB中进行，当前每个TLAB均为4KB，有提到可能会在不久后进行优化。而在TLAB内部，内存分配是通过指针碰撞的方式进行的，也就是说，在SGen GC中，初生代内存并没有进行粒度划分也没有分块管理。 初生代对象跟随Minor GC和Major GC进行回收。 2. 内存分配策略 - 旧代 在SGen GC中，旧生代内存划分方式可以概括为： Section（1MB） → Block（16KB）→ Page（4KB）→ Slot（不同粒度） 在使用内存时，按照上述链条依次向下拆分，与贝姆GC相同，同一个Block中的Page也只能拆分成相同粒度的Slot。 虽然在初生代中并没有划分内存粒度，但是当对象从初生代转移到旧生代时会找到对应粒度的Slot进行存储。释放对象时，对应的Slot也会返还给空闲链表（类似贝姆GC中的ok_freeList），并在某一级结构完全清空时依次向上一级返还。 旧生代内存最终是通过一个GCMemSection结构的链表进行管理的。 3. 内存分配策略 - 大对象 超过8KB的对象均被视为大对象，大对象通过单独的LOSSection结构进行管理。而大对象的内存管理又分为两种情况： 不超过1MB的，仍然存储在Mono自己的托管堆上，清理后返还给托管堆； 超过1MB的，直接从操作系统申请内存，清理后内存也同样返还给操作系统。 4. 内存分配策略 - 固定内存对象 有一些对象被显式或隐式地标记为了固定内存的对象，这些对象在初始时依然被分配在初生代中，但不会被GC过程移动位置。 显式：用户显式声明的，比如通过fixed关键字进行修饰； 隐式：在GC开始时，所有寄存器和ROOT中直接指向的对象都视为固定内存对象。 这是侑虎科技第1924篇文章，感谢作者Jamin供稿。欢迎转发分享，未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859） 作者主页：https://www.zhihu.com/people/liang-zhi-ming-70 再次感谢Jamin的分享，如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859）
哪些因素和参数会影响Bloom的性能开销
1）哪些因素和参数会影响Bloom的性能开销 2）为何Scripting Threads的#0线程数量异常 这是第457篇UWA技术知识分享的推送，精选了UWA社区的热门话题，涵盖了UWA问答、社区帖子等技术知识点，助力大家更全面地掌握和学习。 UWA社区主页：community.uwa4d.com UWA QQ群：793972859 From UWA AI 问答 Q：哪些因素和参数会影响bloom的性能开销？ A：Bloom（泛光）的性能开销主要来自GPU带宽、计算量与多级采样次数，其关键影响因素和可调参数如下： 一、核心性能影响因素 采样分辨率与下采样层级 Bloom默认从1/2渲染分辨率开始逐级下采样（如Blur1–Blur5），分辨率越低（如1/16）、层级越多，带宽越低但模糊质量下降；反之，高分辨率采样（如1/2）显著增加纹理读取带宽和显存吞吐压力。 移动端建议从1/4或1/8起始，减少首层采样开销。 BloomQuality级别 r.BloomQuality=5（默认）启用全部5层模糊+高质量卷积，开销最大；=1~3可禁用部分层级或使用简化高斯，大幅降低GPU耗时。 ScreenPercentage控制卷积精度 r.Bloom.ScreenPercentage=50表示FFT卷积在50%分辨率下执行；提高该值（如至100%）会线性增加计算与带宽，需谨慎权衡。 泛光卷积（Convolution）启用 启用Bloom Convolution会触发FFT卷积运算，依赖全分辨率内核纹理+额外缓冲区（r.Bloom.ConvolutionBuffer），显著提升GPU计算与显存带宽压力，仅推荐高端设备或离线渲染使用。 后处理链路叠加效应 Bloom常与Tonemapping、Depth of Field、Motion Blur等串联，导致多次全屏纹理读写。尤其在移动平台，多Pass后处理易引发带宽瓶颈（实测可达总GPU带宽的20%+）。 二、关键可调控制台变量（Unreal Engine） 三、优化建议 优先降级BloomQuality+ScreenPercentage，比关闭Bloom更能平衡画质与性能； 使用Stat GPU和Stat RHI定量定位Bloom耗时（查看PostProcessBloom阶段）； 避免在低端机启用Convolution或Dirt Mask（额外纹理采样+混合）； 结合r.VSync与帧率限制，防止Bloom在高帧率下反复重采样。 注：Bloom对GPU温度与功耗影响显著 —— 实测10GB/s带宽可贡献约1000mW功耗（占整机20%+），是移动端发热主因之一。 分享链接： https://www.uwa4d.com/main/uwa-shared.html?shareId=f3f99f69-7e55-4af8-9a58-2531d33ec39a From UWA AI 问答 Q：请教个问题, 我们有用到线程池, 我在Profiler的时候看到在Scripting Threads里面有几十个#0的线程显示, 这个是正常的吗？ A：是不正常的，需立即排查。 Unity的Profiler中出现大量名称为#0的Scripting Threads，通常表明： 线程未正确命名或未注册：Unity默认对System.Threading.ThreadPool中的线程不自动命名，Profiler无法识别其用途，统一显示为#0（或空名+编号）。 线程泄漏风险高：若线程池长期持有大量闲置线程（尤其在Unity 2019 – 2022版本中），可能因ThreadPool.SetMinThreads调用不当、异步任务未完成/未释放、或Task.Run+长阻塞操作导致线程堆积。 非Unity管理线程被误纳入Profiler视图：#0线程无法通过Profiler.BeginThreadProfiling()注册，说明它们未被主动追踪，也意味着你无法在Timeline中定位其耗时来源。 建议动作： 检查是否滥用Task.Run或ThreadPool.QueueUserWorkItem；优先改用Unity Job System或Unity.Collections.LowLevel.Unsafe异步方案。 确认无new Thread(...).Start()后未调用Profiler.EndThreadProfiling()（否则资源泄漏且线程残留）。 在Editor中启用Call Stacks+Deep Profiling，筛选GC Alloc/CPU高耗时帧，定位创建线程的调用栈。 使用UWA GOT Online的Mono/Threading模块，查看线程数趋势与存活时间，确认是否随游戏时长持续增长。 注：Unity官方不鼓励直接使用.NET线程池进行高频短任务调度 —— Job System才是跨平台、可Profiler可视化的首选。 分享链接： https://www.uwa4d.com/main/uwa-shared.html?shareId=3ba6d84d-9e4e-4fb2-a66f-f6fdc562b15f 无论是社区里开发者们的互助讨论，还是AI基于知识沉淀的快速反馈，核心都是为了让每一个技术难题都有解、每一次踩坑都有回响。本期分享分别来自UWA AI问答和UWA问答社区，希望这些从真实开发场景中提炼的经验，能直接帮你解决当下的技术卡点，也让你在遇到同类问题时，能更高效地找到破局方向。 封面图来源于网络 今天的分享就到这里。生有涯而知无涯，在漫漫的开发周期中，我们遇到的问题只是冰山一角，UWA社区愿伴你同行，一起探索分享。欢迎更多的开发者加入UWA社区。 UWA官网：www.uwa4d.com UWA社区：community.uwa4d.com UWA学堂：edu.uwa4d.com 官方技术QQ群：793972859
Gears 实测室：第八期・全面透视4X游戏性能瓶颈
在4X策略游戏的宏大战场上，每一场征战都是对性能系统的终极考验。从万人同屏的史诗会战到漫长精细的资源运营，庞大的数据模拟、复杂的地图渲染与持续运行的进程承载，让性能压力在玩家漫长的征服之路上悄然累积。这种多维度、长周期的复合负载，使4X游戏成为性能优化挑战最为严峻的类型之 —— 从CPU计算、内存管理到画面渲染，每一个环节都直接关系到游戏体验的流畅与响应。 面对如此挑战，UWA Gears能帮助开发团队精准锁定性能瓶颈，并将问题转化为清晰的优化蓝图。让玩家在运筹帷幄、扩张帝国之时，依然享受丝滑的操作与即时的反馈。 《Total Battle》作为一款上线近九年、凭借玩法融合与深度运营在激烈SLG市场中保持生命力的典型产品，其长期稳定运行离不开持续的性能优化。本文将以该游戏为例，系统展示如何运用UWA Gears对这类4X游戏进行全面的性能剖析，定位核心瓶颈，并提供可行的优化方向。 工具声明 UWA Gears仅采集性能数据，评测仅为展示工具功能。 测试环境 为了更直观地展示真实体验，我们选择了中低端机型Realme 8作为测试设备。 性能分析 UWA Gears在无需修改游戏代码、不影响游戏正常运行的前提下，即可完成从帧率、内存、GPU到网络、功耗等全链路性能数据的采集。 本文将系统呈现此次诊断的关键数据与分析方法，展示UWA Gears如何将性能监控从“问题描述”阶段，推进至“根因定位”与“优化优先级判断”的工程决策层面，为复杂游戏的性能调优提供可执行的数据蓝图。 1. 帧率表现 帧率是玩家对游戏性能最直接的感知。帧率不稳会直接导致画面卡顿，尤其在快速滑动地图或释放技能时，瞬间的延迟可能打断沉浸感，甚至影响对战结果。 通过UWA Gears我们可以看到，测试过程中FPS基本维持在60帧左右，帧率波动明显，曲线中也出现了多处耗时尖峰。 放大波动显著区间并结合截图分析，可发现帧时间尖峰主要由UI切换引起，峰值大多在100ms左右，属于可接受范围。但若在战斗密集场景中出现，仍可能导致操作反馈延迟，影响玩家即时决策。 2. 内存使用 内存优化不仅关乎长期游玩的稳定性，也直接影响加载速度与后台切换体验，尤其在低配设备上更为关键。 测试初期，PSS内存快速上升至1.77GB，在17:42左右的场景切换后大幅回落。后续流程在场景切换后能有效回收，表明资源管理机制基本健康。但结合Memory Detail中的各项内存曲线进一步观察，内存仍存在优化空间，进一步降低内存占用可提升多任务切换时的稳定性。 GL mtrack内存曲线与PSS走势基本一致，说明图形资源（如纹理、模型）是内存增长的主因，需关注其加载与释放策略。Native Heap在进入游戏后迅速增长至约900MB，且测试过程中未见释放，可以使用Perfetto等工具对Native内存进行深入排查。 3. GPU性能 GPU负载直接影响画面流畅度与设备发热。如果GPU负载过高不仅会导致帧率下降，还会引起设备发热、耗电加快，影响长时间游戏体验。 整体来看，GPU性能表现良好，主城的GPU Clocks相较其他场景较高，但依然处于合理范围内。GPU带宽在部分场景中存在峰值，可能与画面中的云雾特效有关，可通过LOD或特效分级进行优化。 针对游戏中的复杂场景，可结合GPU Primitive数据观察不同阶段的图元处理情况，进一步排查渲染管线的优化可能性。 此外，UWA Gears还支持更多GPU相关参数，如GPU Shaded、GPU Shader Cycles、GPU Shader Instructions等，帮助开发者全方位监控GPU性能状态。 4. 更多参数 除了帧率、内存与GPU等核心指标，UWA Gears还提供多维度的参数监控，将这些“软性能”指标纳入监控体系，提供更完整的优化视角。 网络 进入游戏时的资源加载阶段网络下载速率较高，这可能引起短时间功耗上升，需留意中低端机型上潜在的发热影响。 功耗 整机平均功率保持在稳定区间，电量消耗曲线平滑，说明游戏在能效控制上表现良好，有利于长时游戏与电池续航。 Gears的核心价值：不止于监控，更在于洞察 通过本次对《Total Battle》的测试与分析，UWA Gears展现了其全面、精准的性能剖析能力。通过无侵入的数据采集与深度分析，不仅全面、细致地呈现了游戏运行时的性能状况，更通过多维度关联分析，将性能问题转化为具体的优化建议。 主要表现为： 无侵入采集，真实反映性能 无需修改游戏代码即可完成全链路数据采集，确保测试数据真实，大幅降低评估成本。 全链路监控，精准定位瓶颈 覆盖帧率、内存、GPU、网络、功耗与温度，支持从系统层面关联分析，快速定位性能瓶颈。 可视化分析，直观呈现问题 提供丰富的图表与曲线，支持逐层下钻分析，帮助团队直观理解数据，提升排查效率。 场景化性能调优，全面提升体验 基于真实设备数据，协助开发者在性能、画质与续航之间取得平衡，提升大众设备的运行稳定性。 数据说明：本文数据基于《Total Battle》公开版本，测试环境为实验室可控条件，结果仅反映单台设备在测试场景下的性能表现，仅供游戏研发团队优化参考。 无论您的项目处于研发攻坚还是长线运营阶段，UWA Gears都能提供从洞察到执行的完整支持，让每一次优化都有数可依，有路可循。 若您希望针对自家游戏进行类似性能测试，或了解UWA Gears的更多功能细节，可点击阅读原文获得产品下载链接与使用指南。 更多讨论可以前往UWA社区：https://answer.uwa4d.com；或者联系UWA微信号：17502188376；QQ群：793972859。 相关推荐 《UWA Gears 全攻略：从功能到应用场景，助你高效优化游戏性能》
1）如何减少托管堆内存碎片 2）MIX贴图中的sRGB（Color Texture）是否要取消勾选 这是第456篇UWA技术知识分享的推送，精选了UWA社区的热门话题，涵盖了UWA问答、社区帖子等技术知识点，助力大家更全面地掌握和学习。 UWA社区主页：community.uwa4d.com UWA QQ群：793972859 From UWA AI 问答 Q：如何减少托管堆内存碎片？ A：减少托管堆内存碎片是提升游戏运行稳定性和降低GC压力的关键。内存碎片会导致堆空间利用率下降，即使总空闲内存足够，也可能因无法分配连续大块内存而触发不必要的GC或导致分配失败。 1. 识别与诊断 首先需确认是否存在严重碎片问题。使用UWA GOT Online的Mono模式进行测试，重点关注： Mono堆内存总量：观察其增长趋势。 Empty Heap Space（空闲堆空间）：若该值持续增长且占比高，表明存在大量碎片。 GC Allocated波形与实际耗时匹配性：如波峰明显但数值不高，可能因碎片导致GC效率低下。 2. 核心优化策略 避免高频小对象分配 减少byte[]、MemoryStream、DeflateStream等频繁创建的大数组操作。 使用对象池复用常用对象（如List、StringBuilder），避免重复分配与回收。 对于字符串拼接，优先使用StringBuilder并预设容量，避免多次扩容引发的复制。 优化集合类使用 预估集合大小并初始化时指定容量（如new List(100)），防止动态扩容产生临时数组。 避免在循环中调用LINQ方法（如Where、Select），因其常生成迭代器对象造成额外分配。 资源加载与数据处理 加载二进制数据时，使用预先分配的缓冲区（如UnityWebRequest配合DownloadHandlerScript传入固定byte[]），避免每帧生成新数组。 资源按需加载，避免一次性加载过多Asset导致堆膨胀。可结合Addressables实现细粒度管理。 代码层规避装箱 避免将值类型作为Object传递（如字典键值、事件参数）。 使用泛型替代非泛型集合（如Dictionary<int, T>而非Hashtable）。 扩展支持泛型参数的格式化方法（如AppendFormat），消除StringBuilder.AppendFormat中的装箱。 3. 引擎与配置调优 禁用不必要的反射：Type.GetProperty()、GetValue()易引发AOT问题且不利于剥离，尽量通过接口或委托替代。 合理设置GC策略：在移动平台启用增量式GC（Incremental GC），分散收集压力。 控制场景切换频率：频繁加载/卸载场景会加剧堆碎片，可通过异步加载与资源预加载平滑过渡。 4. 工具辅助分析 利用Unity Profiler的Memory模块截取堆快照，查看大块空闲区域分布。 结合GOT Online报告中的驻留对象分析，定位长期存活的小对象来源，针对性重构。 通过以上措施，可显著降低托管堆碎片率，提升内存利用效率和运行流畅度。 分享链接： https://www.uwa4d.com/main/uwa-shared.html?shareId=49ae18c9-5cb3-4f4d-a4d4-a2b451551c11 From 问答社区 Q：我在排查项目中材质资源的时候发现MIX贴图中sRGB（Color Texture）有的有勾选，有的没勾选。 问了AI都是建议取消勾选，但是美术那边又说取消勾选可能效果就不对了。 这边还是问下诸位同僚，你们项目中的MIX贴图里的sRGB（Color Texture）选项是否都是取消勾选的？ A：和美术这边做了几个测试，确认是得取消勾选的。 之前导入资源时默认勾选了这个选项，导致后续都是在错误的基础上调整效果，所以一开始取消勾选会认为效果不对。 实际上是暴露了错误参数下的效果（比方说光滑度过高导致取消勾选后物件变得油腻）。 感谢题主土司喵@UWA问答社区提供了回答，有经验的朋友欢迎转至社区交流： https://answer.uwa4d.com/question/692eb082682c7e5cd61bfb78 无论是社区里开发者们的互助讨论，还是AI基于知识沉淀的快速反馈，核心都是为了让每一个技术难题都有解、每一次踩坑都有回响。本期分享分别来自UWA AI问答和UWA问答社区，希望这些从真实开发场景中提炼的经验，能直接帮你解决当下的技术卡点，也让你在遇到同类问题时，能更高效地找到破局方向。 封面图来源于网络 今天的分享就到这里。生有涯而知无涯，在漫漫的开发周期中，我们遇到的问题只是冰山一角，UWA社区愿伴你同行，一起探索分享。欢迎更多的开发者加入UWA社区。 UWA官网：www.uwa4d.com UWA社区：community.uwa4d.com UWA学堂：edu.uwa4d.com 官方技术QQ群：793972859
告别“数据迷宫”！UWA AI批量分析上线，让千万条测试数据开口说话
“这版Shader优化后，3款机型的渲染耗时降了但内存涨了，是巧合吗？” “QA连续测了8次加载场景，为什么第5次开始突然掉帧？” “对比上周的测试数据，哪些优化措施真正起了作用？” —— 每到版本迭代关键期，Unity开发者和QA人员总会陷入这样的“数据迷雾”。 单份报告的AI解读精准却片面，多份数据的人工比对耗时且易漏，传统分析模式早已跟不上敏捷开发的节奏。如今，深耕游戏性能优化十年的UWA给出了破局答案：UWA GOT Online「AI 批量分析」功能正式上线！作为AI问答、AI解读功能的重磅升华，它将分散的测试数据串联成系统的洞察网络，让开发者告别“盲人摸象”，从全局视角掌控性能真相。 一次操作，解锁三大核心场景的系统性分析 打开UWA GOT Online的“批量分析”入口，AI将自动完成数据关联与深度挖掘，并智能推荐分析质量较高的数据报告： 几分钟后就能分析报告的性能趋势和呈现的问题，直击三类高频需求： 跨报告趋势追踪：自动聚合FPS均值、Jank均值、设备内存等核心指标的变化曲线。比如批量分析结果中，AI会明确标注“备注***的报告中，FPS上升或者下降，是否超出推荐值”，让优化效果与潜在风险一目了然。 多维度问题定位，避免人工误判与遗漏，产能直接翻倍。单份报告只能卡着“单点视角”看问题，但多份报告的分析工作本身存在大量操作导致的时间损耗，即使简化至两份数据对比这种最常见的应用场景，分析师也要挨个点开十几个页签手动比对，不仅耗上1-2小时，还容易漏过数据间的关键关联。而AI批量分析直接打破这种局限，既能快速揪出“藏在多份数据里的共性问题”，也能精准挖出“不同报告的个性差异”。全程智能化完成，效率和精准度直接拉满。 比如某MMORPG项目测了4款机型，AI先锁定“所有机型Mono堆内存占用高”的共性，再拆解个性：既不用团队反复切换报告记数据，也避免把个性差异误判成单一机型兼容问题，大幅省了无效分析的时间。 测试行为诊断：智能分析测试习惯与数据质量的关联。针对QA团队的测试记录，AI会输出“测试覆盖建议”：“近10次测试中，低配机型占比仅20%，且未覆盖高温环境场景，存在性能隐患”；同时标注“重复测试提示”：“第6、7轮测试参数完全一致，可合并分析节省时间”，帮团队优化测试流程。 UWA AI批量分析的底气，源于十年的服务经验积累，更精准切中Unity开发者的核心痛点，对UWA而言，AI批量分析不是孤立功能的叠加，更是“AI + 性能优化”生态的关键一环。 欢迎一并探索UWA AI全家桶之： AI问答 & AI解读！ 即刻试用：让你的测试数据发挥最大价值 目前，UWA AI批量分析已全面开放至GOT Online平台，我们诚挚邀请Unity开发者与QA团队登录官网（https://www.uwa4d.com/）体验。同时，我们期待你的反馈：若需要特定品类的分析模型，或希望新增自定义指标关联功能 —— 你的需求，正是UWA AI进化的方向。 关于UWA UWA是一家创业十年的高新技术企业，作为游戏行业的深耕者，UWA始终专注于为使用Unity、Unreal引擎的开发者提供丰富的优化产品，帮助开发者高效解决开发问题、定位性能瓶颈、提供解决方案，已支持超过一万款游戏项目。还打造了技术博客、问答、开源库、学堂等社区产品，为开发者提供便利和高效的支持。线上培训和线下教育的新业务， 满足行业对人才培育的需求。