Linux系统响应速度性能改进实例

发表于:2007-07-04来源:作者:点击数: 标签:
Cameron Laird 提供了一些有用的示例,这些示例对于很有可能在您自己的应用程序 开发 中发生的各种 性能 问题而言,是很合适的模型。 性能突破似乎有两种不同的实现方式:简单的和困难的。这并非陈词滥调;这两者之间的界限极其清晰。 当您听说了某些简单的

    Cameron Laird 提供了一些有用的示例,这些示例对于很有可能在您自己的应用程序开发中发生的各种性能问题而言,是很合适的模型。
  
    性能突破似乎有两种不同的实现方式:简单的和困难的。这并非陈词滥调;这两者之间的界限极其清晰。
  
    当您听说了某些简单的方式时,您拍手叹道:“噢”、“对极了”或“太棒了”。虽然在许多情况下实现这些方式的第一个应用需要相当聪明的头脑,但是它们易于理解。
  
    另一种方式涉及了仔细的测量、专门的知识和大量的调优工作。这些过程通常是令人痛苦的,也是值得的,要视“本地条件”(比如硬件的具体情况)而定。
  
    优秀的程序员可以用“困难”或“简单”方式进行工作。由于性能对 Linux 程序员而言是非常重要的主题,因此我提供了四个来源于实际(编程)生活中的故事,它们按“困难”和“简单”方式配成两对。
  
    始终从需求入手
  
    死亡和交税都是不可避免的。对于开发人员而言,与此差不多同等重要的是仔细考虑需求;这是每种编程都会提到的一个主题。
  
    尽管性能的确很重要,但是处理这种需求的最佳方法往往并不是显而易见的。我经常会碰到一个软件难题,它大致符合这样的模式:程序正处于使用之中。它的功能是正确的。但是某个用户使用了一下,然后报告说它“太慢了”,需要加速。某团队成员很快添加了“监视器”,这降低一点性能,但是使用户能一直知晓长期运行的计算还需多少时间。于是满意度就增加了。
  
    我在 20 世纪 60 年代末首次注意到这种现象,此后每十年就可以看到一回。我觉得从某种意义上讲自从计算开始它就一直在发生。关键并不在于最终用户容易受骗或者可以被光鲜的“装饰”转移注意力。相对我们狭隘、专业的“性能”概念而言,他们只是有更广泛的目的。在许多情况下,当他们指出计算需要更快一点时,他们真正的意思是他们需要更快更可靠地知道该计算将要进行多久。得知精确的时间信息后,最终用户可以在计算机延迟期间快乐地安排其它任务。
  
    建议参与处理性能的每个人都进行“热身练习”。首先,阅读或重读您最喜欢的有关需求管理方面的参考资料;下面的参考资料一节提供了几个有用的起点。
  
    其次,在您的工具箱中放置一些“进度监视器”、“进度栏”或“秒表”,当您发现让用户等得太久时可以迅速地将其插入应用程序中。这些根本不用付出太多,如下面这两个示例所示。
  
    两个倒计时示例
  
    执行一个长时间的计算,同时又要让用户知晓其进度,这构成了应用程序设计中一个非常有趣的问题。它是个并发性或“多任务”的实例。许多开发人员都相信:作为正确的解决方案,并发性需要有精巧的机制,特别需要有复杂的线程代码。
  
    其实不然。下面的参考资料一节列出了一篇较早的 developerWorks 专栏文章,我在其中概述了并发性涉及的许多技术。此外,至少自 1988 年以来,基本上已经在所有 UNIX 主机上实现了简单的并发编程,在 1988 年这一年 ksh 对其“协同进程(co-process)”进行了标准化。如果您将下面清单 1 中的 ksh 源代码保存为 ex1.ksh,然后运行它,您会看到“倒计时”显示:“十、九、八,等等”,然后看到 ksh 子进程的结果:“All done”。实际运用中,您可能会对长期运行的化学计算或数据库检索使用类似这样的操作:启动操作作为子进程,但是让用户始终知道操作的进展或完成之前还剩多少时间。更新显示只用了几行源代码。
  
    清单 1. 示例倒计时显示的 ksh 源代码
  
  (echo "This models a long-lasting process"; sleep 10;
                      echo "All done") |&
      for ((i = 10; i > 0; i--))
      do
        printf "%2d seconds before completion ...\r" $i
        sleep 1
      done
  
      read -p line;   # Discard the title line.
      read -p line
      echo ""
      echo "Output from the sub-process is '$line'."
  
  
  
    为用户提供“实时”信息只需要进行适量的编码;即使基于字符的应用程序也可以做到这一点。如果图形用户界面(GUI)更适合您的情况,那也很简单。许多工具箱都内置了“忙碌”、“等待”或“进度”显示。如果您还没有这样一个工具箱,那么几行 Tk 或另一个新式的 GUI 工具箱就足以制作有点“类似的”倒计时钟面或进度栏。下面是一个在几个页面试用过的“从头开始做的”示例:
  
    清单 2. 示例倒计时显示的 Tk 源代码
  
  proc countdown {seconds} {
       init
       countdown_kernel $seconds
     }
     
     
     proc countdown_kernel seconds {
       hands $seconds
       if !$seconds return
       after 1000 [list countdown_kernel [incr seconds -1]]
     }
     
     
     proc draw_hand {angle decorations} {
       eval .c create line $::size $::size [get_xy $angle] $decorations
     }
     
     
     proc end_coordinate difference {
       set hand_length [expr $::size * .9]
       return [expr $::size + $hand_length * $difference]
     }
     
     
     proc get_xy angle {
       return [list [end_coordinate [expr sin($angle)]]            [end_coordinate [expr -cos($angle)]]]
     }
     
     
     proc hands seconds {
       catch {.c delete withtag hands}
     
       set twopi 6.283185
       set seconds_angle [expr $seconds * $twopi / 60.]
       draw_hand $seconds_angle "-width 1 -tags hands"
       set minutes_angle [expr $seconds_angle / 60.]
       draw_hand $minutes_angle            "-width 3 -capstyle projecting -tags hands"
     }
     
     proc init {} {
       catch {destroy .c}
       set ::size 30
       set full_diameter [expr 2 * $::size]
       pack [canvas .c -width $full_diameter -height $full_diameter]
       set border 2
       set diameter [expr 2 * $::size - $border]
       .c create oval $border $border             $diameter $diameter             -fill white -outline black
     }
     
     
     countdown 10
  
  
  
    这个倒计时显示了分针和秒针,如图 1 所示,其信息内容与前面的程序相同。
  
    图 1. 用 Tk 编码的模拟倒计时时钟的外观
  
   javascript:window.open(this.src);" style="CURSOR: pointer" onload="return imgzoom(this,550)">
    请记住:信息可以代替功能。您的最终用户对应用程序的内部状态越了解,他们对您的要求就越少。只要让您的程序显示它们是如何工作的,就可以解决许多明显的性能问题。
  
    排序很难
  
    前面介绍的是“简单的”课程;没有专门的编程背景知识的“平民百姓”也可以理解前面的段落。但是排序却属于“困难”这一类。而且这肯定是困难的:Donald Knuth 的一整卷有关计算的经典系列都致力于研究排序和搜索(请参阅参考资料以获取对 Knuth 撰写的 The Art of Computer Programming 的评论的链接)。
  
    结果证明,由于深层次的原因,排序是许多性能难题的核心所在。性能只是大规模计算的问题。人类一次只能理解几个方面,因此对于那些大得要花掉很长时间才能掌握的问题,我们采用的方式是用某种方式把它们组织起来或使其结构化。排序是这些方式中最常见的一种;可以对已排序列表进行二元搜索而不进行线性搜索。例如,这使得在纽约市电话号簿中进行的手工查找得到了简化,将一个百万级大小的问题简化成一个一百万的对数(也就是,大约为 14)级的问题。
  
    那是典型的排序工作;高级算法通常比低级算法快一千倍或更多。值得进行巧妙的排序。
  
    但是最聪明的做法就是避免进行整体排序,或者至少将排序仅限于在不受注意的场合进行。这在数据库管理系统中很常见;其它好处之一就是,它们允许构造“插入时”索引。需要排序结果时,可以直接从索引中读取。这一操作的代价就是:创建或插入元素的时间稍微有点长,但是如果应用程序具有典型的工作流的话,用户觉察不到这一点。
  
    Knuth 的参考资料描述了其它许多策略,比如用来在特定的条件下加快排序操作的“Boyer-Moore”或“Rabin-Karp”。统一这些策略的一个公共原则是:解决通用问题比解决比较特定的问题更容易。数学家对此很熟悉;如果将代数中的常见问题考虑成复数而不是实数时,那么这些问题就更容易处理,尽管复数算法较难。
  
    这就是我对“装饰-排序-去除装饰(decorate-sort-undecorate,DSU)”的看法。假定您拥有这样的数据集:
  
  "Jane Jones" -> 15,000
    "Dan Smith" -> 6,000
    "Kim Black" -> 40,000
  
  
  
    假定您需要按照他们的相关收入对人名进行排序。一个简单的方法可能是使用库入口点来进行排序,所提供的比较函数指出:“Kim Black”位于“Dan Smith”之前,因为 40000 大于 6000,等等。对于小问题而言,这既简单又有效。
  
    但是,为了更加有效 - 特别是每次对一百万项进行排序时,这在生物科学、金融和其它一些领域很常见 - 就要专门创建专用的“反向表”。尽管这项操作很耗时,但是它使排序变得非常快速。Python 的列表理解(list comprehension)使得这表达起来特别简洁;但是这一原则同样也适用于您手头可以使用的任何语言:
  
    清单 3. 用 Python 编写的最小的装饰-排序-去除装饰例子
  
  decorated = [ (revenues[name], name) for name in name_list ]
   decorated.sort()
   for (revenue, name) in decorated:
     print "%14s: %8d" % (name, revenue)
  
  
  
    对派生的数据集进行排序可以轻松地使整个操作加速一个或几个数量级。对于学生来说,计算“计算复杂度”似乎是较难的课题之一;但是当这些方法产生这样的加速时,它们的价值无庸置疑。
  
    快了 500 倍
  
    就在去年,独立顾问 Alex Martelli 取得了一次更惊人的突破。就象他在 Python 业务论坛(Python Business Forum)邮件列表中所叙述的那样,他已经实现了一个 XML 处理器,该处理器完成其任务需要 8 小时。这是无法接受的;最终用户不可能等这么久。他结合多次修正,结果使时间降到了一分钟,是他一开始所用时间的五百分之一。
  
    首先,他从内置 Python XML 库转而使用专用的 pyRXP 解析器。这个 pyRXP 与前面有关 XML 处理性能的 developerWorks 文章中提到的 pyRXP 是同一个解析器;请参阅参考资料以获取那篇文章的链接。与其它 Python 范围的 XML 引擎相比较,pyRXP 不仅使用较少的处理循环,还显著地减少了内存占用。这产生了巨大的差别,这原本是许多性能瓶颈 - 应用程序内存不足 - 的一个典型问题。即使形式上比较顺利的算法,如果它们使用了太多的内存以至于需要进行交换,那么它们也会慢得要命。这便是 Martelli 最大的问题。
  
    因此,评估备用产品或算法时,不仅仅要对它们表现出的吞吐量进行基准测试,还要测量它们对内存的影响。后者通常控制着生产应用程序的有效可伸缩性。而且,如果您决定尝试通过添加硬件来解决性能问题,这往往是明智的,那么就从考虑更多内存入手。增加主存花费并不昂贵,但往往会产生惊人的结果。
  
    因此,第二个简单原则是:添加更多的内存,并更好地利用您所拥有的内存。许多人,包括经理,似乎比较喜欢添加内存。
  
    磁盘驱动器值得怀疑  
  
    如果您渴求高性能,那么最后一个技巧就是怀疑磁盘驱动器。最近几十年海量存储器已经在密度和可靠性方面取得了惊人的进步,但是最近我看到至少有一些制造商在操作质量这一关把得太松。导致一些结果,包括一些单元彻底损坏,更多的单元则给出变化的结果。
  
    除非您仔细检查,否则您无法知道磁盘子系统实际的平均存取时间或吞吐量。当海量存储器配置为 RAID、SAN 或其它新式技术时,这会特别困难。特定的单元可能经常发生故障,但是在应用程序级别出现的唯一结果是在整体性能方面的各种怪异变化。例如,很容易就会发生这种情况:某个 RAID 单元实际上已经丢掉了一整个轴(spindle),因此特定程序消耗的大多数时间却都花在该单元的错误更正上。
  
    对此您该怎么办呢?许多补救方法都是可行的,但是它们的文档记录却少得可怜;我所知道的基本上是系统管理员私底下交流的一些“民间说法”。下面是一些要点:
  
    ◆ 购买您信得过的设备。在海量存储器这一领域,宁可多花些钱购买可靠的产品,也不要接受标价最低的商品。
  
    ◆ 别做第一个吃螃蟹的人。让别人先试用 SAN、最新一代的 SCSI、gigaether 存储器和其它产品吧。
  
    ◆ 在您购买的产品中寻找可测性。寻找一些可以监控其自身性能和环境(包括温度)的磁盘驱动器。保存这些记录,这样您就知道设备会发生什么情况。如果您的项目非常大,您会遇到有关磁盘的一些奇怪的问题。您最好留个有记录的基线,而不是在故障发生之后设法跟踪它。
  
    一些性能修正的构思甚至实现只需花几分钟;而另一些性能修正在成功之前则需要投入几天的功夫进行研究。要追踪到您的应用程序在速度上的确切限制,您需要准备处理困难和简单的性能问题。
  
    参考资料
  
    ◆ 这个有关需求获取的读物的页面源自 comp.software-eng Usenet 新闻组 。
  
    ◆ 较之Requirements Management Place 的实现,Cameron 更加注重其目的,但是他认为这很大程度上只是个人口味的问题。无论如何,参与需求获取的每个人(当然还有所有程序员和管理员)至少应当掌握一定的有关需求分析的参考资料和想法。
  
    ◆ “用并发来提高速度 ”除了介绍线程化之外,还介绍了许多有关多任务的知识。
  
    ◆ Cameron 有关 ksh 的个人页面 提供了一些链接,可以通过它们获取有关这一有趣且功能强大的语言的更多信息。请确保您安装了 ksh93;缺省情况下,许多 Linux 分发版都安装了 ksh88。
  
    ◆ Danny Yee 对 Donald Knuth 的The Art of Computer Programming 一书的评论 给出了有关后者对排序和搜索的权威性处理方面的一些背景知识。
  
    ◆ Brian Goetz讨论了他在使用 Java 语言的项目 中所见到的一些最常见的性能错误。
  
    ◆ “ ”提到了 pyRXP,对于有些应用程序而言这是受支持的最快的 XML 解析器。
  
    ◆ 旧的Server/Workstation Expert 中有一个精彩的关于存储器概念和实践的专栏,它仍然是有价值的读物。我认为当前任何有关对管理海量存储器难题的介绍都不能与之相提并论。
  
    ◆ 有关 IBM 存储器产品的完整内容,请访问 IBM TotalStorage 页面。
  
    ◆Tivoli Storage Management 页面对于获取有关 Tivoli 如何帮助管理驱动器、备份、SAN 等等信息方面,是个很好的起点。
  
    ◆ 在 专区查找更多针对 Linux 开发人员的参考资料。
  
    关于作者
  
    Cameron Laird 是独立咨询公司 Phaseit, Inc. 的全职开发人员,他经常就开放源码和其它技术主题撰写文章并发表演讲。他自己的大多数开发工作是在系统编程和服务器端方面。可以通过 claird@phaseit.net 与他联系。
  
  
  
  
    

原文转自:http://www.ltesting.net