这一点在我们尝试手动为C#编译器优化算法时尤为明显。我们的举动往往使得JIT编译无法做出更加有效的优化。那些以优化为目的的工作,结果往往是生成更慢的代码。我们完全不必追求创建最直截了当的代码,有些工作完全可以交给编译器完成。有些过分优化会造成问题,一个典型的例子就是我们为了避免进行函数调用而创建一个又长又复杂的函数。这样的做法会降低.Net应用程序的表现,是和初衷相违背的。让我们来注意一下其中的细节。
这里简单的介绍一下JIT编译的工作原理。.Net在运行时通过JIT编译器将C#编译器生成的IL转换为机器代码。这个工作贯穿在程序运行的生命周期中。JIT并不是在程序开始时就处理整个应用程序,而是一个函数一个函数的处理。在程序启动时只处理必需的部分函数,其他的代码只在需要使用时才进行JIT编译。那些永远不会被调用的函数永远也不会被JIT编译。相比那中少而大的函数设计,小而多的函数设计反而能减少代码的额外开销。我们考虑下面的代码:
string BuildMsg( bool takeFirstPath)
{
StringBuilder msg =
new StringBuilder();
if (takeFirstPath)
{
msg.Append("A problem occurred");
msg.Append("\nThis is a problem");
msg.Append("imagine much more text");
}
else
{
msg.Append("This path is not so bad");
msg.Append("\nIt is only a minor inconvenience");
msg.Append("Add more detailed diagnostics here");
}
return msg.ToString();
}
在第一时间BuildMsg被调用,所有的代码都会被JIT编译。但其中只有一条路径上的代码是有用的。但是我们可以考虑这样改进函数:
string BuildMsg( bool takeFirstPath)
{
StringBuilder msg =
new StringBuilder();
if (takeFirstPath)
{
return FirstPath();
}
else
{
return SecondPath();
}
}
不同于最开始的代码,现在每个分支都调用了它们各自的函数。这种做法节省了运行时的消耗,虽然这点看起来消耗微不足道。但是我们考虑一下更极端一点的例子:一个if的两个分支中各包含有20个甚至更多分支。原先的做法会在开始时将整个函数读入,招致不必要的消耗。如果将函数细化JIT编译器就会以需求逻辑对函数进行编译。那些不必需的代码就不会马上被编译。对于那些较长的switch分支来说,将每个case分别定义为不同的函数可以将消耗节省几倍。
小而简单的函数有助于JIT编译器更轻松的对其进行注册。通过注册,局部变量可以被存储在寄存器中而不是栈中。创建较少的局部变量有助于JIT编译器找到最佳候选注册变量。同样控制流程也会影响到JIT编译器注册变量。如果一个函数中包含一个循环,那么这个循环变量就很可能被注册。但是一旦一个函数中有多个循环,那么JIT编译器就必需在这些循环变量中做出一些选择。简单的函数可能包含较少的局部变量,这有助于JIT编译器优化对寄存器的使用。