UnityC#教程——通过用结构替换类来优化代码
通过用结构替换类来优化代码,Unity C# 教程
新的改变
Allocation 分配
让我们从简单的代码开始,该代码演示了创建类和结构之间的性能比较。
我们将创建 100 万个:
```csharp
public sealed class Example: MonoBehaviour
{
private ClassData[] _classArray;
private StructData[] _structArray;
private void Start()
{
Profiler.BeginSample("Class creation"); //for performance measurements
_classArray = new ClassData[1000000];
for (int i = 0; i < _classArray.Length; i++)
_classArray[i] = new ClassData();
Profiler.EndSample(); //for performance measurements
Profiler.BeginSample("Struct creation"); //for performance measurements
_structArray = new StructData[1000000];
for (int i = 0; i < _structArray.Length; i++)
_structArray[i] = new StructData();
Profiler.EndSample(); //for performance measurements
}
private sealed class ClassData
{
public int Value { get; }
}
private readonly struct StructData
{
public int Value { get; }
}
}

Unity 编辑器分析器屏幕截图
让我们比较一下:
Class 类
- 26.7mb allocation - 26.7MB 分配
- 40.58ms - 40.58毫秒
Struct 结构
- 3.8mb allocation (~7x less)- 3.8MB 分配(~7 倍少)
- 1.88ms (~21x faster) - 1.88ms(~21 倍快)
如何改变文本的样式
这些数字对于不同类型的数据会有所不同,但我们可以清楚地看到结构在这里表现得更好。
为什么?
类的每个新实例都需要在托管内存中进行分配,这会产生一些性能成本。
此外,任何类实例都需要比结构更多的内存,并且必须在托管内存内对齐。
此外,请务必记住,每个类实例都是在堆中单独分配的,并参与垃圾回收循环。这种对性能的影响不会在分配时结束,它会影响以后的 GC 收集。这实际上是考虑结构的更重要原因。
用法
迭 代
让我们添加遍历所有已创建对象的代码
private void Update()
{
Profiler.BeginSample("Iterate Classes"); //for performance measurements
IterateClasses();
Profiler.EndSample(); //for performance measurements
Profiler.BeginSample("Iterate Structs"); //for performance measurements
IterateStructs();
Profiler.EndSample(); //for performance measurements
}
private void IterateClasses()
{
int sum = 0;
for (int i = 0; i < _classArray.Length; i++)
sum += _classArray[i].Value;
}
private void IterateStructs()
{
int sum = 0;
for (int i = 0; i < _structArray.Length; i++)
sum += _structArray[i].Value;
}

Classes 类
- 6.58ms - 6.58毫秒
Structs 结构
- 4.28ms (~1.5x faster) - 4.28ms(~1.5 倍快)
数组内元素的“数据局部性”。
如果它是结构的数组(或集合),那么内存中的数据直接位于数组内部并按顺序进行,这会增加“缓存命中”。
与此相反,类的数组(或集合)仅包含对实例的引用,并且这些实例可能随机分散在内存中,因此性能会因情况而异。
此外,还有访问类实例的基础取消引用。
通过
有 3 种方法可以将这些值传递给方法:
- Class 类
- Struct 结构
- 引用结构(ref/in/out 关键字)
我们将尝试所有这些方法并测量:
private int _sum;
private void Update()
{
ClassData classData = new();
StructData structData = new();
Profiler.BeginSample("Pass class");
for (int i = 0; i < 1000000; i++)
PassClass(classData);
Profiler.EndSample();
Profiler.BeginSample("Pass struct");
for (int i = 0; i < 1000000; i++)
PassStruct(structData);
Profiler.EndSample();
Profiler.BeginSample("Pass struct by ref");
for (int i = 0; i < 1000000; i++)
PassStructByRef(in structData);
Profiler.EndSample();
}
private void PassClass(ClassData data) => _sum += data.Value;
private void PassStruct(StructData data) => _sum += data.Value;
private void PassStructByRef(in StructData data) => _sum += data.Value;

在这种情况下,通过引用传递结构是最快的。
现在让我们修改数据以占用更多内存:
private sealed class ClassData
{
public int Value { get; }
public int Value1 { get; }
public int Value2 { get; }
public int Value3 { get; }
public int Value4 { get; }
}
private readonly struct StructData
{
public int Value { get; }
public int Value1 { get; }
public int Value2 { get; }
public int Value3 { get; }
public int Value4 { get; }
}

直接传递结构变得最繁重,因为更多的数据意味着更多的时间来复制它。
但是通过引用传递结构仍然具有最佳性能。
如果可能的话,考虑通过引用传递结构,特别是如果它在内存方面不是很小的结构。
建议
- 最有可能的是,你不需要可变结构,除非你知道你在做什么,否则更喜欢使用只读结构
- 考虑在代码中性能较高的部分以及包含大量不太重的数据时使用结构
- 不要制作太重的结构。如果对象占用大量内存,则通常最好将其设置为类,否则复制会降低性能
- 使用结构时,请避免“装箱”。如果你发现需要“装箱”一些东西,可以考虑使用类来代替。


3495

被折叠的 条评论
为什么被折叠?



