David's profileprowyh's spaceBlogLists Tools Help

Blog


    July 21

    对象引用与托管指针(object references and managed pointers)

    C/C++中的指针是一种非常灵活而强大的引用机制,但同时也非常脆弱,稍有不慎,就会出错。
     
    Java完全摈弃了指针的概念,而代之以对象引用(object reference),基本上消灭了由指针而导致的错误。
     
    C#借鉴了Java的作法,引入了对象引用的概念,但同时,不像Java做的那么绝,仍然提供了指针的概念。
     
    CLR支持三种类型的指针:
     
    1) 托管指针(managed pointers)
    2) 非托管指针(unmanaged pointers)
    3) 非托管函数指针(unmanaged function pointers)
     
    托管指针是一种新类型的指针,指向托管堆中的内存区。
    非托管指针是传统的C/C++指针,指向非托管内存区。
    非托管函数指针也是传统的C/C++指针,指向函数地址。
     
    C#中的指针属于非托管指针,在C#中不能直接使用托管指针,但作为 by-ref 参数传递机制的 ref 和 out 机制就是利用托管指针实现的。
     
    C#代码:
     
    using System;
    public class Test
    {
    public static void Main()
    {
    int n = 20;
    int m;
     
    compute(n, out m);
    }
    private static void compute(int x, out int y)
    {
    y = x * 2;
    }
    }
     
    编译以后,生成如下 IL 代码(为了简单起见,这里只取compute()方法的 IL 代码):
     
    .method private hidebysig static void  compute(int32 x, [out] int32& y) cil managed
    {
      // Code size       7 (0x7)
      .maxstack  8
      IL_0000:  nop
      IL_0001:  ldarg.1
      IL_0002:  ldarg.0
      IL_0003:  ldc.i4.2
      IL_0004:  mul
      IL_0005:  stind.i4
      IL_0006:  ret
    } // end of method Test::compute
     
    从compute()的 IL 代码即可明显看出,参数 y 的类型即是托管指针 int32&。
     
    stind.i4 指令将栈顶的 32 位整数值存储到由次栈顶元素(即参数 y)所指示的内存地址中。
     
    上面的C#代码用的是 out 指示符,如果换成 ref 指示符,生成的 IL 代码是一样的,所不同的是,C#编译器会检查 m 的赋值情况,在执行 compute(n, ref m) 之前,m 必须被明确赋值(definite assigned),而对于 out 指示符,m 不需要赋值。
     
    托管指针和非托管指针的区别是明显的,托管指针指向的是托管堆中的地址,而非托管指针指向的是非托管内存中的地址。
     
    那么,对象引用与托管指针又有什么不同呢?
     
    从物理实现的角度看,对象引用也是一种指针,而且是一种托管类型的指针。与托管指针不同,对象引用只能指向对象的起始位置,而不能指向对象内部,也就是说,对象引用指向的是对象的整体,而非局部。
     
    而托管指针一般是指向对象内部某个成员的地址。除此之外,托管指针还可以指向求值栈(evaluation stack)中的位置,或静态变量,甚至非托管内存区。
     
    July 20

    To box or not to box (III)

    C#代码:
     
    using System;
    public class Test
    {
    public static void Main()
    {
    object o = 30;
    int k = (int)o;
    }
    }
     
    编译以后,生成如下的IL代码:
     
    .method public hidebysig static void  Main() cil managed
    {
      .entrypoint
      // Code size       17 (0x11)
      .maxstack  1
      .locals init (object V_0,
               int32 V_1)
      IL_0000:  nop
      IL_0001:  ldc.i4.s   30
      IL_0003:  box        [mscorlib]System.Int32
      IL_0008:  stloc.0
      IL_0009:  ldloc.0
      IL_000a:  unbox.any  [mscorlib]System.Int32
      IL_000f:  stloc.1
      IL_0010:  ret
    } // end of method Test::Main
     
    其中,局部变量 o 成为 V_0,局部变量 k 成为 V_1。
     
    ldc.i4.s 30 指令我们上文已经见过,将 30 推入栈中。
     
    box [mscorlib]System.Int32 指令将栈顶元素(30)进行装箱操作,并将对象引用推入栈中。
     
    stloc.0 指令将栈顶元素(30装箱后的对象引用)存入 V_0。
     
    ldloc.0 将 V_0 推入栈中。
     
    unbox 指令有两种形式:unbox valuetype,unbox.any typeTok
     
    unbox valuetype 的意思是将装箱的值类型转换为其原初形式(raw form)[1],实际上是返回指向对象中的值的指针(valueTypePtr),并推入栈中。
     
    unbox.any typeTok 指令抽取对象中的值(也就是被装箱的值),返回,并推入栈中。
     
    由此可见,指令 unbox 和 unbox.any 是不同的:unbox 只是返回指向值的指针,而 unbox.any 则返回实际的值。
     
    unbox.any [mscorlib]System.Int32 指令从栈顶元素(对象引用,即 o)所引用的对象中抽取装箱的值(即30),并将其推入栈顶。
     
    stloc.1 指令将栈顶元素存入 V_1(即 k)。
     
    由此可见,相比装箱操作(boxing)而言,拆箱操作(unboxing)只是取对象中的地址或抽取对象中的值,并不算太费时。
     
    [1] 值类型有两种表示:
     
    1) 原初形式(raw form) ,当某个值类型嵌入其它对象中时,即以原初形式表示;
    2) 装箱形式(boxed form),当某个值类型的数值(data)被装箱到一个对象中时,即以装箱形式表示,此时可以作为一个独立的实体(an independent entity)而存在。
     
    July 19

    To box or not to box (II)

    C#代码:
     
    using System;
    public class Test
    {
    public static void Main()
    {
    int n = 30;
    Console.Write("n: {0}", n);
    }
    }
     
    编译以后,将生成如下的IL代码:
     
    .method public hidebysig static void  Main() cil managed
    {
      .entrypoint
      // Code size       22 (0x16)
      .maxstack  2
      .locals init (int32 V_0)
      IL_0000:  nop
      IL_0001:  ldc.i4.s   30
      IL_0003:  stloc.0
      IL_0004:  ldstr      "n: {0}"
      IL_0009:  ldloc.0
      IL_000a:  box        [mscorlib]System.Int32
      IL_000f:  call       void [mscorlib]System.Console::WriteLine(string, object)
      IL_0014:  nop
      IL_0015:  ret
    } // end of method Test::Main
     
    ldc.i4.s num 的意思是 push num onto the stack as int32, short form. 所以 ldc.i4.s 30 的意思是将 30 作为 32 位整数推入栈顶。
    stloc.0 的意思是 pop a value from stack into local variable 0.
     
    所以 ldc.i4.s 30 和 stloc.0 两条指令执行的结果是完成C#中的赋值(变量定义)语句:
     
    int n = 30;
     
    这里,局部变量 n 变成了 V_0。
     
    ldstr "n: {0}" 指令的意思是将字符串 "n: {0}" 推入栈顶。
     
    ldloc.0 的意思是 load local variable 0 onto stack,即将 V_0 的值推入栈顶。(此时由ldstr推入的字符串成为次栈顶元素。)
     
    box typeTok 的意思是 convert a boxable value to its boxed form,即将值转换为装箱形式,即引用类型。
    box [mscorlib]System.Int32 即是将栈顶的 32 位整数值转换为装箱后的形式,然后返回对象引用,并将此对象引用推入栈顶。
     
    call void [mscorlib]System.Console::WriteLine(string, object) 即是以栈顶元素(装箱后的对象引用)和次栈顶元素(由ldstr推入的字符串)为参数,调用WriteLine方法。
     
    这段代码唯一不好理解的是由 box 完成的装箱操作。
     
    简单说来,box 操作分为三步:
     
    1) 创建一个无名的新对象,类似于 new Object();
    2) 将要装箱的值(存在于栈顶)从栈顶弹出,拷贝到新创建的对象中;
    3) 返回该对象引用,并推入栈顶。
     
    如果转换为C的语义,可以这样理解:
     
    1) 调用malloc()分配适当大小的内存区;
    2) 将值从栈中弹出,拷贝到该内存区;
    3) 返回该内存区的地址,并推入栈顶。
     
    当然,这只是一个有助于理解的类比,实际的实现并不一定与此相同。
     
    由 box 指令的操作可以看出,存在两个比较费时的操作,即1) 创建新对象,以及2) 值的拷贝。
     
    由此可见,虽然C#通过 boxing 机制模糊了引用类型和值类型的边界,使得程序员可以写出一致的代码,但 box 操作不可避免地存在着性能损失。所以,应该尽可能地避免 box 操作。
     
    July 15

    To box or not to box

    给定:
     
    int n = 30;
     
    下面的两条C#语句有什么区别吗?
     
    1. System.Console.WriteLine("n: {0}", n);
    2. System.Console.WriteLine("n: {0}", n.ToString());
     
    当然,这两条语句产生的结果是没有任何区别的:
    n: 30
     
    既然如此,还有什么可研究的吗?先别急着下结论,研究研究再说。
     
    首先,我们来看看WriteLine()的语法格式:
     
    public static void WriteLine(string format, Object arg0)
     
    从此语法格式可知,WriteLine()的第二个参数要求是Object类型,上述的语句2是满足的,因为n.ToString()的结果是string类型,而string类型就是Object类型;而语句1是不满足的,因为n是值类型(value type),而WriteLine()又没有形如WriteLine(string format, int arg0)的语法,既然如此,那 n 又是如何转换为 Object 类型的呢?(否则编译就会出错:类型不匹配!)
     
    C#区分了引用类型(reference type)和值类型(value type)。
     
    引用类型涉及到两个实体(当然,对程序员是透明的):对象和对象引用,对象存在于堆(heap)中,而对象引用存在于栈(stack)中。对于一个对象,可以存在若干个对象引用指向该对象;当然也存在不指向任何对象的对象引用,该对象引用的值即为null;对于不存在任何对象引用的对象,即成为GC要回收的对象。
     
    而对于值类型,只存在一个实体,即值本身,该实体存在于栈中。
     
    由此可见,引用类型和值类型有着很大的区别。这种区别有时候是必要的,如:
     
    for (int i = 0; i < 10; i++)
    {
    // loop statements
    }
     
    这里的循环控制变量 i 作为值类型,就非常自然而高效。如果 i 也作为引用类型,将大大降低 for 语句的效率。但有的时候却是不必要的,如上述的WriteLine()语句,如果引用类型和值类型不能互相转换,则必须要有类似如下的WriteLine语法:
     
    public static void WriteLine(string format, int arg0)
     
    而且WriteLine对于所有的值类型都要有类似的语法,这将使得 FCL 非常庞大而笨拙!
     
    C#语言通过 box/unbox 设施(机制)比较好地解决了这个问题。使得下述声明
     
    public static void WriteLine(string format, Object arg0)
     
    适用于所有的数据类型。所以,我们可以写出像
     
    WriteLine("n: {0}", n);
     
    这样的语句。这里的 n 通过 box 机制自动转换为 Object 类型。通过检查编译器生成的中间代码可以更清楚地看出这一点:
     
      .locals init (int32 V_0)
      IL_0000:  nop
      IL_0001:  ldc.i4.s   30
      IL_0003:  stloc.0
      IL_0004:  ldstr      "n: {0}"
      IL_0009:  ldloc.0
      IL_000a:  box      [mscorlib]System.Int32
      IL_000f:   call      void [mscorlib]System.Console::WriteLine(string, object)
     
    从这段 IL 代码中可以看出,n 通过 box 指令转换为了引用类型,然后再调用WriteLine(string, object)方法。
     
    从这个例子可以看出,C#中的 box/unbox 机制,就像 using 语句(using statement,非using directive)一样,是一种便用设施(handy facility)。这种便用设施使得程序更加优雅,正因为此,Java语言从5.0开始也引入了 box/unbox 机制。