惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

罗磊的独立博客
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园_首页
博客园 - 叶小钗
酷 壳 – CoolShell
酷 壳 – CoolShell
Apple Machine Learning Research
Apple Machine Learning Research
云风的 BLOG
云风的 BLOG
量子位
博客园 - 三生石上(FineUI控件)
Stack Overflow Blog
Stack Overflow Blog
小众软件
小众软件
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
V2EX
人人都是产品经理
人人都是产品经理
V
Visual Studio Blog
Jina AI
Jina AI
L
LangChain Blog
M
MIT News - Artificial intelligence
MongoDB | Blog
MongoDB | Blog
Last Week in AI
Last Week in AI
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学

博客园 - 太阳天子

汽车导航仪哪个好 JavaScript总结 A Programmers Overview Of C# 结构和类 .net中清除EXCEL进程最有效的方法 ADO.net 中数据库连接方式 浅析.Net下的多线程编程 .net如何实现页面间的参数传递 .NET组件和COM组件之间的相互操作 asp.net中常用的一些小技巧 Microsoft SQL Server 2005 安装程序 错误解决 .net中的正则表达式使用高级技巧 (四)(from internet) net中的正则表达式使用高级技巧 (三)(from internet) .net中的正则表达式使用高级技巧 (二)(from internet) .net中的正则表达式使用高级技巧 (一)(from thinhunan's blog) ASP.NET 中的正则表达式(microsoft) 正则表达式语法(from internet) 正则表达式的基本语法 探索设计模式系列文章
C#装箱拆箱和性能
太阳天子 · 2006-05-15 · via 博客园 - 太阳天子
C#装箱拆箱和性能 - My Soul For You[5:34]

    有收获就写下来。

     首先,说一下装箱和拆箱。

      在.net中的通用类型系统(Common Type system,CTS)中,所有类型都是对象(object),都派生自System.Object。CTS支持两组类型:值类型和引用类型。如果变量是值类型那么这个变量就包含实际的数据。也就是在内存中确实会分配那么一部分空间给这个变量并存储值,引用类型就类似一个类型安全的指针,本身并没有开辟内存空间去存储东西。这玩意是基础,罗嗦的重复一下。

     而装箱(box)就是将值类型转换为引用类型的过程。相反的过程就叫拆箱(unbox)。

     a、装箱

     一个很简单的例子。新建一个控制台程序,在Main()里面就写两句话。   

     int i = 13;
     object ob = i;

     编译。然后用.net 提供的工具ILDASM.exe(MSIL Disassembler )查看新生产这个程序的配件代码(Microsoft intermediate language ,MSIL。顺带说一句.net framework SDK除了这个MSIL的反汇编工具,当然还提供了汇编工具ILASM.exe,可以使用MSIL编写程序,当然。。谁也不会没事这么干。那个反汇编工具倒是挺有用,可以了解一些底层机制)

     用那个工具查看一下编译后程序的Main(string[] args)方法,显示如下(我现在用的时.net framework 2.0可能MSIL代码显示出来的和原来的1.0或者1.1稍有不同,不过没关系核心没变):

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       12 (0xc)
  .maxstack  1
  .locals init ([0] int32 i,
           [1] object ob)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   13
  IL_0003:  stloc.0
  IL_0004:  ldloc.0
  IL_0005:  box        [mscorlib]System.Int32
  IL_000a:  stloc.1
  IL_000b:  ret
} // end of method Program::Main

稍微解释一下:

 (1)先注意  .locals ,定义了两个类型分别为int32 和object 的局部变量

 (2)然后看   IL_0001处,ldc是个指令,后面的i4.s指出作为32位(4个字节)整数被压入堆栈。而压入的值就是13  

 (3)下面的stloc把上面的值从堆栈弹出给局部变量i,这里的.0是指弹出给到第一个局部变量中,也就是i了

 (4)这个值(13),被弹出后,就被装载回堆栈,也就是后面IL_0004行的ldloc命令做的事情

 (5)然后使用CIL(Common Language Infrastructure )box将这个值转换为引用类型。装箱喽~

 (6)stloc.1根据(3)的解释就好理解了,就是把box返回值弹出给第二个局部变量ob中。

  但是这个box指令内部又发生了什么呢?有牛人告诉了我们。

  (1)在堆上分配内存。因为值类型最终有一个对象代表,所有堆上分配的内存量必须是值类型的大小加上容纳此对象及其内部结构(比如虚拟方法表)所需的内存量。

  (2)值类型的值被复制到新近分配的内存中

  (3)新近分配的对象地址被放到堆栈上,现在它指向一个引用类型

   b、拆箱

   在刚才程序的基础上,再加一句话变成,编译:

            int i = 13;
            object ob = i;
            int j = (int)ob;

   在装箱的时候,并不需要显示类型转换。但在拆箱时需要类型转换。这是因为在拆箱时对象可以被转换为任何类型。看看MSIL代码变成这德行了:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       19 (0x13)
  .maxstack  1
  .locals init ([0] int32 i,
           [1] object ob,
           [2] int32 j)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   13
  IL_0003:  stloc.0
  IL_0004:  ldloc.0
  IL_0005:  box        [mscorlib]System.Int32
  IL_000a:  stloc.1
  IL_000b:  ldloc.1
  IL_000c:  unbox.any  [mscorlib]System.Int32
  IL_0011:  stloc.2
  IL_0012:  ret
} // end of method Program::Main

    整个流程就不再重复叙述了,参照前面的解释现在这个过程应该能看明白。

    说说拆箱unbox的内部过程:

   (1)因为一个对象将被转换,所以编译器必须先判断堆栈上指向合法对象的地址,以及这个对象类型是否可以转换为MSL unbox指令调用中指定的值类型。如果检查失败就抛出InvalidCastException异常。

   (2)校验通过后,就返回指向对象内的值的指针。可以看出,装箱操作会创建转换类型的副本,而拆箱就不会。不过注意一下,在我们装箱的时候是先把变量i的值复制了一份赋给ob的,所变量j拿到的是ob这个变量的引用。也就是后面再改变i的值并不会影响j的值,但是改变ob的值就会。

   c、再来一个稍微复杂点的例子,有如下代码:

            int i = 13;
            object ob = i;
            Console.WriteLine(i + "," + (Int32)ob);

    这里做了几次装箱和拆箱操作呢?我开始想当然的以为是1次装1次拆箱操作了,可实际上确是3次装箱1次拆箱操作!先看看MSIL代码:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       45 (0x2d)
  .maxstack  3
  .locals init ([0] int32 i,
           [1] object ob)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   13
  IL_0003:  stloc.0
  IL_0004:  ldloc.0
  IL_0005:  box        [mscorlib]System.Int32
  IL_000a:  stloc.1
  IL_000b:  ldloc.0
  IL_000c:  box        [mscorlib]System.Int32
  IL_0011:  ldstr      ","
  IL_0016:  ldloc.1
  IL_0017:  unbox.any  [mscorlib]System.Int32
  IL_001c:  box        [mscorlib]System.Int32
  IL_0021:  call       string [mscorlib]System.String::Concat(object,
                                                              object,
                                                              object)

  IL_0026:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_002b:  nop
  IL_002c:  ret
} // end of method Program::Main

    (1)前面好说,跟前面一样  object ob = i;引起了一次装箱操作也就是 IL_0005处代码。

    (2)后面可以看出Console.WriteLine方法调用的是单个String作为参数的版本。因此上面调用了String.Concat方法将i + "," + (Int32)ob这3个值连接产生单个String再传给WriteLine。

    (3)String.Concat的重载版本里面找到最匹配的就是Concat(object, object,object)。这样为了匹配这3个参数:

        (3.1) IL_000c处代码,第一个参数i被装箱 

        (3.2)IL_0011 处ldstr      "," 就是将字符串','压入堆栈

        (3.3)然后 IL_0017 (int32)ob引起了一次拆箱操作

        (3.4)我们可怜的(int32)ob,又为了匹配Concat的参数,再次被装箱(IL_001c)

   明显后面那个(int32)ob造成了一次不必要的拆箱和装箱操作!所以正因为.net的自动类型处理能力,还是小心地注意一下写法,否则就会引起不必有的性能损失。

   下面举类似的小例子

    还是个那个控制台代码写成这样

        static ArrayList al;
        static void Main(string[] args)
        {
            int i = 13;
            al = new ArrayList();
            al.Add(i);

            Console.WriteLine("{0}", i);
           
        } 

  MSIL命令如下:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       49 (0x31)
  .maxstack  2
  .locals init ([0] int32 i)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   13
  IL_0003:  stloc.0
  IL_0004:  newobj     instance void [mscorlib]System.Collections.ArrayList::.ctor()
  IL_0009:  stsfld     class [mscorlib]System.Collections.ArrayList ConsoleApplication1.Program::al
  IL_000e:  ldsfld     class [mscorlib]System.Collections.ArrayList ConsoleApplication1.Program::al
  IL_0013:  ldloc.0
  IL_0014:  box        [mscorlib]System.Int32
  IL_0019:  callvirt   instance int32 [mscorlib]System.Collections.ArrayList::Add(object)
  IL_001e:  pop
  IL_001f:  ldstr      "{0}"
  IL_0024:  ldloc.0
  IL_0025:  box        [mscorlib]System.Int32
  IL_002a:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
  IL_002f:  nop
  IL_0030:  ret
} // end of method Program::Main

其他的都不用管,看懂了前面我说的,那么这里就知道因为ArrayList.Add(object)做了一次装箱和Console.WriteLine(string,object)又做了一次装箱。如果我们换一种写法,把程序改成这样:

        static ArrayList al;
        static void Main(string[] args)
        {
            int i = 13;
            object ob = i;
            al = new ArrayList();
            al.Add(ob);

            Console.WriteLine("{0}", ob);
           
        }

MSIL就变成:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       46 (0x2e)
  .maxstack  2
  .locals init ([0] int32 i,
           [1] object ob)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   13
  IL_0003:  stloc.0
  IL_0004:  ldloc.0
  IL_0005:  box        [mscorlib]System.Int32
  IL_000a:  stloc.1
  IL_000b:  newobj     instance void [mscorlib]System.Collections.ArrayList::.ctor()
  IL_0010:  stsfld     class [mscorlib]System.Collections.ArrayList ConsoleApplication1.Program::al
  IL_0015:  ldsfld     class [mscorlib]System.Collections.ArrayList ConsoleApplication1.Program::al
  IL_001a:  ldloc.1
  IL_001b:  callvirt   instance int32 [mscorlib]System.Collections.ArrayList::Add(object)
  IL_0020:  pop
  IL_0021:  ldstr      "{0}"
  IL_0026:  ldloc.1
  IL_0027:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
  IL_002c:  nop
  IL_002d:  ret
} // end of method Program::Main

   怎么样?就只有一次我明确指出的装箱操作了。