英文原文:https://www.jacksondunstan.com/articles/4840
LINQ的CPU性能相当差,但是内存怎么样?每个 LINQ 函数是否总是创建大量垃圾供 GC 收集,或者是否存在不太糟糕的异常?今天的文章测试了许多 LINQ 函数来找出答案!
LINQ 函数有很多。它们中的大多数至少有两次重载,大大增加了它们的总数。对于今天的测试,我们将使用每种方法的一个重载,以保持事情简单明了。重载之间的性能可能会有所不同,但这种差异应该很小。
今天的测试使用了一个简单的类,其中包含几乎所有 LINQ 函数:
class Element
{
public float Value;
}
整个测试在一个 MonoBehaviour 中进行,仅在 Awake 中创建几个字段,并由大多数测试的 LINQ 函数使用:
Element[] array;
Element singleElement;
IOrderedEnumerable<Element> orderedEnumerable;
void Awake()
{
array = new [] { new Element { Value = 1 } };
singleElement = new Element { Value = 4 };
}
接下来,我们有一组 LINQ 函数使用的简单函数:
Element ReturnIdentity(Element a)
{
return a;
}
Element ReturnSecond(Element a, Element b)
{
return b;
}
bool ReturnTrue(Element a)
{
return true;
}
float ReturnValue(Element a)
{
return a.Value;
}
Element ReturnFirst(Element a, IEnumerable<Element> b)
{
return a;
}
IEnumerable<Element> ReturnArray(Element a)
{
return array;
}
这些都作为委托字段缓存在 Awake 中,以避免在测试单个 LINQ 函数时计算委托的创建:
Func<Element, Element> returnIdentityDelegate;
Func<Element, Element, Element> returnSecondDelegate;
Func<Element, bool> returnTrueDelegate;
Func<Element, float> returnValueDelegate;
Func<Element, IEnumerable<Element>, Element> returnFirstDelegate;
Func<Element, IEnumerable<Element>> returnArrayDelegate;
void Awake()
{
returnIdentityDelegate = ReturnIdentity;
returnSecondDelegate = ReturnSecond;
returnTrueDelegate = ReturnTrue;
returnValueDelegate = ReturnValue;
returnFirstDelegate = ReturnFirst;
returnArrayDelegate = ReturnArray;
}
为了将这一切结合在一起,Update 函数执行一个开关以将每个测试分开两帧运行。这使得每个测试运行时在探查器中一目了然,即使测试的名称没有显示。创建一个球体以直观地通知所有测试都已完成。
void Update()
{
switch (testIndex)
{
case 0: Aggregate(); break;
case 2: All(); break;
case 4: Any(); break;
// many more cases...
case 106: GameObject.CreatePrimitive(PrimitiveType.Sphere); break;
}
testIndex++;
}
最后是实际测试功能。每个测试一个 LINQ 函数:
Element Aggregate()
{
return array.Aggregate(returnSecondDelegate);
}
bool All()
{
return array.All(returnTrueDelegate);
}
bool Any()
{
return array.Any(returnTrueDelegate);
}
// many more functions...
总的来说,测试如下:
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class TestScript : MonoBehaviour
{
class Element
{
public float Value;
}
Func<Element, Element> returnIdentityDelegate;
Func<Element, Element, Element> returnSecondDelegate;
Func<Element, bool> returnTrueDelegate;
Func<Element, float> returnValueDelegate;
Func<Element, IEnumerable<Element>, Element> returnFirstDelegate;
Func<Element, IEnumerable<Element>> returnArrayDelegate;
Element[] array;
Element singleElement;
IOrderedEnumerable<Element> orderedEnumerable;
int testIndex;
void Awake()
{
returnIdentityDelegate = ReturnIdentity;
returnSecondDelegate = ReturnSecond;
returnTrueDelegate = ReturnTrue;
returnValueDelegate = ReturnValue;
returnFirstDelegate = ReturnFirst;
returnArrayDelegate = ReturnArray;
array = new [] { new Element { Value = 1 } };
singleElement = new Element { Value = 4 };
}
void Update()
{
switch (testIndex)
{
case 0: Aggregate(); break;
case 2: All(); break;
case 4: Any(); break;
case 6: Append(); break;
case 8: AsEnumerable(); break;
case 10: Average(); break;
case 12: Cast(); break;
case 14: Concat(); break;
case 16: Contains(); break;
case 18: Count(); break;
case 20: DefaultIfEmpty(); break;
case 22: Distinct(); break;
case 24: ElementAt(); break;
case 26: ElementAtOrDefault(); break;
case 28: Empty(); break;
case 30: Except(); break;
case 32: First(); break;
case 34: FirstOrDefault(); break;
case 36: GroupBy(); break;
case 38: GroupJoin(); break;
case 40: Intersect(); break;
case 42: Join(); break;
case 44: Last(); break;
case 46: LastOrDefault(); break;
case 48: LongCount(); break;
case 50: Max(); break;
case 52: Min(); break;
case 54: OfType(); break;
case 56: OrderBy(); break;
case 58: OrderByDescending(); break;
case 60: Prepend(); break;
case 62: Range(); break;
case 64: Repeat(); break;
case 66: Reverse(); break;
case 68: Select(); break;
case 70: SelectMany(); break;
case 72: SequenceEqual(); break;
case 74: Single(); break;
case 76: SingleOrDefault(); break;
case 78: Skip(); break;
case 80: SkipWhile(); break;
case 82: Sum(); break;
case 84: Take(); break;
case 86: TakeWhile(); break;
case 88: ThenBy(); break;
case 90: ThenByDescending(); break;
case 92: ToArray(); break;
case 94: ToDictionary(); break;
case 96: ToList(); break;
case 98: ToLookup(); break;
case 100: Union(); break;
case 102: Where(); break;
case 104: Zip(); break;
case 106: GameObject.CreatePrimitive(PrimitiveType.Sphere); break;
}
testIndex++;
}
Element Aggregate()
{
return array.Aggregate(returnSecondDelegate);
}
bool All()
{
return array.All(returnTrueDelegate);
}
bool Any()
{
return array.Any(returnTrueDelegate);
}
IEnumerable<Element> Append()
{
return array.Append(singleElement);
}
IEnumerable<Element> AsEnumerable()
{
return array.AsEnumerable();
}
float Average()
{
return array.Average(returnValueDelegate);
}
IEnumerable<Element> Cast()
{
return array.Cast<Element>();
}
IEnumerable<Element> Concat()
{
return array.Concat(array);
}
bool Contains()
{
return array.Contains(singleElement);
}
int Count()
{
return array.Count(returnTrueDelegate);
}
IEnumerable<Element> DefaultIfEmpty()
{
return array.DefaultIfEmpty();
}
IEnumerable<Element> Distinct()
{
return array.Distinct();
}
Element ElementAt()
{
return array.ElementAt(0);
}
Element ElementAtOrDefault()
{
return array.ElementAtOrDefault(0);
}
IEnumerable<Element> Empty()
{
return Enumerable.Empty<Element>();
}
IEnumerable<Element> Except()
{
return array.Except(array);
}
Element First()
{
return array.First(returnTrueDelegate);
}
Element FirstOrDefault()
{
return array.FirstOrDefault(returnTrueDelegate);
}
IEnumerable<IGrouping<float, Element>> GroupBy()
{
return array.GroupBy(returnValueDelegate);
}
IEnumerable<Element> GroupJoin()
{
return array.GroupJoin(
array,
returnValueDelegate,
returnValueDelegate,
returnFirstDelegate,
null);
}
IEnumerable<Element> Intersect()
{
return array.Intersect(array);
}
IEnumerable<Element> Join()
{
return array.Join(
array,
returnIdentityDelegate,
returnIdentityDelegate,
returnSecondDelegate);
}
Element Last()
{
return array.Last(returnTrueDelegate);
}
Element LastOrDefault()
{
return array.LastOrDefault(returnTrueDelegate);
}
long LongCount()
{
return array.LongCount(returnTrueDelegate);
}
float Max()
{
return array.Max(returnValueDelegate);
}
float Min()
{
return array.Min(returnValueDelegate);
}
IEnumerable<Element> OfType()
{
return array.OfType<Element>();
}
IOrderedEnumerable<Element> OrderBy()
{
orderedEnumerable = array.OrderBy(returnValueDelegate);
return orderedEnumerable;
}
IOrderedEnumerable<Element> OrderByDescending()
{
return array.OrderByDescending(returnValueDelegate);
}
IEnumerable<Element> Prepend()
{
return array.Prepend(singleElement);
}
IEnumerable<int> Range()
{
return Enumerable.Range(1, 3);
}
IEnumerable<int> Repeat()
{
return Enumerable.Repeat(1, 3);
}
IEnumerable<Element> Reverse()
{
return array.Reverse();
}
IEnumerable<Element> Select()
{
return array.Select(returnIdentityDelegate);
}
IEnumerable<Element> SelectMany()
{
return array.SelectMany(returnArrayDelegate);
}
bool SequenceEqual()
{
return array.SequenceEqual(array);
}
Element Single()
{
return array.Single();
}
Element SingleOrDefault()
{
return array.SingleOrDefault();
}
IEnumerable<Element> Skip()
{
return array.Skip(0);
}
IEnumerable<Element> SkipWhile()
{
return array.SkipWhile(returnTrueDelegate);
}
float Sum()
{
return array.Sum(returnValueDelegate);
}
IEnumerable<Element> Take()
{
return array.Take(0);
}
IEnumerable<Element> TakeWhile()
{
return array.TakeWhile(returnTrueDelegate);
}
IOrderedEnumerable<Element> ThenBy()
{
return orderedEnumerable.ThenBy(returnValueDelegate);
}
IOrderedEnumerable<Element> ThenByDescending()
{
return orderedEnumerable.ThenByDescending(returnValueDelegate);
}
Element[] ToArray()
{
return array.ToArray();
}
Dictionary<Element, Element> ToDictionary()
{
return array.ToDictionary(
returnIdentityDelegate,
returnIdentityDelegate);
}
List<Element> ToList()
{
return array.ToList();
}
ILookup<Element, Element> ToLookup()
{
return array.ToLookup(
returnIdentityDelegate,
returnIdentityDelegate);
}
IEnumerable<Element> Union()
{
return array.Union(array);
}
IEnumerable<Element> Where()
{
return array.Union(array);
}
IEnumerable<Element> Zip()
{
return array.Zip(array, returnSecondDelegate);
}
Element ReturnIdentity(Element a)
{
return a;
}
Element ReturnSecond(Element a, Element b)
{
return b;
}
bool ReturnTrue(Element a)
{
return true;
}
float ReturnValue(Element a)
{
return a.Value;
}
Element ReturnFirst(Element a, IEnumerable<Element> b)
{
return a;
}
IEnumerable<Element> ReturnArray(Element a)
{
return array;
}
}
使用Unity 2018.2.0f2,我在Edit > Project Settings > Player 中设置以下配置:
- Scripting Runtime Version: .NET 4.x Equivalent
- Scripting Backend: IL2CPP
- Api Compatibility Level: .NET Standard 2.0
然后我使用以下设置创建了一个独立的 macOS 版本:
- Development Build: Enabled
- Autoconnect Profiler: Enabled
- Script Debugging: Enabled
- Wait For Managed Debugger: Disabled
- Scripts Only Build: Disabled
剖析器会在启动单机版时快速捕获所有测试数据。然后,只需在 CPU 使用情况剖析器的层次结构视图的 GC 分配列和内存剖析器的每帧 GC 分配中记录帧即可。以下是我得到的结果:
少数 LINQ 函数根本不分配任何垃圾。其中只有 6 个:AsEnumerable、Cast、ElementAt、ElementAtOrDefault、Single 和 SingleOrDefault。
29 个函数仅分配一个大小约为 32-88 字节的对象。这包括 Min 和 Max 等简单函数以及 Select 等常见函数。还有两个函数(All 和 Range)分配两位数字节计数,但分布在两个分配中。
最糟糕的情况是分配了 100 多个字节。这些函数共有 17 个。在最糟糕的情况下,OrderBy 的分配量高达 0.5 KB,而 ToDictionary 的情况也差不多糟糕,分配了 434 字节。其中 10 个函数分配了不止一次,而其余 7 个函数只分配了一次。
考虑到这些信息,很容易建议不要逐帧使用 LINQ,因为在大多数游戏中,GC 分配即使不是完全禁止,通常也应该保持在最低限度。有一些例外不会产生任何垃圾,但这些都是很少使用的小功能。取代 LINQ 的很可能是手动编写的循环。这些可以简单地编写并放置在“辅助”函数中。要获得更完整的解决方案(更像 LINQ 但没有垃圾创建),请查看迭代器库。