您的位置:首页 > 其它

[转]关于struct的一些解释与class对比

2011-10-29 12:39 190 查看
  近日看到的一个文章,搜索的来,不知哪位前辈,感谢了~

  有关构造函数,调用这两个方面的内容

  最近才知道struct和class的静态构造函数的触发规则是不同的,不像class在第一次使用类的时候触发静态构造函数。如果只访问struct实例的字段是不会触发静态构造函数调用的。通过测试发现当访问静态字段,struct本身的函数(静态和实例)和带参数的构造函数就会引起静态构造函数的执行。而调用默认构造和未覆写的基类虚函数是不会的。为什么呢?

  让我们先来看看class和struct在调用构造函数时的区别。class使用newobj指令而struct使用initobj指令来构造对象。newobj在堆上申请一块内存并调用相应的构造函数进行初始化,然后将对象地址返回给计算栈。initobj则是从本地变量表中载入已经分配出来的struct实例然后初始化struct的各字段。这个初始化过程是CLR内部执行的,而不像class编译器会给class添加一个默认构造函数(这就是为什么struct不能给字段添加默认值的原因。但在类中如果给字段添加了默认值编译器就会自动在构造函数中添加字段赋值操作)。如果给struct中定义了一个有参数的构造函数,那么系统就不会使用initobj指令,而是直接用call指令调用带参数的构造函数。

  我们最常见最常用的调用函数的指令是call和callvirt。对于静态函数使用call指令,对于class使用callvirt指令(不论class中的函数是不是虚的)。只有子类调用父类的函数的时候(避免递归调用)以及构造函数中(由编译器添加保证父类字段被初始化)使用call指令。而对于struct我们发现只要调用的函数是struct本身定义的都是使用call指令。call和callvirt指令的差别在于,call会把调用的函数当作静态函数看待,而不会关心调用当前函数时实例指针(this)是否为空。这就是struct调用函数时为什么都是call因为struct实例是不可能被置为null的。实际上class在调用非虚函数时实际上也是使用call的只是多做了一步验证——this是否为空,让我们来验证一下。

usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Text;
namespaceCLR{
classClass_Test{
publicvoidTest1(){Console.WriteLine("Test1");}
publicvirtualvoidTest2(){Console.WriteLine("Test2");}
publicstaticvoidTest3(){Console.WriteLine("StaticMethod");}
publicoverridestringToString(){
returnbase.ToString();
}
}
classProgram{
staticvoidMain(string[]args){
Class_Testc=newClass_Test();
c.Test1();
c.Test2();
Class_Test.Test3();
stringstr=c.ToString();
Console.ReadLine();
}
}
}

.csharpcode,.csharpcodepre
{
font-size:small;
color:black;
font-family:consolas,"CourierNew",courier,monospace;
background-color:#ffffff;
/*white-space:pre;*/
}
.csharpcodepre{margin:0em;}
.csharpcode.rem{color:#008000;}
.csharpcode.kwrd{color:#0000ff;}
.csharpcode.str{color:#006080;}
.csharpcode.op{color:#0000c0;}
.csharpcode.preproc{color:#cc6633;}
.csharpcode.asp{background-color:#ffff00;}
.csharpcode.html{color:#800000;}
.csharpcode.attr{color:#ff0000;}
.csharpcode.alt
{
background-color:#f4f4f4;
width:100%;
margin:0em;
}
.csharpcode.lnum{color:#606060;}

对应的汇编如下:

c.Test1();//实例非虚函数
0000006bmovecx,esi//将this放到ecx中,ecx在.net函数调用规则中保存第一个参数
0000006dcmpdwordptr[ecx],ecx//验证this是否为空,空指针的话dwordptr[ecx]就会报错
0000006fcallFFEEC130//调用函数
00000074nop
c.Test2();//实例虚函数
00000075movecx,esi
00000077moveax,dwordptr[ecx]//得到方法表地址,引用类型在堆上开始4个字节是方法表地址
00000079calldwordptr[eax+38h]//因为是虚函数每次调用的时候都要计算要调用的函数地址
0000007cnop
Class_Test.Test3();//静态函数
00000083callFFEEC140//调用函数
00000088nop
publicoverridestringToString()//子类调用父类函数
{
//省略前面的汇编
returnbase.ToString();//如果使用callvirt就会死循环
00000026movecx,edi//从ecx中得到this
00000028call77A00F68//调用函数
0000002dmovesi,eax//.net函数调用规则中eax保存返回值
0000002fmovebx,esi
00000031nop
00000032jmp00000034
}

.csharpcode,.csharpcodepre
{
font-size:small;
color:black;
font-family:consolas,"CourierNew",courier,monospace;
background-color:#ffffff;
/*white-space:pre;*/
}
.csharpcodepre{margin:0em;}
.csharpcode.rem{color:#008000;}
.csharpcode.kwrd{color:#0000ff;}
.csharpcode.str{color:#006080;}
.csharpcode.op{color:#0000c0;}
.csharpcode.preproc{color:#cc6633;}
.csharpcode.asp{background-color:#ffff00;}
.csharpcode.html{color:#800000;}
.csharpcode.attr{color:#ff0000;}
.csharpcode.alt
{
background-color:#f4f4f4;
width:100%;
margin:0em;
}
.csharpcode.lnum{color:#606060;}

通过上边的汇编我们可以看出class调用非虚函数时本质上使用了call指令,而调用父类函数时就是直接使用call,并且因为在实例函数中所以不需要验证this是否为空。这里说点题外话,在IL中我们经常会看到执行函数时将本地变量加载到计算栈中或者将计算栈中的结果保存到本地变量中这不是很慢的操作吗?实际上在大多数情况下是通过esi,edi这些寄存器来当缓存的,如果局部变量比较多才会保存到相应的栈上。从这里我们又印证了事实,.net的线程栈在每次执行函数时所创建的栈帧包含参数表,本地变量表,返回地址和计算栈。
  继续说call指令的问题,我前面说了struct本身定义的都是使用call指令调用的如果你亲自动手实验的就会发现我说不对。如果struct覆写了基类的函数(GetHashCode,ToString)在调用是IL会使用callvirt来调用,我真的错了吗?

usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Text;

namespaceCLR{
structStruct_Test{
bool_a;
int_b;
int_c;

publicStruct_Test(boola,intc,intb){
this._a=a;
this._b=b;
this._c=c;
}
publicvoidTest(){}
publicoverridestringToString(){
returnstring.Format("{0},{1},{2}",this._a,this._b,this._c);
}
}
classProgram{
staticvoidMain(string[]args){
Struct_Tests=newStruct_Test(true,15,20);
stringstr=s.ToString();
Console.ReadLine();
}
}
}


.csharpcode,.csharpcodepre
{
font-size:small;
color:black;
font-family:consolas,"CourierNew",courier,monospace;
background-color:#ffffff;
/*white-space:pre;*/
}
.csharpcodepre{margin:0em;}
.csharpcode.rem{color:#008000;}
.csharpcode.kwrd{color:#0000ff;}
.csharpcode.str{color:#006080;}
.csharpcode.op{color:#0000c0;}
.csharpcode.preproc{color:#cc6633;}
.csharpcode.asp{background-color:#ffff00;}
.csharpcode.html{color:#800000;}
.csharpcode.attr{color:#ff0000;}
.csharpcode.alt
{
background-color:#f4f4f4;
width:100%;
margin:0em;
}
.csharpcode.lnum{color:#606060;}

对应的IL代码

IL_0001:ldloca.ss
IL_0003:ldc.i4.1
IL_0004:ldc.i4.s15
IL_0006:ldc.i4.s20
IL_0008:callinstancevoidTest_Console.Struct_Test::.ctor(bool,int32,int32)
IL_000d:nop
IL_000e:ldloca.ss
IL_0010:constrained.Test_Console.Struct_Test
IL_0016:callvirtinstancestring[mscorlib]System.Object::ToString()
IL_001b:stloc.1

.csharpcode,.csharpcodepre
{
font-size:small;
color:black;
font-family:consolas,"CourierNew",courier,monospace;
background-color:#ffffff;
/*white-space:pre;*/
}
.csharpcodepre{margin:0em;}
.csharpcode.rem{color:#008000;}
.csharpcode.kwrd{color:#0000ff;}
.csharpcode.str{color:#006080;}
.csharpcode.op{color:#0000c0;}
.csharpcode.preproc{color:#cc6633;}
.csharpcode.asp{background-color:#ffff00;}
.csharpcode.html{color:#800000;}
.csharpcode.attr{color:#ff0000;}
.csharpcode.alt
{
background-color:#f4f4f4;
width:100%;
margin:0em;
}
.csharpcode.lnum{color:#606060;}

  如果你仔细观察会发现在callvirt调用的上面有这么一条指令constrained。让我们看看msdn里让人头晕的解释:

如果callvirtmethod指令前面带有前缀constrainedthisType,该指令将按照以下步骤执行:
如果thisType为引用类型(相对于值类型),则ptr被取消引用,并作为“this”指针传递到method的callvirt。
如果thisType为值类型,而且thisType实现method,则ptr作为“this”指针在不作任何修改的状态下传递到callmethod指令,以便thisType实现method。
如果thisType为值类型,而且thisType不实现method,则将取消对ptr的引用,对它进行装箱,然后将它作为“this”指针传递到callvirtmethod指令。
  说白了就是:如果值类型在调用一个虚函数时如果改虚函数是该值类型实现的那么就以call形式调用,如果没有实现就以callvirt形式调用,并且要对值类型装箱。关于constrained更详细的分析请看这里。下面使用简易的方法来验证这个结论:

Struct_Tests=newStruct_Test(true,15,20);
Console.WriteLine(GC.GetTotalMemory(false));
inthash=0;
for(inti=0;i<10000000;++i){
hash=s.GetHashCode();
}
Console.WriteLine(GC.GetTotalMemory(false));
Console.WriteLine(GC.CollectionCount(0));

.csharpcode,.csharpcodepre
{
font-size:small;
color:black;
font-family:consolas,"CourierNew",courier,monospace;
background-color:#ffffff;
/*white-space:pre;*/
}
.csharpcodepre{margin:0em;}
.csharpcode.rem{color:#008000;}
.csharpcode.kwrd{color:#0000ff;}
.csharpcode.str{color:#006080;}
.csharpcode.op{color:#0000c0;}
.csharpcode.preproc{color:#cc6633;}
.csharpcode.asp{background-color:#ffff00;}
.csharpcode.html{color:#800000;}
.csharpcode.attr{color:#ff0000;}
.csharpcode.alt
{
background-color:#f4f4f4;
width:100%;
margin:0em;
}
.csharpcode.lnum{color:#606060;}

运行结果为:141200 399104 127
  从上面的结果可以看到如果没有覆写虚函数确实引起了装箱。让我在对比一下与调用ToString()时的不同,s.ToString()请看反汇编;
s.ToString();
0000003dleaecx,[ebp-44h]
00000040callFFE4C0B0
00000045nop
s.GetHashCode();
00000046movecx,7C3810h//Struct_Test方法表地址
0000004bcallFFE31FAC//在堆上分配空间
00000050movebx,eax
00000052leaedi,[ebx+4]
00000055cmpecx,dwordptr[edi]
00000057leaesi,[ebp-44h]//将栈上数据拷贝到堆上
0000005amovqxmm0,mmwordptr[esi]
0000005emovqmmwordptr[edi],xmm0
00000062addesi,8
00000065addedi,8
00000068movsdwordptres:[edi],dwordptr[esi]
00000069movecx,ebx
0000006bmoveax,dwordptr[ecx]//虚函数调用
0000006dcalldwordptr[eax+30h]

.csharpcode,.csharpcodepre
{
font-size:small;
color:black;
font-family:consolas,"CourierNew",courier,monospace;
background-color:#ffffff;
/*white-space:pre;*/
}
.csharpcodepre{margin:0em;}
.csharpcode.rem{color:#008000;}
.csharpcode.kwrd{color:#0000ff;}
.csharpcode.str{color:#006080;}
.csharpcode.op{color:#0000c0;}
.csharpcode.preproc{color:#cc6633;}
.csharpcode.asp{background-color:#ffff00;}
.csharpcode.html{color:#800000;}
.csharpcode.attr{color:#ff0000;}
.csharpcode.alt
{
background-color:#f4f4f4;
width:100%;
margin:0em;
}
.csharpcode.lnum{color:#606060;}

  所以我们使用struct要小心不要因为忘记了覆写虚函数而造成不必要的性能损失。而且在这里因为没有调用Struct_Test本身的函数所以不会触发静态构造的执行。最后说一下struct在调用函数的时候首先要得到this指针,比如IL_000e:ldloca.ss。大家注意看这里不是ldloc所以对于Struct_Test的函数调用来说第一个参数是refStruct_Test,感觉ref的这个参数修饰用在这里才是最能体现价值的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
章节导航