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