Effective C# Item 31: Prefer Small,Simple Functions

      对于有经验的程序员来说,不论在接触C#之前使用什么语言,都有一些提高代码效率的方法。不过有些做法虽然在之前的语言中有效,但在.Net环境中却适得其反。这一点在我们尝试手动为C#编译器优化算法时尤为明显。我们的举动往往使得JIT编译无法做出更加有效的优化。那些以优化为目的的工作,结果往往是生成更慢的代码。我们完全不必追求创建最直截了当的代码,有些工作完全可以交给编译器完成。有些过分优化会造成问题,一个典型的例子就是我们为了避免进行函数调用而创建一个又长又复杂的函数。这样的做法会降低.Net应用程序的表现,是和初衷相违背的。让我们来注意一下其中的细节。

      这里简单的介绍一下JIT编译的工作原理。.Net在运行时通过JIT编译器将C#编译器生成的IL转换为机器代码。这个工作贯穿在程序运行的生命周期中。JIT并不是在程序开始时就处理整个应用程序,而是一个函数一个函数的处理。在程序启动时只处理必需的部分函数,其他的代码只在需要使用时才进行JIT编译。那些永远不会被调用的函数永远也不会被JIT编译。相比那中少而大的函数设计,小而多的函数设计反而能减少代码的额外开销。我们考虑下面的代码:

        public 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编译。但其中只有一条路径上的代码是有用的。但是我们可以考虑这样改进函数:

        public 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编译器优化对寄存器的使用。

      JIT编译器同样会关系到内联函数。内联函数就是以函数体来取代函数调用。考虑下面的例子:

        private string _name;
        
public string Name
        
{
            
get
            
{
                
return _name;
            }

        }

       
//访问
        string val = Obj.Name

      和调用函数相比,属性访问器使用更少的指令代码。函数调用不仅要保存寄存器状态、执行方法代码和保存返回值,而且当其参数需要入栈时更会需要更多的代码。如果我们这样写,所需的机器指令会更少:

string val = Obj._name

      当然,我们都明白使用属性是好过于直接创建公有的数据成员的。JIT编译器为了同时兼顾效率和规范,将内联属性访问器。JIT编译器会内联一些可以获得速度和空间优化的函数。我们不需要为内联定义额外的规则,内联函数并不是我们的职责。C#语言也没有提供一个可以只是编译器该方法为内联方法的关键字。事实上C#编译器也没有为JIT编译器提供任何内联的信息。我们所有能做的就是让代码保持清晰,这样JIT编译器更容易做出最佳判断。较小的函数是适于内联的。但是注意任何虚函数或者包含了try/catch的函数都不可能成为内联函数,即便它非常小。

      我们不需要为我们的算法确定一个在机器级别最好的表现。C#编译器和JIT编译器会为我们做这些工作。C#编译器为每个方法生成IL。JIT编译器再将这些IL转换为目标机的机器代码。我们不必太过专注于JIT编译器使用的每一条规则,而是应该将这些精力投入到如何将算法表达的更加规范,让运行环境的工具可以更有效的为我们工作。

      将C#代码转换为机器可执行的代码需要两个步骤。C#编译器生成IL,JIT编译器在需要时为每个方法生成机器代码。小函数可以让JIT编译器分期处理以减少消耗,而且也有助于内联。但是光是小还是不够,更简单的控制流程也很重要。减少控制流程分支有助于JIT编译器注册临时变量。这不仅关系到我们的代码是否清晰,也关系到执行效率问题。

      译自   Effective C#:50 Specific Ways to Improve Your C#                      Bill Wagner著

      回到目录
 

posted on 2007-05-27 21:03  aiya  阅读(1355)  评论(4编辑  收藏  举报