技术开发 频道

CLR内部有太多太多IL看不到的东西

 那篇文章的目的是证明“.NET中,就算在使用Object作为泛型类型的时候,也不会比直接使用Object类型性能差”。于是老赵准备了两个类,一个是MyList泛型容器,一个是MyArrayList直接使用Object类型的容器。在Main方法中将对MyList<Object>和MyArrayList的下标索引进行访问。至此,便出现了一些疑问,为泛型容器使用Object类型,是否比直接使用Object类型性能要差?于是乎,我们来看MyArrayList.get_Item和MyList<T>.get_Item两个方法的IL代码——没错,就是它们的下标get操作:

// MyArrayList的get_Item方法

 .method public hidebysig specialname instance object get_Item(int32 index) cil managed noinlining

 {

 .maxstack 8

 L_0000: ldarg.0

 L_0001: ldfld object[] TestConsole.MyArrayList::m_items

 L_0006: ldarg.1

 L_0007: ldelem.ref

 L_0008: ret

 }

 // MyList<T>的get_Item方法

 .method public hidebysig specialname instance !T get_Item(int32 index) cil managed noinlining

 {

 .maxstack 8

 L_0000: ldarg.0

 L_0001: ldfld !0[] TestConsole.MyList`1::m_items

 L_0006: ldarg.1

 L_0007: ldelem.any !T

 L_000c: ret

 }

 朋友们一定已经发现了,这两个方法的区别只在于红色的两句。嗯,我们就“默认”ldfld指令的功能在两段代码中产生的效果完全相同(毕竟是相同的指令嘛),但是您觉得ldelem.ref指令和ldelem.any两条指令的效果如何,它们是一样的吗?我们通过查阅一些资料可以了解到说,ldelem.any的作用是加载一个泛型向量或数组中的元素。不过它的性能如何?您能得出结果说,它就和ldelem.ref指令一样吗?

 我想,除非您了解到JIT对待这两个指令的具体方式,否则您是无法得出其中性能高低的。因为IL还是过于高级,您看到了一条IL指令,您可以知道它的作用,但是您还是不知道它最终造成了何种结果。您还是无法证明“Object泛型集合的性能不会低于直接存放Object的非泛型集合”。也正因为如此,老赵在那篇文章里探究问题的手段,是比较了MyArrayList.get_Item方法和MyList<Object>.get_Item方法的汇编代码,最后得出结果是“毫无二致”。由于汇编代码和机器代码一一对应,因此观察汇编代码就可以完全了解CPU是如何执行这两个方法的。汇编代码一模一样,就意味着CPU对待这两个方法的方式一模一样,它们的性能怎么会有不同呢?

 于是,您过去,现在或将来,可能就会在某本书、某篇博客或文章看到这样一种说法:.NET的Object泛型容器的性能不会低于直接使用Object的容器,因为CLR在处理Object泛型的时候,会生成与直接使用Object类型时一模一样的类型,因此性能是不会降低的。但是您是通过学习IL可以了解这些吗?老赵认为,如果您只是学习了IL,最终还是要“听别人说”才能知道这些,而即使您不学IL,在“听别人说”了之后您也了解了这些——同时也不会因为不了解IL而变得“易忘”等等。

 同样道理,IL的call指令和callvirt指令的区别是什么呢?“别人会告诉你”call指令直接就去调用了那个方法,而callvirt还需要去虚方法表里去“寻找”那个真正的方法;“别人可能还会告诉你”,查找虚方法是靠方法表地址加偏移量;《Essential .NET》还会将方法表的实现结构告诉给你,而这些都是IL不会告诉您的。您就算了解再多IL,也不如“别人告诉你”的这些来得重要。您要了解“别人告诉你”的东西,也不需要了解多少IL。

 示例二:只有经过调用的方法才能获得其汇编代码吗?

 许多资料都告诉我们,在一个方法被第一次调用之前,它是不会被JIT的。也就是说,直到第一次调用时它才会被转化为机器码。不过,这个真是这样吗?我们还是准备一段简单的C#代码:

namespace TestConsole

 {

 class Program

 {

 [MethodImpl(MethodImplOptions.NoInlining)]

 private static void SomeMethod()

 {

 Console.WriteLine("Hello World!");

 }

 static void Main(string[] args)

 {

 Console.WriteLine("Before JITed.");

 Console.ReadLine();

 SomeMethod();

 Console.WriteLine("After JITed");

 Console.ReadLine();

 }

 }

 }

 那么Main方法的IL代码是怎么样的呢?

.method private hidebysig static void Main(string[] args) cil managed

 {

 .entrypoint

 .maxstack 8

 // 分配字符串"Before JITed"

 L_0000: ldstr "Before JITed."

 // 调用Console.WriteLine方法

 L_0005: call void [mscorlib]System.Console::WriteLine(string)

 // 调用Console.ReadLine方法

 L_000a: call string [mscorlib]System.Console::ReadLine()

 L_000f: pop

 // 调用Program.SomeMethod方法

 L_0010: call void TestConsole.Program::SomeMethod()

 // 分配字符串"After JITed"

 L_0015: ldstr "After JITed"

 // 调用Console.WriteLine方法

 L_001a: call void [mscorlib]System.Console::WriteLine(string)

 // 调用Console.ReadLine方法

 L_001f: call string [mscorlib]System.Console::ReadLine()

 L_0024: pop

 L_0025: ret

 }

0
相关文章