都说不要C#装箱,那装箱到底带来了什么开销?

相信很有朋友在面试时大多会被问到 装箱 的问题,也是一个经典的问题,可深可浅,那今天我们就从 汇编 和 内存 角度进行统一解读下。

为了方便演示,先上一段装箱的代码。.

    class Program
    {
        static void Main(string[] args)
        {
            var i = 10;

            var o = (object)i;

            Console.ReadLine();
        }
    }

接下来用 windbg 看一下它的汇编代码。

0:000> !U /d 022e089a
Normal JIT generated code
ConsoleApp1.Program.Main(System.String[])
Begin 022e0848, size 5b

D:\net5\ConsoleApp1\ConsoleApp1\Program.cs @ 15:
022e0872 c745f80a000000  mov     dword ptr [ebp-8],0Ah

D:\net5\ConsoleApp1\ConsoleApp1\Program.cs @ 17:
022e0879 b9a8429e62      mov     ecx,offset mscorlib_ni!System.Text.Encoding.GetEncodingCodePage(Int32)$##6006719 <PERF> (mscorlib_ni+0x142a8) (629e42a8)           // 获取编码类型
022e087e e845282ffe      call    005d30c8 (JitHelp: CORINFO_HELP_NEWSFAST)  //生成一个初始化类型放在 eax 中。(objheader+methodtable+占位符)
022e0883 8945f0          mov     dword ptr [ebp-10h],eax                    //备份地址到 栈中
022e0886 8b45f0          mov     eax,dword ptr [ebp-10h]                    //恢复 eax 值
022e0889 8b55f8          mov     edx,dword ptr [ebp-8]                      //将 0A 赋给 edx 上
022e088c 895004          mov     dword ptr [eax+4],edx                      //将 edx 赋给 this.x 位置
022e088f 8b45f0          mov     eax,dword ptr [ebp-10h]                    //提取栈值到 eax 值
022e0892 8945f4          mov     dword ptr [ebp-0Ch],eax                    //将eax赋值给变量 o

因为每句汇编代码都有注释,我就不解释了,这里主要看一下 CORINFO_HELP_NEWSFAST方法,它是干什么的呢?这得从源码说起:

    /* Allocating a new object. Always use ICorClassInfo::getNewHelper() to decide 
       which is the right helper to use to allocate an object of a given type. */

    CORINFO_HELP_NEW_CROSSCONTEXT,  // cross context new object
    CORINFO_HELP_NEWFAST,
    CORINFO_HELP_NEWSFAST,          // allocator for small, non-finalizer, non-array object
    CORINFO_HELP_NEWSFAST_FINALIZE, // allocator for small, finalizable, non-array object
    CORINFO_HELP_NEWSFAST_ALIGN8,   // allocator for small, non-finalizer, non-array object, 8 byte aligned
    CORINFO_HELP_NEWSFAST_ALIGN8_VC,// allocator for small, value class, 8 byte aligned
    CORINFO_HELP_NEWSFAST_ALIGN8_FINALIZE, // allocator for small, finalizable, non-array object, 8 byte aligned
    CORINFO_HELP_NEW_MDARR,         // multi-dim array helper (with or without lower bounds - dimensions passed in as vararg)
    CORINFO_HELP_NEW_MDARR_NONVARARG,// multi-dim array helper (with or without lower bounds - dimensions passed in as unmanaged array)
    CORINFO_HELP_NEWARR_1_DIRECT,   // helper for any one dimensional array creation
    CORINFO_HELP_NEWARR_1_R2R_DIRECT, // wrapper for R2R direct call, which extracts method table from ArrayTypeDesc
    CORINFO_HELP_NEWARR_1_OBJ,      // optimized 1-D object arrays
    CORINFO_HELP_NEWARR_1_VC,       // optimized 1-D value class arrays
    CORINFO_HELP_NEWARR_1_ALIGN8,   // like VC, but aligns the array start

    CORINFO_HELP_STRCNS,            // create a new string literal
    CORINFO_HELP_STRCNS_CURRENT_MODULE, // create a new string literal from the current module (used by NGen code)

可以看到,CORINFO_HELP_NEWSFAST 是用于分配 小对象,无终结器,非数组 的专用方法,也属于高效的 快速分配路径,那分配完之后的初始化长什么样子呢?这就需要用 windbg 下断点调试,从汇编代码看,最后的结果会存放在 eax 上, 如下图所示:

都说不要C#装箱,那装箱到底带来了什么开销?最后将栈上的10复制到堆上区域。

可以看到,这里涉及到了如下几个性能开销。

  1. 内存分配

风险在于分配引发的gc回收概率,比如判代回收 (临时代,FullGC)。

  1. 多次内存复制 (stack -> heap -> register)

一个装箱就有 6 个mov,反复的在 ,,寄存器 之间交换。

  1. 增加 gc 回收压力

gc本来工作压力就很大,这又有无谓的分配,难哈。

最后就是如何解决,大概有如下两点。尽可能避免装箱 或者合理的使用 泛型