[C#] LINQ 到底产生了多少垃圾?

文章详细测试了LINQ函数在Unity中的性能,发现大部分函数会产生垃圾对象,尤其是OrderBy和ToDictionary等复杂操作可能导致大量内存分配。建议在游戏开发中谨慎使用逐帧LINQ,考虑手动循环或迭代器库以减少垃圾收集影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

英文原文: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 但没有垃圾创建),请查看迭代器库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值