在应用程序开发过程中,开发者都力求写出更加高效的代码。但是当你想手工为C#编译器优化代码时,你的种种优化可能反倒会阻碍JIT进行更加高效的优化。因此,我们最好尽可能的写出最清晰的代码,将优化工作交给JIT编译器去完成。
在.NET平台下开发程序的开发者都应该知道:.NET运行时将调用JIT编译器来将C#编译器生成的IL翻译成机器码。JIT不会在程序刚开始的时候就完全翻译所有的IL代码,CLR根据函数的粒度来逐一进行JIT编译。没有被调用的函数根本不会被JIT编译,因此将那些非常重要的逻辑分解成更多的小方法要比把所有逻辑放在一起形成大型复杂函数更有效率。例如下面的代码:
1 public string BuildMsg(bool takeFirstPath) 2 { 3 StringBuilder msg = new StringBuilder(); 4 5 if (takeFirstPath) 6 { 7 msg.Append("A problem occurred."); 8 msg.Append("\nThis is a problem."); 9 msg.Append("imagine much more text"); 10 } 11 else 12 { 13 msg.Append("This Path is not so bad."); 14 msg.Append("\nIt is only a minor inconvenience."); 15 msg.Append("Add more detailed diagnostics here."); 16 } 17 return msg; 18 }
在第一次调用BuildMsg时,if-else两个分支都将被JIT编译。而实际上仅需要编译其中的一个分支就足够了,我们可以拆分这个方法,对其进行优化,下面是优化后的代码:
1 public string BuildMsg(bool takeFirstPath) 2 { 3 if (takeFirstPath) 4 { 5 return FirstPath(); 6 } 7 else 8 { 9 return SecondPath(); 10 } 11 } 12 13 public string FirstPath() 14 { 15 StringBuilder msg = new StringBuilder(); 16 17 msg.Append("A problem occurred."); 18 msg.Append("\nThis is a problem."); 19 msg.Append("imagine much more text"); 20 21 return msg.ToString(); 22 } 23 24 public string SecondPath() 25 { 26 StringBuilder msg = new StringBuilder(); 27 28 msg.Append("This Path is not so bad."); 29 msg.Append("\nIt is only a minor inconvenience."); 30 msg.Append("Add more detailed diagnostics here."); 31 32 return msg.ToString(); 33 } 34 }
这时候两个方法可以根据需要再进行JIT编译,而不必在第一次调用BuildMsg方法是进行。我们可以看出:更小的函数让JIT编译器更方便的根据需要进行编译,而不是将时间浪费在不急于一时使用的代码上。对于switch语句中的每个case中的代码,这个规则的影响更明显。
寄存器的优化
小而简单的方法会让JIT更容易的进行寄存器的选择工作,即选择哪个局部变量可以存放在寄存器中,而不是栈上。越少使用局部变量,也就让JIT编译器能够更方便的找到最适合放在寄存器的那一些。而越小的函数包含的局部变量也越少,也就更方便JIT对寄存器进行优化。
内联的优化
内联表示把函数体替换到函数被调用的位置。由JIT编译器负责决定哪些方法应该被内联,当内联可以有效提高效率时,JIT编译器将自动执行。不过内联的标准并不是固定的,且当前的规则也不能保证将来不会发生变化,此外,是否内联完全由JIT自己决定。不过我们可以使用下面的特性选项通知JIT不要内联某个方法:
1 [MethodImpl(MethodImplOptions.NoInlining)]
方法越简单就越适合内联。不过虚方法和包含ctry/catch代码块的方法将不会被内联。内联也改变了:代码在执行时才会被JIT编译 这一原则。所以在.NET平台下编程我们的责任应该就是尽量编写短小精悍的方法,而为你的算法生成高效的机器码是C#编译器和JIT编译器的责任。
小节:
将C#代码翻译为可执行的机器码有两个步骤:1.C#编译器将代码生成为IL,并放在程序集中。2.JIT再根据需要逐一为方法(或是一组方法,如果涉及内联)生成机器码。短小的方法让JIT编译器能够更好的平摊编译的代价。短小的代码也更适合内联。方法除了短小之外,简化控制流程也很重要,控制的分支越少JIT编译器也更容易选择找到最适合放在寄存器中的变量。因此,编写短小精悍的代码不但影响代码的可读性,也影响到程序运行的效率。