技术开发 频道

IL可以看到的东西,其实大都也可以用C#来发现

 有朋友可能会说:即使无法把握JIT对于IL的优化,但是从IL中可以看出高级语言,如C#的编译器的优化效果啊。这话本没有错,但问题还是在于,C#的编译器优化效果,是否在“反编译”回来之后就无法观察到了呢?“优化过程”往往都是不可逆的,它会造成信息丢失,导致我们很难从“优化结果”中看出“原始模样”,这一点在上一篇文章中也有过论述。换句话说,我们通过C# => IL => C#这一系列“转化”之后,几乎都可以清楚地发现C#编译器做过哪些优化。这里还是使用经典的foreach作为示例,您知道以下两个方法的性能高低如何?

static void DoArray(int[] source)

 {

 foreach (int i in source)

 {

 Console.WriteLine(i);

 }

 }

 static void DoEnumerable(IEnumerable<int> source)

 {

 foreach (int i in source)

 {

 Console.WriteLine(i);

 }

 }

  经过了C#编译器的优化,再使用.NET Reflector查看IL反编译成C#(None Optimization)的结果,就会发现它们变成了此般模样:

 private static void DoArray(int[] source)

 {

 int num;

 int[] numArray;

 int num2;

 numArray = source;

 num2 = 0;

 goto Label_0014;

 Label_0006:

 num = numArray[num2];

 Console.WriteLine(num);

 num2 += 1;

 Label_0014:

 if (num2 < ((int)numArray.Length))

 {

 goto Label_0006;

 }

 return;

 }

 private static void DoEnumerable(IEnumerable<int> source)

 {

 int num;

 IEnumerator<int> enumerator;

 enumerator = source.GetEnumerator();

 Label_0007:

 try

 {

 goto Label_0016;

 Label_0009:

 num = enumerator.Current;

 Console.WriteLine(num);

 Label_0016:

 if (enumerator.MoveNext() != null)

 {

 goto Label_0009;

 }

 goto Label_002A;

 }

 finally

 {

 Label_0020:

 if (enumerator == null)

 {

 goto Label_0029;

 }

 enumerator.Dispose();

 Label_0029: ;

 }

 Label_002A:

 return;

 }

 C#编译器的优化效果表露无遗:对于int数组的foreach其实是被转化为类似于for的下标访问遍历,而对于IEnumerable<int>还是保持了foreach关键字中标准的“while...MoveNext”模式(如果您把Console.WriteLine语句去掉的话,就可以使用.NET 3.5 Optimization直接看出两者的不同,您不妨一试)。由此看来,DoArray的性能会比后两者要高。事实上,由于性能主要是由“实现方式”决定的,因此我们可以通过反编译成C#代码的方式来阅读.NET框架中的大部分代码,IL在这里起到的效果很小。例如在文章《泛型真的会降低性能吗?》里,Teddy大牛就通过阅读.NET代码来发现数组的IEnumerable实现,为什么性能远低于IEnumerable<T>。

 不过,判断两者性能高低,最简单,也最直接的方式还是进行性能测试。例如您可以使用CodeTimer来比较DoArray和DoEnumerable方法的性能,一目了然。

 值得一提的是,如果要进行性能优化,需要做的事情有很多,而“阅读代码”在其中的重要性其实并不高,而且它也最容易误入歧途的一种。“阅读代码”充其量是一种人工的“静态分析”,而程序的运行效果是“动态”的。这篇文章解释了为什么使用foreach对ArrayList进行遍历的性能会比List<T>低,其中使用了Profiler来说明问题。Profiler能告诉我们很多难以观察到的事情,例如在遍历中究竟是ArrayList哪个方法消耗时间最长。此外它还发现了ArrayList在遍历时创建了大量的对象,这种对于内存资源的消耗,几乎不可能从一小段代码中观察得出。此外,不同环境下,同样的代码可能执行效果会有不同。如果没有Profiler,我们可能会选择把一段执行了100遍的代码性能提升1秒钟,却不会把一段执行100000遍的代码性能提升100毫秒。性能优化的关键是“有的放矢”,如果没有Profiler帮我们指明道路,做到这一点相当困难。

 其实老赵对于性能方面说的这些,可以大致归纳为以下三点:

 关注IL,对于从微观角度观察程序性能很难有太大帮助,因为您很难具体指出JIT对IL的编译方式。

 关注IL,对于从宏观角度观察程序性能同样很难有太大帮助,因为它的表述能力不会比C#来的直观清晰。

 性能优化,最关键的一点是使用Profiler来找出性能瓶颈,有的放矢。

 所以,如果您问老赵:“学习IL,对写出高性能的.NET程序有帮助吗?”我会回答:“有,肯定有啊”。

 但是,如果您问老赵:“我想写出高性能的.NET程序,应该学习IL吗?”我会回答:“别,别学IL”。

 总结

 feilng在前文留下的一些评论,我认为说得非常有道理:

 IL只是在CLR的抽象级别上说明干什么,而不是怎么干……重要的是要清楚在现实条件下,需要进入那个层次才能获取足够的信息,掌握接口的完整语义和潜在副作用。

 IL的确比C#等高级语言来的所谓“底层”,但是很明显,IL本身也是一种高级抽象。而即使是机器码,它也可以说是基于CPU的抽象,CPU上如流水线,并行,内存模型,Cache Lock等东西对于汇编/机器码来说也可以说是一种“封装”。从不同层次可以获得不同信息,我们追求“底层”的目的肯定也不是“底层”这两个字,而是一种收获。了解自身需要什么,然后能够选择一个合理的层次进入,并得到更好的收益,这本身也是一种能力。追求IL的做法,本身并没有错,只是追求IL一定是当前情况下的最优选择吗?这是一个值得不断讨论的问题,我的这篇文章也只是表达了我个人对某些问题的看法。

原文地址:http://www.cnblogs.com/jeffreyzhao/archive/2009/06/06/my-view-of-il-3-use-c-sharp-instead-of-il.html

0
相关文章