前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Unity性能调优手册9Unity的Script:空生命周期函数,tags,组件,string,显式销毁的类(Texture2D、Sprite、Material),burst

Unity性能调优手册9Unity的Script:空生命周期函数,tags,组件,string,显式销毁的类(Texture2D、Sprite、Material),burst

作者头像
立羽
发布2023-11-27 10:37:32
1750
发布2023-11-27 10:37:32
举报
文章被收录于专栏:Unity3d程序开发Unity3d程序开发

翻译自https://github.com/CyberAgentGameEntertainment/UnityPerformanceTuningBible/

Unity的Script

随意使用Unity提供的功能可能会导致意想不到的陷阱。本章通过实际的例子介绍了与Unity内部实现相关的性能调优技术。

空Unity事件函数

当Unity提供的事件函数(如Awake, Start和Update)被定义时,它们会在运行时缓存在Unity内部列表中,并通过列表的迭代执行。 即使在函数中没有做任何事情,它也会被缓存,因为它被定义了。保留不需要的事件函数将使列表膨胀并增加迭代成本。 例如,如下面的示例代码所示,Start和Update是从Unity上新生成的脚本开始定义的。如果您不需要这些函数,请务必删除它们。

代码语言:javascript
复制
public class NewBehaviourScript : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
    }
    // Update is called once per frame
    void Update()
    {
    }
}

译者真机部分 可以通过扫描代码,找出空的Start,Update函数 https://www.cnblogs.com/mrblue/p/5530370.html

使用tags与names

从UnityEngine继承的类。对象提供标记和名称属性。这些属性对于对象标识很有用,但实际上GC.Alloc。 我从UnityCsReference中引用了他们各自的实现。您可以看到,这两个调用进程都是用本机代码实现的。 Unity用c#实现脚本,但Unity本身是用c++实现的。由于c#内存空间和c++内存空间不能共享,所以分配内存是为了将字符串信息从c++端传递到c#端。这是在每次调用它时完成的,所以如果您想多次访问它,您应该缓存它 有关Unity如何在c#和c++之间工作和内存的更多信息,请参阅“Unity Runtime”。 取自UnityCsReference GameObject.bindings.cs

代码语言:javascript
复制
public extern string tag
{
    [FreeFunction("GameObjectBindings::GetTag", HasExplicitThis = true)]
    get;
    [FreeFunction("GameObjectBindings::SetTag", HasExplicitThis = true)]
    set;
}

取自UnityEngineObject.bindings.cs

代码语言:javascript
复制
public string name
{
get { return GetName(this); }
set { SetName(this, value); }
}
[FreeFunction("UnityEngineObjectBindings::GetName")]
extern static string GetName([NotNull("NullExceptionObject")] Object obj);

译者增加部分 tag是场景中GameObject的标签,而GameObject的成员tag是一个属性,在获取该属性时,实质上是调用get_tag()函数,从native层返回一个字符串。字符串属于引用类型,这个字符串的返回,会造成堆内存的分配。然而,Unity引擎也没有通过缓存的方式对get_tag进行优化,在每次调用get_tag时,都会重新分配堆内存。所以如果频繁使用,在类成员中保存起来

获取组件

在下面的示例代码中,您将有每帧搜索刚体组件的成本。如果您经常访问该站点,则应该使用该站点的预缓存版本。

代码语言:javascript
复制
void Update()
{
    Rigidbody rb = GetComponent<Rigidbody>();
    rb.AddForce(Vector3.up * 10f);
}

译者增加部分 在Lua中使用GetComponent 【腾讯文档】Lua缓存C#类型 https://docs.qq.com/doc/DWklHQWxRa2NlTGpI

使用Transform

Transform组件是经常访问的组件,例如位置、旋转、规模(扩展和收缩)以及父子关系更改。如下面的示例代码所示,您经常需要更新多个值。

代码语言:javascript
复制
void SetTransform(Vector3 position, Quaternion rotation, Vector3 scale)
{
    transform.position = position;
    transform.rotation = rotation;
    transform.localScale = scale;
}

当transform被检索时,在Unity内部调用GetTransform()过程。它经过了优化,比上一节中的GetComponent()更快。但是,它比缓存的情况要慢,因此也应该缓存和访问它,如下面的示例代码所示。对于位置和旋转,你也可以使用SetPositionAndRotation()来减少函数调用的次数

代码语言:javascript
复制
void SetTransform(Vector3 position, Quaternion rotation, Vector3 scale)
{
    var transformCache = transform;
    transformCache.SetPositionAndRotation(position, rotation);
    transformCache.localScale = scale;
}

需要显式丢弃的类

因为Unity是用c#开发的,所以不再被GC引用的对象会被释放。然而,Unity中的一些类需要被明确地销毁。典型的例子有Texture2D、Sprite、Material和PlayableGraph。如果使用new或专用的Create函数生成它们,请确保显式地销毁它们。

代码语言:javascript
复制
void Start()
{
    _texture = new Texture2D(8, 8);
    _sprite = Sprite.Create(_texture, new Rect(0, 0, 8, 8), Vector2.zero);
    _material = new Material(shader);
    _graph = PlayableGraph.Create();
}
void OnDestroy()
{
    Destroy(_texture);
    Destroy(_sprite);
    Destroy(_material);
    if (_graph.IsValid())
    {
        _graph.Destroy();
    }
}

String规范

避免使用字符串指定要在Animator中播放的状态和要在Material中操作的属性。

代码语言:javascript
复制
_animator.Play("Wait");
_material.SetFloat("_Prop", 100f);

在这些函数中,Animator.StringToHash()和Shader.PropertyToID()被执行以将字符串转换为唯一的标识值。由于在多次访问站点时每次都执行转换是浪费的,因此缓存标识值并重复使用它。如下面的示例所示,为了便于使用,建议定义一个列出缓存标识值的类。

代码语言:javascript
复制
public static class ShaderProperty
{
    public static readonly int Color = Shader.PropertyToID("_Color");
    public static readonly int Alpha = Shader.PropertyToID("_Alpha");
    public static readonly int ZWrite = Shader.PropertyToID("_ZWrite");
}
public static class AnimationState
{
    public static readonly int Idle = Animator.StringToHash("idle");
    public static readonly int Walk = Animator.StringToHash("walk");
    public static readonly int Run = Animator.StringToHash("run");
}

JsonUtility的问题

Unity为JSON序列化/反序列化提供了一个类JsonUtility。官方文档(https://docs.unity3d.com/ja/current/Manual/JSONSerialization.html )还指出,它比c#标准更快,并且经常用于性能敏感的实现 JsonUtility(尽管它的功能比.Net的JSON少)在基准测试中被证明比常用的要快得多。 然而,有一件与性能相关的事情需要注意。但是有一个与性能相关的问题需要注意null的处理 下面的示例代码显示了序列化过程及其结果。您可以看到,即使类A的成员b1被显式地设置为null,它也是用默认构造函数生成的类B和类C进行序列化的。序列化为null的对象,在JSON转换期间将新建一个虚拟对象,因此您可能需要考虑到这个开销。

代码语言:javascript
复制
[Serializable] public class A { public B b1; }
[Serializable] public class B { public C c1; public C c2; }
[Serializable] public class C { public int n; }
void Start()
{
    Debug.Log(JsonUtility.ToJson(new A() { b1 = null, }));
    // {"b1":{"c1":{"n":0}, "c2":{"n":0}}
}

Render 与 MeshFilter的问题

Renderer.material与MeshFilter.mesh会产生重复的实例,使用结束后必须显式销毁。官方文件也分别明确说明了以下几点。 如果材质被任何其他renderers渲染器使用,这将克隆共享材质并从现在开始使用它。 将获取的材料和网格保存在成员变量中,并在适当的时候销毁它们。当游戏对象被销毁时,销毁自动实例化的网格与材质。

代码语言:javascript
复制
void Start()
{
    _material = GetComponent<Renderer>().material;
}
void OnDestroy()
{
    if (_material != null) 
        Destroy(_material);
}

译者增加部分 可以使用MaterialPropertyBlock修改材质 【腾讯文档】材质MaterialPropertyBlock https://docs.qq.com/doc/DWnhRZ09Za2xzTVBY

删除日志输出代码

Unity提供了Debug.Log()、Debug.LogWarning()和Debug.LogError()等日志输出函数。虽然这些函数很有用,但它们也存在一些问题。 •日志输出本身是一个繁重的过程。 •它也在发布版本中执行。 •字符串生成和连接会导致GC.Alloc。 如果你关闭Unity中的Logging设置,堆栈跟踪将停止,但是日志将被输出。如果UnityEngine.Debug.unityLogger.logEnabled设置为false。Unity,没有日志记录输出,但由于它只是函数内部的一个分支,函数调用成本和字符串生成和连接应该是不必要的。也可以选择使用#if指令,但是处理所有日志输出处理是不现实的。

代码语言:javascript
复制
#if UNITY_EDITOR
Debug.LogError($"Error {e}");
#endif

在这种情况下可以使用条件属性。如果指定的符号未定义,具有条件属性的函数将被编译器删除调用部分。将条件属性添加到自制类端的每个函数中是一个好主意,作为通过自制日志输出类调用Unity端的日志函数的规则,这样可以在必要时删除整个函数调用。

代码语言:javascript
复制
public static class Debug
{
    private const string MConditionalDefine = "DEBUG_LOG_ON";
    [System.Diagnostics.Conditional(MConditionalDefine)]
    public static void Log(object message)
    => UnityEngine.Debug.Log(message);
}

需要注意的一点是,指定的符号必须能够被函数调用者引用。在#define中定义的符号的作用域将被限制在写入它们的文件中。在每个调用带有条件属性的函数的文件中定义一个符号是不实际的。Unity有一个功能叫做ScriptingDefine Symbols,允许您为整个项目定义符号。这可以在“Project Settings -> Player -> Other Settings”下完成。

在这里插入图片描述
在这里插入图片描述

使用Burst加速代码

Burst 6是用于高性能c#脚本的官方Unity编译器。 Burst使用c#语言的一个子集来编写代码。Burst将c#代码转换为IR(Intermediate Representation中间表示),这是7的中间语法,一个称为LLVM的编译器基础结构,然后在将其转换为机器语言之前对IR进行优化。 此时,代码尽可能地向量化,并替换为SIMD,这是一个主动使用指令的过程。这有望产生更快的程序输出。 SIMD代表单指令/多数据,指的是将单个指令同时应用于多个数据的指令。换句话说,通过主动使用SIMD指令,可以在单个指令中一起处理数据,从而使操作速度比普通指令更快。 *6 https://docs.unity3d.com/Packages/com.unity.burst@1.6/manual/docs/QuickStart.html *7 https://llvm.org/ 使用Burst来加速代码 Burst使用c#的一个子集,称为高性能c# (HPC#) *8来编写代码。 HPC#的一个特性是c#的引用类型,比如类和数组,是不可用的。因此,通常使用结构来描述数据结构。 对于像数组这样的集合,请使用NativeArray之类的NativeContainer *9。有关hpc#的更多细节,请参考脚注中列出的文档。 Burst与c#作业系统一起使用。因此,它自己的处理在实现IJob的作业的Execute方法中描述。通过将bustcompile属性赋给所定义的作业,该作业将被Burst优化。 给出了一个将给定数组的每个元素平方并将其存储在Output数组中的示例

代码语言:javascript
复制
[BurstCompile]
private struct MyJob : IJob
{
    [ReadOnly]
    public NativeArray<float> Input;
    
    [WriteOnly]
    public NativeArray<float> Output;
    
    public void Execute()
    {
        for (int i = 0; i < Input.Length; i++)
        {
            Output[i] = Input[i] * Input[i];
        }
    }
}

第14行中的每个元素都可以独立计算(计算中没有顺序依赖),并且由于输出数组的内存对齐是连续的,因此可以使用SIMD指令一起计算它们。 *8https://docs.unity3d.com/Packages/com.unity.burst@1.7/manual/docs/CSharpLanguageSupport_Types.html *9 https://docs.unity3d.com/Manual/JobSystemNativeContainer.html 您使用BurstInspector 看到使用Burst将代码转换为汇编代码

在这里插入图片描述
在这里插入图片描述

代码第14行的进程将在ARMV8A_AARCH64的程序集中转换为如下

代码语言:javascript
复制
fmul v0.4s, v0.4s, v0.4s
fmul v1.4s, v1.4s, v1.4s

程序集的操作数以.4s为后缀,这一事实证实使用SIMD指令。 在实际设备上比较了用纯c#实现的代码和用Burst优化的代码的性能。 实际设备是Android Pixel 4a和IL2CPP,使用脚本后端进行比较。数组的大小是2^20 = 1,048,576。重复了同样的过程10次,取平均处理时间。

在这里插入图片描述
在这里插入图片描述

我们观察到,与纯c#实现相比,它的速度提高了5.8倍。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-11-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Unity的Script
  • 空Unity事件函数
  • 使用tags与names
  • 获取组件
  • 使用Transform
  • 需要显式丢弃的类
  • String规范
  • JsonUtility的问题
  • Render 与 MeshFilter的问题
  • 删除日志输出代码
  • 使用Burst加速代码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档