副标题#e#
最近才知道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是否为空,让我们来验证一下。
class Class_Test
{
public void Test1() {}
public virtual void Test2() {}
public static void Test3() {}
public override string ToString()
{
return base.ToString();
}
}
Class_Test c = new Class_Test ();
c.Test1();
c.Test2();
Class_Test.Test3();
string str = c.ToString();
#p#副标题#e#
对应的汇编如下:
c.Test1(); //实例非虚函数
0000006b mov ecx,esi //将this放到ecx中,ecx在.net函数 挪用法则中生存第一个参数
0000006d cmp dword ptr [ecx],ecx //验证this是否为 空,空指针的话dword ptr [ecx]就会报错
0000006f call FFEEC130 //挪用函数
00000074 nop
c.Test2(); //实例虚函数
00000075 mov ecx,esi
00000077 mov eax,dword ptr [ecx] //获得要领表地点,引用范例在堆上的开始4个字节是要领表地点
00000079 call dword ptr [eax+38h] //因为是虚函数每次挪用的时候都要计较要调 用的函数地点
0000007c nop
Class_Test.Test3(); //静态函数
00000083 call FFEEC140 //挪用函数
00000088 nop
public override string ToString() //子类挪用父类函数
{
//省略前面的汇编
return base.ToString(); //假如利用callvirt就会 死轮回
00000026 mov ecx,edi //从ecx中获得 this
00000028 call 77A00F68 //挪用函数
0000002d mov esi,eax //.net函数挪用法则中eax生存返回值
0000002f mov ebx,esi
00000031 nop
00000032 jmp 00000034
}
通过上边的汇编我们可以看出class挪用非虚函数时本质上利用了call指令,而挪用父类函数时就是直 接利用call,而且因为在实例函数中所以不需要验证this是否为空。这里说点题外话,在IL中我们常常会 看到执行函数时将当地变量加载到计较栈中可能将计较栈中的功效生存到当地变量中这不是很慢的操纵吗 ?实际上在大大都环境下是通过esi,edi这些寄存器来当缓存的,假如局部变量较量多才会生存到相应的 栈上。从这里我们又印证了事实,.net的线程栈在每次执行函数时所建设的栈帧包括参数表,当地变量表 ,返回地点和计较栈。
#p#分页标题#e#
继承说call指令的问题,我前面说了struct自己界说的都是利用call指令挪用的假如你亲自动手尝试 的就会发明我说差池。假如struct覆写了基类的函数(GetHashCode,ToString)在挪用是IL会利用callvirt 来挪用,我真的错了吗?
struct Struct_Test
{
bool _a ;
int _b;
int _c;
public Struct_Test(bool a, int c, int b)
{
this._a = a;
this._b = b;
this._c = c;
}
public void Test() {}
public override string ToString()
{
return string.Format("{0}, {1}, {2}", this._a, this._b, this._c);
}
}
Struct_Test s = new Struct_Test(true, 15, 20);
string str = s.ToString();
IL_0001: ldloca.s s
IL_0003: ldc.i4.1
IL_0004: ldc.i4.s 15
IL_0006: ldc.i4.s 20
IL_0008: call instance void Test_Console.Struct_Test::.ctor(bool, int32, int32)
IL_000d: nop
IL_000e: ldloca.s s
IL_0010: constrained. Test_Console.Struct_Test
IL_0016: callvirt instance string [mscorlib]System.Object::ToString()
IL_001b: stloc.1
假如你仔细调查会发此刻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_Test s = new Struct_Test(true, 15, 20);
Console.WriteLine (GC.GetTotalMemory(false));
int hash = 0;
for (int i = 0; i < 10000000; ++i)
{
hash = s.GetHashCode();
}
Console.WriteLine(GC.GetTotalMemory(false));
Console.WriteLine (GC.CollectionCount(0));
运行功效为:
141200
399104
127
从上面的功效可以看到假如没有覆写虚函数确实引起了装箱。让我在比拟一下与挪用ToString时的不 同,s.ToString()请看反汇编;
s.ToString();
0000003d lea ecx,[ebp-44h]00000040 call FFE4C0B0
00000045 nop
s.GetHashCode();
00000046 mov ecx,7C3810h //Struct_Test要领表地点
0000004b call FFE31FAC //在堆上 分派空间
00000050 mov ebx,eax
00000052 lea edi, [ebx+4]00000055 cmp ecx,dword ptr [edi]00000057 lea esi,[ebp-44h] //将栈上数据拷贝到堆上
0000005a movq xmm0,mmword ptr [esi]0000005e movq mmword ptr [edi],xmm0
00000062 add esi,8
00000065 add edi,8
00000068 movs dword ptr es:[edi],dword ptr [esi]00000069 mov ecx,ebx
0000006b mov eax,dword ptr [ecx] //虚函数挪用
0000006d call dword ptr [eax+30h]
所以我们利用struct要小心不要因为健忘了覆写虚函数而造成不须要的机能损失。并且在这里因为没 有挪用Struct_Test自己的函数所以不会触发静态结构的执行。最后说一下struct在挪用函数的时候首先 要获得this指针,好比IL_000e: ldloca.s s。各人留意看这里不是ldloc所以对付Struct_Test的函数 挪用来说第一个参数是ref Struct_Test,感受ref的这个参数修饰用在这里才是最能浮现代价的。
#p#分页标题#e#
以上是我研究struct相关问题的一点心得,假如你看完这篇文章还存在疑问可能我有写表明的差池地 方请留言。我很是但愿能和对底层感乐趣的伴侣探讨。