现在的位置: 首页 > 综合 > 正文

Effective C#之Item 31:Prefer Small, Simple Functions

2014年01月05日 ⁄ 综合 ⁄ 共 8690字 ⁄ 字号 评论关闭

Item 31: Prefer Small, Simple
Functions

优先选择小而简单的方法

As
experienced programmers, in whatever language we favored before C#, we
internalized several practices for developing more efficient code. Sometimes
what worked in our previous environment is counterproductive in the .NET
environment. This is very true when you try to hand-optimize algorithms for the
C# compiler. Your actions often prevent the JIT compiler from more effective
optimizations. Your extra work, in the name of performance, actually generates slower
code. You're better off writing the clearest code you can create. Let the JIT
compiler do the rest. One of the most common examples of premature
optimizations causing problems is when you create longer, more complicated
functions in the hopes of avoiding function calls. Practices such as hoisting
function logic into the bodies of loops actually harm the performance of your
.NET applications. It's counterintuitive, so let's go over all the details.

作为有经验的程序员,无论在C#之前你偏爱什么语言,我们有一些可以让你开发更高效代码的实践。有时,我们在先前的环境下所做的工作,在.Net环境下反而起了相反的效果。当你尝试手动为C#编译器优化一些算法的时候,这表现的更强烈。你的动作经常阻止JIT编译器生成更高效的优化。你的额外的工作,打着性能的旗号,实际上生成了更慢的代码。你最好以你最清楚的方式编写代码。让JIT编译器做剩下的工作。最常见的例子就是预先优化,你创建一个很长很复杂的方法,本想用它来避免太多的函数调用,结果会导致很多问题。实际操作时,提升这样一个方法逻辑到循环体中对.Net程序的性能是有害的。这与你的直觉是相反的,让我们来看一些细节。

This chapter's
introduction contains a simplified discussion of how the JIT compiler performs
its work. The .NET runtime invokes the JIT compiler to translate the IL
generated by the C# compiler into machine code. This task is amortized across
the lifetime of your program's execution. Instead of JITing your entire
application when it starts, the CLR invokes the JITer on a function-by-function
basis. This minimizes the startup cost to a reasonable level, yet keeps the
application from becoming unresponsive later when more code needs to be JITed.
Functions that do not ever get called do not get JITed. You can minimize the
amount of extraneous code that gets JITed by factoring code into more, smaller
functions rather than fewer larger functions. Consider this rather contrived
example:

本章的介绍部分包含了一个简单的讨论:JIT编译器如何工作。.Net运行时调用JIT编译器将C#编译器生成的IL转换成机器代码。该任务贯穿了程序运行整个生命周期,是分布的。不是在启动时,编译整个应用程序,相反,CLR是一个方法一个方法的调用JIT的。这样的方式将启动花费减小到一个合理的级别,并且在后来当更多的代码需要被JIT的时候,可以避免应用程序无反应。不被调用的方法不会被JIT。你可以通过将代码提炼成更多更小的方法(而不是更少的大方法),来减小需要被JIT的外部代码的总量,考虑这个相当做作的例子:

  1. public string BuildMsg( bool takeFirstPath )
  2. {
  3.   StringBuilder msg = new StringBuilder( );
  4.   if ( takeFirstPath )
  5.   {
  6.     msg.Append( "A problem occurred." );
  7.     msg.Append( "/nThis is a problem." );
  8.     msg.Append( "imagine much more text" );
  9.   } else
  10.   {
  11.     msg.Append( "This path is not so bad." );
  12.     msg.Append( "/nIt is only a minor inconvenience." );
  13.     msg.Append( "Add more detailed diagnostics here." );
  14.   }
  15.   return msg.ToString( );
  16. }

The
first time BuildMsg gets called, both paths are JITed. Only one is needed. But
suppose you rewrote the function this way:

BuildMsg第一次被调用时,2个路径都被执行JIT编译,但是只有一个是必须的。假设你这样重写该方法:

  1. public string BuildMsg( bool takeFirstPath )
  2. {
  3.   if ( takeFirstPath )
  4.   {
  5.     return FirstPath( );
  6.   } else
  7.   {
  8.     return SecondPath( );
  9.   }
  10. }

Because
the body of each clause has been factored into its own function, that function
can be JITed on demand rather than the first time BuildMsg is called. Yes, this
example is contrived for space, and it won't make much difference. But consider
how often you write more extensive examples: an if statement with 20 or more
statements in both branches of the if statement. You'll pay to JIT both clauses
the first time the function is entered. If one clause is an unlikely error
condition, you'll incur a cost that you could easily avoid. Smaller functions
mean that the JIT compiler compiles the logic that's needed, not lengthy
sequences of code that won't be used immediately. The JIT cost savings
multiplies for long switch statements, with the body of each case statement
defined inline rather than in separate functions.

因为每个括号的内容都被提炼到了各自的方法中,所以这些方法可以根据需要被JIT,而不是在BuildMsg第一次被调用的时候执行。是的,这个例子是精心准备的,也没有什么太特别的。但是考虑一下你编写的更大的例子呢:一个if语句中,在每个片段中都包含了20个或者更多的语句?在该方法第一次被调用的时候,你需要为2个括号付出代价。如果一个括号不像是错误条件,你会招致了本来可以简单避免的花费。更小的方法意味着JIT编译器只编译需要的逻辑,不编译不会马上就被使用的一长串代码。对于switch语句,每个case语句被定义成内联的,比定义在单独的方法里面,JIT会花费数倍的存储代价。

Smaller
and simpler functions make it easier for the JIT compiler to support
enregistration. Enregistration is the process of selecting which local
variables can be stored in registers rather than on the stack. Creating fewer local
variables gives the JIT compiler a better chance to find the best candidates
for enregistration. The simplicity of the control flow also affects how well
the JIT compiler can enregister variables. If a function has one loop, that
loop variable will likely be enregistered. However, the JIT compiler must make
some tough choices about enregistering loop variables when you create a
function with several loops. Simpler is better. A smaller function is more
likely to contain fewer local variables and make it easier for the JIT compiler
to optimize the use of the registers.

小而简单的方法使得JIT编译器支持可注册更容易。可注册是一个选择过程:将哪些局部变量存储到寄存器中,这比存储到堆栈中要好。创建更少的局部变量给JIT提供了更好的机会寻找最合适寄存器候选对象。这个简单的控制流程同样会影响JIT编译对变量注册的好坏。如果一个方法只有一个循环,那么循环变量就很可能被放入寄存器。然而,当你一个方法中含有多个循环时,对于寄存器的变量注册,JIT编译器就不得不做出一些困难的决择。简单就是好,小函数很可能只包含几个变量,这样可以让JIT很容易优化寄存器的使用。

The JIT
compiler also makes decisions about inlining methods. Inlining means to
substitute the body of a function for the function call. Consider this example:

JIT编译器同样也决定是否内联方法。内联意味着直接使用函数体而不调用函数。考虑这个例子:

  1. // readonly name property:
  2. private string _name;
  3. public string Name
  4. {
  5.   get
  6.   {
  7.     return _name;
  8.   }
  9. }
  10.  
  11. // access:
  12. string val = Obj.Name;

 

The body
of the property accessor contains fewer instructions than the code necessary to
call the function: saving register states, executing method prologue and
epilogue code, and storing the function return value. There would be even more
work if arguments needed to be pushed on the stack as well. There would be far
fewer machine instructions if you were to write this:

比使用必要的代码来调用该方法,属性访问器包含了更少的指令,对于方法调用,需要存储寄存器状态,顺序执行方法,存储方法的返回值。如果参数还需要被压入栈的话,那么将有更多的工作要做。如果你这样编写的话,便可以有更少的机器指令了。

  1. string val = Obj._name;

Of
course, you would never do that because you know better than to create public
data members (see Item 1). The JIT compiler understands your need for both
efficiency and elegance, so it inlines the property accessor. The JIT compiler
inlines methods when the speed or size benefits (or both) make it advantageous
to replace a function call with the body of the called function. The standard
does not define the exact rules for inlining, and any implementation could
change in the future. Moreover, it's not your responsibility to inline
functions. The C# language does not even provide you with a keyword to give a
hint to the compiler that a method should be inlined. In fact, the C# compiler
does not provide any hints to the JIT compiler regarding inlining. All you can
do is ensure that your code is as clear as possible, to make it easier for the
JIT compiler to make the best decision possible. The recommendation should be
getting familiar by now: Smaller methods are better candidates for inlining.
But remember that even small functions that are virtual or that contain
try/catch blocks cannot be inlined.

当然,你从不会那样做,因为最好不要创建公共的数据成员(Item 1)JIT编译器了解你的对于效率与优雅的需求,因此它对属性访问符进行了内联。当速度或者存储空间的利益(或者两者都有)使得有必要用方法体代码本身替换方法调用时,JIT编译器就会对方法进行内联。规范并没有为内联定义精确的规则,同时任何实现在将来都可能改变。还有,内联方法不是你的职责。C#语言甚至没有提供一个关键字,来提示JIT编译器:哪个方法需要被内联。事实上,C#编译器,没有给JIT编译器任何提示要求考虑内联。为了使JIT编译器更容易做出最好的决定,所有你能做的就是保证代码尽可能简洁。到现在为止,这个建议应该很熟悉了:小方法是内联的更好的候选者。但是记住:即使是小方法,如果是虚的或者包含了try/catch块的话,也是不能内联的。

Inlining
modifies the principle that code gets JITed when it will be executed. Consider
accessing the name property again:

内联修改了代码被JIT的原则。再次考虑访问name属性:

  1. string val = "Default Name";
  2. if ( Obj != null )
  3.   val = Obj.Name;

 

If the
JIT compiler inlines the property accessor, it must JIT that code when the
containing method is called.

如果JIT编译器内联了该属性访问符,那么,当包含的方法被调用时,就必须JIT那些代码。

It's not
your responsibility to determine the best machine-level representation of your
algorithms. The C# compiler and the JIT compiler together do that for you. The
C# compiler generates the IL for each method, and the JIT compiler translates
that IL into machine code on the destination machine. You should not be too
concerned about the exact rules the JIT compiler uses in all cases; those will
change over time as better algorithms are developed. Instead, you should be
concerned about expressing your algorithms in a manner that makes it easiest
for the tools in the environment to do the best job they can. Luckily, those
rules are consistent with the rules you already follow for good
software-development practices. One more time: smaller and simpler functions

为你的算法决定机器级别上最好的表现,不是你的责任。C#编译器和JIT编译器一起为你做这件事情。C#编译器为每个方法生成IL代码,JIT编译器将IL转变成目标机器的机器代码。你没必要太关心JIT编译器处理所有情况的精确规则;随着时间的变化,当更好的算法被开发出来后,这些都会改变。相反,你应该关心用什么样的方式来表达你的算法,使得你的环境下的工具能最好的发挥它们的作用。幸运的是,这些规则和你已经遵循的好的软件开发实践的规则是一致的。再说一次:小而简单的方法。

Remember
that translating your C# code into machine-executable code is a two-step
process. The C# compiler generates IL that gets delivered in assemblies. The
JIT compiler generates machine code for each method (or group of methods, when
inlining is involved), as needed. Small functions make it much easier for the
JIT compiler to amortize that cost. Small functions are also more likely to be
candidates for inlining. It's not just smallness: Simpler control flow matters
just as much. Fewer control branches inside functions make it easier for the
JIT compiler to enregister variables. It's not just good practice to write
clearer code; it's how you create more efficient code at runtime.

记住,将C#代码转换成机器可执行的代码是一个两步的过程。C#编译器生成以程序集的形式发布的ILJIT编译器为每个方法(或者,如果涉及到内联的话,就是一组方法)生成必需的机器码。小方法使得JIT编译器平衡代价更容易。小方法也更可能成为内联的候选者。它不仅仅是小:简单的控制流程也是很重要的。方法内部更少的控制分支使得JIT编译器为寄存器注册变量更容易。编写简洁的代码,不仅仅是良好的实践,同时,也表示了你如何创建在运行时高效的代码。

抱歉!评论已关闭.