Unity3D C# 中使用foreach的GC产出(2023年带数据)

注意:笔者有点被杠怕了…确实也不严谨,也怕看不到,所以开头这里加一句:foreach本身不会产生GC,产生GC的原因是foreach使用了迭代器Enumerator,而取决于容器的不同,有些迭代器的初始化会产生GCAlloc…

一、Foreach究竟会不会产生GC?

很多读者在听一些群内大佬谈话过程中可能会听说foreach遍历集合会产生GC,笔者也是这么了解的,所以很多读者可能会和笔者一样在网上看到各种说法,将信将疑。
主要分为这几个立场:

1.foreach 会产生GC,在unity里别用,Mono的问题
2.foreach产生GC是被遍历的集合有问题,实现的不好,不是foreach的锅
3.foreach的GC问题已经修复了,大家可以毫不顾忌的使用

笔者在搜索了资料的基础上自己亲手实验,试图证明这些结论哪个是正确的,得到的结论是

网上的其他回答太过远古,甚至存在莫名的歧视foreach
有时候会产生一点点GC,但无需否定,甚至在现在可以忽略不计

如果不想看实验过程,可以直接翻到文末有结论!!!!

二、实验过程

首先我们以最常用的Dictionary进行讨论,因为我们经常使用foreach便捷的遍历Dictionary,难以用for进行

1. foreach遍历字典是否存在GC

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;
public class MyGCTest : MonoBehaviour
{
    Dictionary<int,int> dic = new Dictionary<int, int>()
    {
        { 0, 0 },
        { 1, 1 }
    };
    void Update()
    {
        Profiler.BeginSample("ForeachGC");
        foreach (var x in dic){}
        Profiler.EndSample();
    }
}

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

答案显然是存在的但是笔者在不经意间发现,写在Update的foreach,居然仅仅在第一次调用时产生GC,以后的循环的foreach均不产生GC!!

2. foreach遍历字典在什么时候产生GC Alloc

根据上一步,笔者产生了以下猜想

1.字典内增加一个元素,foreach是否会再次产生GC
2.如果我分别遍历多个字典,会不会产生双份的GC

3.如果我遍历几个不同类型的字典呢?

根据以下代码验证,笔者在两个文件中分别监测GC的产生

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;


public class MyGCTest : MonoBehaviour
{
	//先整两个不同的字典
    Dictionary<int, int> dic = new Dictionary<int, int>()
    {
        { 0, 0 }
    };
    Dictionary<int, int> dic2 = new Dictionary<int, int>()
    {
        { 0, 0 },
        { 100,100}
    };
    void Update()
    {
        Profiler.BeginSample("ForeachGC");
        //先遍历一次第一个字典试试
        foreach (var x in dic){}
        //新增一个元素再试试
        if (!dic.ContainsKey(1)) dic.Add(1, 0);
        foreach (var x in dic){}
        //遍历第二个字典
        foreach (var x in dic2) {}
        Profiler.EndSample();
        //此时发现第一帧有0B 的GCAlloc
    }
}
public class MyGCtest2 : MonoBehaviour
{
    Dictionary<int, int> dic2 = new Dictionary<int, int>()
    {
        { 0, 0 },
        { 100,100}
    };
    void Update()
    {
        Profiler.BeginSample("ForeachGC3");
        foreach (var x in dic2) {}
        Profiler.EndSample();
        //第一帧产生96B GCAlloc
    }
}

在这里插入图片描述
结果完全相同,这说明了foreach在同一个方法内,
这样我们就能得出

1.无论遍历几个字典,遍历几次,元素是否改变,都只产生96B的GCAlloc
2.foreach遍历字典的GCAlloc 全局仅产生一次,与所在文件,方法,类都无关

但是我们接下来想试试不同类型的字典…

Dictionary<int, int> dic2 = new Dictionary<int, int>()
    {
        { 0, 0 },
        { 100,100}
    };

    Dictionary<int, float> dic1 = new Dictionary<int, float>()
    {
        { 0, 0.2f },
        { 100,100.0f}
    };
    void Update()
    {
        Profiler.BeginSample("ForeachGC");
        foreach(var x in dic2)
        {

        }
        foreach (var x in dic1)
        {

        }
        Profiler.EndSample();
    }

在这里插入图片描述
GC突然变为192B ,是原来的二倍,显然每个类型的字典都会产生96B

1.无论遍历几个字典,遍历几次,元素是否改变,都只产生96B的GCAlloc
2.foreach遍历字典的GCAlloc 全局仅产生一次,与所在文件,方法,类都无关
3. foreach遍历字典产生GC与字典类型有关

3.foreach 遍历其他Collection呢?

	List<int> list = new List<int>() { 0,1,0};
    int[] arr= new int[3] { 0,1,0};
    void Update()
    {
        Profiler.BeginSample("ForeachGC");
        foreach(var x in list)
        {

        }
        foreach(var x in arr) { }
        Profiler.EndSample();
    }

在这里插入图片描述

甚至第一次GC都没产生,接下来我们仔细发掘一下原理。

三、foreach 为什么在遍历Dictionary时产生GC

foreach本质是对GetEnumerator(),MoveNext()等方法的简化,我们对IEnumerable等接口再熟悉不过了。

1.无论遍历几个字典,遍历几次,元素是否改变,都只产生96B的GCAlloc
2.foreach遍历字典的GCAlloc 全局仅产生一次,与所在文件,方法,类都无关
3. foreach遍历字典产生GC与字典类型有关

产生这些结论,得出foreach的CG产出和字典类型相关,而与其他的因素无关的结论。我能猜测出GetEnumerator始终返回的是Enumerator的单例,每个字典类型都包含一个实例,所以形成每个字典类型都产生一定GC的现象。

在这里插入图片描述
在这里插入图片描述
我们详细展开分析,发现在GetEnumerator处产生96B,在MoveNext处产生96B
甚至笔者为了探究这一内容,写了第三个Dictionary,发现GetEnumerator处产生144B,在MoveNext处产生144B
笔者得到了以下结论

1.每个类型Dictionary<T,K>首次foreach均产生96B 的GCAlloc
2.每个96B的GCAlloc分别为 48B的GetEnumerator()和 48B的MoveNext()

3.Dictionary的迭代方式类似于单例,每个类型全局仅加载一次

四、结论

  1. foreach在遍历System.Collections.Generic内的集合时不会无理由产生不可接受的GC
    事实上,遍历List和数组时不会创建Enumerator,即一直保持0GC
  2. foreach在遍历字典时,仅对每个类型字典在首次调用时产生一次GC,以后同类型字典不会再产生GC,与其他因素无关。
    也就是说,你只需要对Dictionary<int,int>使用过foreach,以后再使用同类型的字典foreach就不会产生GC,无论是否为同一实例,元素是否变化,文件是否相同,方法和类是否相同。
  3. 对字典Values/Keys单独foreach,将产生更多的GC,大概多24B,与上面提到的相近,其他一致。

下面是一些评论的回复,笔者重测的结果…

Re1: 和foreach有关系吗?其实没有,但是往往有关联,重点是迭代器实现的问题,foreach对C#封装的原生容器都没太大问题,但是原生里有几个有GC,小心用户自己封装的可迭代容器即可。

Re2: 部分读者认为结论3测错,在此进行统一回复

直接对Values进行迭代
在这里插入图片描述
在这里插入图片描述
直接对字典迭代
在这里插入图片描述
在这里插入图片描述

先迭代字典再迭代Values
在这里插入图片描述

在这里插入图片描述

  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YUE ZHEN PENG

码字不易,如果你想请我喝杯果汁

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值