欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C#效率优化(3)-- 使用foreach时避免装箱

程序员文章站 2022-07-11 09:15:22
Introduction: ※本文不是在描述旧版本Unity中mono编译器导致的foreach语句额外装箱错误 博主是一名Unity 3D游戏开发者,游戏使用C#+lua开发,最近在优化C#代码时,发现了一处使用foreach不恰当的地方,其结果是造成了每帧近3k的GC Alloc,如此高频率的G ......

introduction:

  ※本文不是在描述旧版本unity中mono编译器导致的foreach语句额外装箱错误

  博主是一名unity 3d游戏开发者,游戏使用c#+lua开发,最近在优化c#代码时,发现了一处使用foreach不恰当的地方,其结果是造成了每帧近3k的gc alloc,如此高频率的gc堆内存分配,会导致垃圾回收的调用更加频繁,从而影响游戏性能,而这只需要简单的修改即可避免;

  ※使用.net 2.0的unity版本,如果是较新的.net 4.x版本,由于fcl实现修改,本文中48->56

   原始声明代码如下:

private readonly idictionary<int, myclass> mdic = new dictionary<int, myclass>();

  在每帧逻辑里面,会多次对其进行遍历,遍历代码如下:

foreach(var keyvaluepair in mdic)
{
    //do...
}

  通过unity自带的profiler分析,可以发现其导致的gc alloc:

C#效率优化(3)-- 使用foreach时避免装箱

body:

  通过上面的profiler可以发现此时foreach语句实际上调用的是dictionary定义中隐式实现的ienumerable<keyvaluepair<tkey, tvalue>>.getenumerator()方法,该方法的声明如下:

ienumerator<keyvaluepair<tkey, tvalue>> ienumerable<keyvaluepair<tkey, tvalue>>.getenumerator()
{
    return new enumerator(this, enumerator.keyvaluepair);
}

   其中,enumerator是在dictionary类中定义的嵌套结构类型:

C#效率优化(3)-- 使用foreach时避免装箱

  结构类型隐式转换为接口类型时会发生装箱,对于该enumerator类型,其装箱后大小为16(开销字节)+8(字段dictionary)+8(字段next+stamp)+16(字段current:8(int字节对齐)+8)=48字节,调用29次即产生48*29=1392字节的堆内存分配,这符合我们看到profiler里面看到的gc alloc;

  为了解决这个问题,只需要将变量声明时改为dictionary即可,不使用接口类型的变量,即:

private readonly dictionary<int, myclass> mdic = new dictionary<int, myclass>();

  此时,在对mdic进行foreach循环时,就会调用dictionary<tkey,tvalue>.getenumerator()方法,该方法返回值类型即结构类型的enumerator,避免了装箱操作:

C#效率优化(3)-- 使用foreach时避免装箱

one more thing:

  这里可能很多人有个误解,即foreach是只能对实现了ienumerable或ienumerable<t>的类型对象进行遍历,其实不然,foreach语句还可以对满足以下条件的任何类型的对象进行遍历:

  实现了可访问的getenumerator()方法,且该方法的返回值类型符合:包含可访问的current属性和bool movenext()方法;

conclusion:

  这样就知道了,.net框架类库提供的泛型集合类型都实现了这样的方法,因此可以放心对泛型集合进行foreach遍历,而不产生堆内存的分配,也因此,我们在使用这些类型时,尽量避免直接对其接口类型的变量进行遍历;

 


如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的认可是我写作的最大动力!

作者:minotauros
出处:

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。