• 程序局部性原理感悟


    局部性原理
      程序的局部性原理是指程序在执行时呈现出局部性规律,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。
      局部性原理又表现为:时间局部性和空间局部性。
      时间局部性是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。
      空间局部性是指一旦程序访问了某个存储单元,则不久之后。其附近的存储单元也将被访问。
      这一规律是是普遍事实的总结,更是许多计算机技术的前提假设,比如.NET中托管堆以及代龄的处理过程,便是基于这个认识。
      之所以有这个规律,很多人认为原因是:程序的指令大部分时间是顺序执行的,而且程序的集合,如数组等各种数据结构是连续存放的。对于这一点,我个人表示赞同。
      程序的局部性原理是如此重要,以至于与程序设计的各个方面都存在密切的关系。
     
    局部性与效率
      熟悉代码的局部性原理,并且按照这个思路去写代码,可以显著的提高代码的执行效率,先看下面的C#代码:
    static void Main(string[] args)
    {
     int[,] a = new int[10000,10000];
     int sum = 0;
     // 按照先行后列的顺序遍历二维数组,这是正常做法
     WriteTimes(() =>
     {
      for (int i = 0; i < 10000; i++)
      {
       for (int j = 0; j < 10000; j++)
       {
        sum += a[i, j];
       }
      }
     });
     // 按照先列后行的顺序遍历二维数组,这是异常做法
     WriteTimes(() =>
     {
      for (int j = 0; j < 10000; j++)
      {
       for (int i = 0; i < 10000; i++)
       {
        sum += a[i, j];
       }
      }
     });
     
     Console.Read();
    }
     
    static void WriteTimes(Action func)
    {
     DateTime dt0 = DateTime.Now;
     func();
     DateTime dt1 = DateTime.Now;
     Console.WriteLine((dt1 - dt0).Milliseconds);
    }

      大家可以输出一下,在我的机器上的输出为(具体的数值可能不同,但是大小比例应该差不多):

    102
    999

      这个例子本身并没什么实际的意义,但如果处理的数据量足够大,并且可能需要频繁的在外存、内存、缓存间调度,又注重效率的话,这个问题就有可能会被陡然放大了。不过,通常来说,效率总是在程序出现性能问题后才应该被关注的方面

     
    局部性与缓存
      归根结底,缓存(各种缓存技术,CPU缓存,数据库缓存,服务器缓存)探讨的也基本都是效率的问题,看另一个来源于网上某位仁兄的问题:
    // 写法一:循环内塞进好几件事
    for (int i = 0; i < 1000; i++)
    {
     WriteIntArray();
     WriteStringArray();
    }
    // 写法二:循环内只干一件事
    for (int i = 0; i < 1000; i++)
    {
     WriteIntArray();
    }
    for (int i = 0; i < 1000; i++)
    {
     WriteStringArray();
    }

    问:两种写法哪个好?

      有的同学认为写法一效率高,因为循环只执行了一遍,而有的同学认为写法二效率高,因为该写法中每个循环内的局部变量大部分情况下是比写法一少,这样更容易利用CPU的寄存器以及各级缓存,这满足局部性原理,所以效率较好。
      我写了简单的程序验证了一下,发现确实有时候写法一执行时间较短,有时候写法二执行之间较短,没有明显的固定规律,所以我认为这里的效率一说不太明显,当然了也许是这里的循环次数比较少,循环多次的低效还没有体现出来,感兴趣的可以自己试一下大的循环。
      即使是这样的结果,我还是倾向于使用第二种写法,这不是效率的原因,而是重构中,提倡一个循环只干一件事。
     
    局部性与重构
      重构的基本原理这里就不多说了,感兴趣的随便搜一下就可以了。重构的基本原则中就有诸如:一个循环只干好一件事,关联性强的代码放到一起,变量定义在使用的地方等等。这些原则与局部性原理阐述的规律竟然是如出一辙。
      看一些我认可的写法:
    // 一个循环内只干好一件事
    for (int i = 0; i < 1000; i++)
    {
     WriteIntArray();
    }
    for (int i = 0; i < 1000; i++)
    {
     WriteStringArray();
    }
     
    // 原始的代码
    List<int> salaryList = new List<int>();
    List<int> levelList = new List<int>();
    List<int> scoreList = new List<int>();
     
    collectHighSalary(salaryList);
    collectHighLevel(levelList);
    collectHighScore(scoreList);
     
    collectMiddleSalary(salaryList);
    collectMiddleLevel(levelList);
     
    collectLowSalary(salaryList);
    collectLowlevel(levelList);
    // 重构成:
    // 有关系的代码放到一起
    // 变量需要时再定义
    List<int> salaryList = new List<int>();
    collectHighSalary(salaryList);
    collectMiddleSalary(salaryList);
    collectLowSalary(salaryList);
     
    List<int> levelList = new List<int>();
    collectHighLevel(levelList);
    collectMiddleLevel(levelList);
    collectLowlevel(levelList);
     
    List<int> scoreList = new List<int>();
    collectHighScore(scoreList);  

      局部性原理不仅与语句和函数的组织方式息息相关,还与组件的组织方式互相呼应。

     
    局部性与高内聚
      从元素(函数,对象,组件,乃至服务)设计的角度,内聚性是描述一个元素的成员之间关联性强弱尺度。如果一个元素具有很多紧密相关的成员,而且它们有机的结合在一起去完成有限的相关功能,那这个元素通常就是高度内聚的。高内聚的设计是一种良好的设计。
      耦合性从另一个角度描述了元素之间的关联性强弱。元素之间联系越紧密,其耦合性就越强,元素的独立性则越差,元素间耦合的高低取决于元素间接口的复杂性,调用的方式以及传递的信息。低耦合的设计是一种良好的设计。
      一个具有低内聚,高耦合的元素会执行许多互不相关的逻辑,或者完成太多的功能,这样的元素难于理解、难于重用、难于维护,常常导致系统脆弱,常常受到变化带来的困扰。
      毫无疑问,遵循良好的局部性原理通常能得到良好的高内聚低耦合元素,反之,代码中元素的高内聚低耦合也使的局部性得以大大加强,此所谓相得益彰。
     
    局部性与命名
      说到命名,不得不提著名的匈牙利命名法。
      在我读了《软件随想录:程序员部落酋长Joel谈软件》一书之前,我认为的匈牙利命名法则就是在驼峰式命名的基础上,在变量名前加上变量的类型,例如iLength表示int型的表示长度的变量。
      但是在我阅读了《软件随想录》一书相关的章节以后,才彻底的了解到其中的误解。原来微软那位大牛推荐的匈牙利命名法居然不是我想的那样。
      在该书中,作者将匈牙利命名法分为两种,流行的并且被废掉的叫“系统型匈牙利命名法则”,这种命名法将变量类型加到了变量名字前面,老实说确实没什么意义,特别是在现代编辑器中。
      事实上,微软那位仁兄推荐的是叫做“应用型匈牙利命名法则”的规则,那就是把变量的应用场景加到变量的名字前面
      比如在页面开发中,直接从用户输入得到的Name字符串可以起名叫:usName,其中us代表unsafe,意思是这个字符串是用户输入的,没用经过编码处理,可能是不安全的。而经过编码的Name字符串可以起名叫sName,其中s代表safe,意思是这个字符串经过了编码处理,是安全的。
      谈到命名的规则,就是为了说明下面这个息息相关的问题:代码错误检查。
      代码错误检查也是一个经典的话题,如何让代码的错误提前暴露出来,而不是发布后由客户去发现,这是个问题。
      满足局部性原理,使得我们的程序内聚性通常很好,但是毫无疑问,有些元素还是必须要贯穿很多的行的,比如在某些函数中,定义变量和末次使用变量的地方可能相差几十行:
    var usName = getName();
    action1(usName);
    // 此处省略20行...
    sName = usName;
    // 此处省略10行...
    document.write(sName);

      我不得不承认,Joel老兄提出的“应用型匈牙利命名法则”还是相当有作用的。比如中间那行:

    sName = usName;

      我们很容易就会从变量名发现这行代码存在安全性威胁。

     
    我们其实生活在世界的局部中
      推而广之,局部性原理不仅仅是适用于程序的理论,而是适用于我们生活的各个方面的重要规律,它的称呼向来随着场合的不同而有所变化,比如有时叫“习惯”,有时叫“惯性”,有时又演化成“熟悉的人/事”等。总之,我个人认为,人总是倾向于在局部的、连续的时间空间内做相关的、熟悉的事情,程序其实是人类做事风格的反应。
  • 相关阅读:
    第三波精品Android源码袭来!免费下载
    推荐!Html5精品效果源码分享
    又来一波!Android精品源码分享
    Android精品资源汇总,10个源码(持续更新)
    Android开发者学习必备:10个优质的源码供大家学习
    专访方立勋:开发者应该保持好奇和热情
    将博客搬至CSDN
    GIT修改commit信息
    使用FormData上传文件
    Spring MVC @PathVariable with dot (.) is getting truncated
  • 原文地址:https://www.cnblogs.com/dxy1982/p/4194218.html
Copyright © 2020-2023  润新知