2005年07月10日

 

Item 6: 如果你不想使用编译器为你产生的函数,就明确拒绝

不动产代理商出售房屋,服务于这样的代理商的软件系统自然要有一个类来表示被出售的房屋:

class HomeForSale { … };

每一个不动产代理商都会很快指出,每一件财产都是独特的——没有两件是完全一样的。在这种情况下,为 HomeForSale 对象做一个拷贝的想法就令人不解了。你怎么能拷贝一个独一无二的东西呢?最好让这种类似企图拷贝 HomeForSale 对象的行为不能通过编译:

HomeForSale h1;

HomeForSale h2;

HomeForSale h3(h1);               // attempt to copy h1 – should
                                  // not compile!

h1 = h2;                          // attempt to copy h2 – should
                                  // not compile!

唉,防止这种编译的方法并非那么简单易懂。通常,如果你不希望一个 class 支持某种功能,你可以简单地不声明赋予它这种功能的函数。这个策略对于拷贝赋值运算符不起作用,因为,就象 Item 5 中指出的,如果你不声明它们,而有人又想调用它们,编译器就会隐式地声明它们。

这就限制了你。如果你不声明拷贝构造函数和拷贝赋值运算符,编译器也可以为你生成它们。你的类还是会支持拷贝。另一方面,如果你声明了这些函数,你的类依然会支持拷贝。我们在这里的目标就是防止拷贝。

解决这个问题的关键是所有的编译器生成的函数都是 public。为了防止生成这些函数,你必须自己声明它们,但是你没有理由把它们声明为 public。相反,应该将拷贝构造函数和拷贝赋值运算符声明为 private。通过显式声明一个成员函数,可以防止编译器生成它自己的版本,而且将这个函数声明为 private,可以防止别人调用它。

通常,这个方案并不十分保险,因为成员函数和友元函数还是能够调用 private 函数。换句话说,除非你不定义它们。那么,当有人不小心地调用了它们,在连接的时候会出现错误。这个窍门–定义一个 private 成员函数却故意不去实现它–确实不错,在 C++ 的 iostreams 库里,就有几个类用此方法防止拷贝。比如,看一下你用的标准库的实现中,ios_basebasic_iossentry 的定义,你就会看到拷贝构造函数和拷贝赋值运算符被声明为 private 而且没有定义的情况。

将这个窍门用到 HomeForSale 上,很简单:

class HomeForSale {

public:
  …
private:
  …
  HomeForSale(const HomeForSale&);            // declarations only

  HomeForSale& operator=(const HomeForSale&);

};

你会注意到,我省略了函数参数的名字。这没有必要,只是一个普通的惯例。毕竟,函数不会被定义,极少有机会被用到,有什么必要指定参数的名字呢?

对于上面的类定义,编译器将阻止客户拷贝 HomeForSale 对象的企图,如果你不小心在成员函数或者友元函数中这样做了,连接程序会提出抗议。

将连接时错误提前到编译时间也是可行的(早发现错误毕竟比晚发现好),不要让 HomeForSale 自己去声明 private 的拷贝构造函数和拷贝赋值运算符,在一个特意设计的基类中声明。这个基类本身非常简单:

class Uncopyable {

protected:                                   // allow construction

  Uncopyable() {}                            // and destruction of

  ~Uncopyable() {}                           // derived objects…

private:

  Uncopyable(const Uncopyable&);             // …but prevent copying

  Uncopyable& operator=(const Uncopyable&);

};

为了禁止拷贝 HomeForSale 对象,我们必须让它从 Uncopyable 继承:

class HomeForSale: private Uncopyable {     // class no longer
  …                                       // declares copy ctor or
};                                          // copy assign. operator

在这里,如果有人——甚至是成员函数或友元函数——试图拷贝一个 HomeForSale 对象,编译器将试图生成一个拷贝构造函数和一个拷贝赋值运算符。就象 Item 12 解释的,这些函数的编译器生成版会试图调用基类的对应函数,而这些调用将被拒绝,因为在基类中,拷贝操作是 private 的。

Uncopyable 的实现和使用包含一些微妙之处,比如,从 Uncopyable 继承不能是 public 的(参见 Item 32 和 39),而且 Uncopyable 的构造函数不必是 virtual 的(参见 Item 7)。因为 Uncopyable 不包含数据,所以它符合 Item 39 描述的空基类优化条件,但因为它是基类,此项技术的应用不能引入多重继承(参见 Item 40)。反过来说,多重继承有时会使空基类优化失效(还是参见 Item 39)。通常,你可以忽略这些微妙之处,而且此处只是用 Uncopyable 来做演示,因为它比较适合做广告。在 Boost(参见 Item 55)中你可以找到一个可用的版本。那个类名为 noncopyable。那是一个好的 class,我只是发现那个名字有一点儿不(un-)……嗯……非自然(nonnatural)。

Things to Remember

  • 为了拒绝编译器自动提供的功能,将相应的函数声明为 private,而且不要给出实现。使用一个类似 Uncopyable 的基类是方法之一。


 

    在《程序员》2003年第12期的一篇访谈录中,Ruby的发明人Matz较详细地阐述了Ruby的设计思想及遵循的原则,其中大部分我都深以为然,特别是他高度重视语言设计过程中人的因素则更是让人激赏。不过他对正交特性的看法却让我颇为迷惑。

    正交(orthogonal)本来是一个几何概念,表示的是垂直相交,后来又被扩展到线性代数中,表示内积为零的两个向量之间的关系。线性空间中一组两两正交的非零向量被称为正交向量组,而在n维线性空间中,n个向量组成的正交向量组一定线性无关,因而也就一定是该空间的基。这意味着什么呢?这意味着该空间中的任意向量都可以由这一组正交向量经线性组合(linear combination)得出,因此我们可以通过这n个向量表达整个线性空间。

    在计算机科学领域,正交这个词通常表示基本元素之间互相独立与良好隔离,并且它们具备组合出一个完整"空间"的能力,而缺少其中任何一个都会丧失这种能力。如果把全面的逻辑运算想象成一种"运算空间"的话,那么not、and、or就是三个互不影响、互相不可替代的正交操作。由它们出发可以完成任意复杂的逻辑运算,但是无论少了谁,我们都会遇到某些无法完成的逻辑任务。再比如unix/linux系统提供了许多基本工具,其中每种工具都只专注于完成一种基本任务,并且基本任务也不互相重叠,所以工具的接口之间是正交关系,而用户则能通过管道等机制组合使用这些工具以完成各种复杂的任务。

    一门程序语言的功能可以看作是一个"功能空间",语言只需拥有一组数量合适的正交特性(这些特性各自提供了互相独立的基本功能)便可以通过组合它们来实现"功能空间"中的任何功能。很明显,这种设计是在不损失语言能力的前提下的最简方案。不过最简是否意味着最优则仁者见仁智者见智了。在这点上Matz认为:正交性是设计的工具,而不是设计的主要目标。但是接下来Matz用来证明他观点的例子则似乎有些问题。

    Matz举的例子是C++的缺省参数和函数重载。他认为这两种特性是正交的,可以同时使用,但是混合使用有时却会导致问题,这就需要人脑去猜测编译器的处理规则,而这种规则往往并不显然,所以这是一个有关正交性的反例。我同意同时提供缺省参数和函数重载并不是个好主意,尤其对初学者来说。比如下面的代码就会产生一个有些晦涩的编译错误:

void foo(int a = 2005)
{
   //do something using a
   …
}

void foo()
{
   // do something
   …
}

int main()
{
   foo(3); //OK
   foo(); //compile error:

          //ambiguous call to overloaded function

   return 0;
}

但是这两个特性是正交特性吗?实际上缺省参数功能可以非常容易地由函数重载特性来实现:

void foo(int a)
{
   // do something using a
   …
}

void foo()
{
   foo(2005);
}


int main()
{
   foo(3); //OK
   foo();  //OK,就好象调用一个缺省参数值为2005的foo函数

   return 0;
}

由此可以看出,缺省参数这个特性在功能上是冗余的,与函数重载不可能是正交关系。这个例子恰恰演示了某些不遵循正交性的设计会带来什么样的不良后果。不过能够做一样事情和能方便地做一样事情是有区别的,人们常常会为了现实的好处而牺牲理论的优美,个人认为这也是为什么C++会提供缺省参数这么一种语法糖衣(syntactic sugar)。

    Matz用一个错误的例子来证明自己的观点,给我留下了很大的问号。希望下次能看到他用真正有说服力的论据来阐明对正交性的看法。

       GCC(GNU Compiler Collection) 是 GNU(GNU’s Not Unix) 计划提供的编译器家族,它能够支持 C, C++, Objective-C, Fortran, Java 和 Ada 等等程序设计语言前端,同时能够运行在 x86, x86-64, IA-6 4, PowerPC, SPARC 和 Alpha等等几乎目前所有的硬件平台上。鉴于这些特征,以及 GCC 编译代码的高效性,使得GCC 成为绝大多数自由软件开发编译的首选工具。虽然对于程序员们来说,编译器只是一个工具,除了开发和维护人员,很少有人关注编译器的发展,但是GCC的影响力是如此之大,它的性能提升甚至有望改善所有的自由软件的运行效率,同时它的内部结构的变化也体现出现代编译器发展的新特征,所以 2005年4月20日,GNU 组织发布的 GCC 4.0 引起了广泛的关注。那么这次 GCC 从 3.4.x 直接跃迁到 4.x 的主版本变化到底有什么值得关注的呢?

       我们可以从不同的角度看待 GCC 的这次变迁,对于普通程序员来说,关注的主要是GCC 的前端支持情况以及编译性能的变化。

1. GCC 4.0 的前端支持

GCC 的开发者和使用者当中,大多数人都是 C 或者 C++ 的用户,所以 GCC 对Fortran 语言支持不足也不令人奇怪。但是,这并不代表 Fortran 是无足轻重的,事实上,开发商业的 Fortran 编译器的公司要远远多于开发 C 和 C++ 编译器的公司。

在科学计算和工程应用领域,程序员们仍然在频繁使用 Fortran 程序,同时,大量的经过长时间考验的函数库也为Fortran语言的数值计算提供了强有力的支持,所以,在一些"超级计算机"(supercomputer)上,Fortran仍然是绝大多数应用的首选语言。

然而,在GCC 4.0发布之前,如果不想购买商用的Fortran编译器,那么程序员们的唯一选择就是GNU的g77编译器。但是g77编译器是一个相当陈旧的技术,很多Fortran语言的新特性都不能支持,比如流行的Fortran 95,它能够支持的模块化编程,并行处理和数组操作等等,g77编译器基本上都无法支持。

这次GCC 4.0发布时推出了支持Fortran 95语言前端的编译器gfortran,它已经能够大大超越g77编译器,支持Fortran 95标准中的很多新特性,虽然gfortran还有一些缺陷,比如不能支持自动并行化(automatic parallelization),不能支持Fortran 2003中的面向对象特性等等,它已经给了Fortran程序员除了商业编译器和g77以外一个更好的选择。

2. GCC 4.0的编译性能

编译器的性能主要可以从三个方面来考查:

1. 编译时间(compile time),指编译器编译一个源程序得到目标代码所需要的时间。

2. 目标代码的大小(object size),编译得到的目标代码当然是越精悍越好了。

3. 目标代码运行时间(run time),运行时间体现了速度和效率。

这里,作者没有亲自测试和实验,引用了Scott Robert Ladd的《GCC 4.0: A Review for AMD and Intel Processors》文章中的一些实验结果。这篇文章引起了比较大的反响,其实验结果和结论也得到了广泛的认可,如果对Scott的具体测试采用的软硬件平台和工具方法感兴趣,原文可以在http://www.coyotegulch.com/reviews/gcc4/index.html看到。

Scott使用AMD和Intel的两款处理器:64位的Opteron处理器和32位的Pentium 4处理器,分别针对GCC 3.4.3和GCC 4.0来进行测试。他选用了POV-Ray 3.6.1, LAME 3.96.1, SciMark 2.0和Linux 2.6.11.8作为benchmark来进行测试,分别记录了GCC 3.4.3和GCC 4.0在ADM Opteron和Intel Pentium 4下的编译时间、代码大小和代码运行时间进行比较,具体的实验结果请参见原文。

这样,根据这些实验数据,我们可以给出一个粗糙的结论,在编译性能方面,GCC 4.0似乎不如GCC 3.4.3,因为在很多时候,GCC 4.0的编译时间、代码大小以及代码运行时间全面高于GCC 3.4.3。这样的结果看似出人意料,GCC这次大的版本变化就是因为引入了新的优化框架,怎么会编译性能有所下降呢?这主要是因为:首先,这是一次主版本变化,我们可以理解巨大的变化带来的性能损耗;另外,更主要的是,GCC新的优化框架的潜力尚未完全发挥出来,这一点,在我们文章结束的时候,读者会有更深的理解。
. GCC 4.0 的内部结构变化

GCC 遵循 GPL 协议,是开放源代码的,其开发过程也是完全开放的,任何人都可以对 GCC 的发展作出贡献,因而 GCC 特别适合用于学习和研究编译器。对于学习和研究编译器本身来说,GCC 内部的结构变化显然更吸引人。

目前 GCC 发行维护者 Mark Mitchell 在接受 internetnews.com 网站采访时这样说到:"毫无疑问地,GCC 4.0 中最引人注意的特性以及为何把 GCC 的这个版本称作 4.0 而不是 3.5,就是因为其新的优化框架(optimization infrastructure)。大体上来说,GCC 以前的版本的代码优化工作主要在底层机器指令级别进行的。不幸的是,到了底层的时候,很多信息已经丢失了,因此,GCC 4.0 在更接近输入高级语言程序的级别上做了很多优化工作。"

Mark Mitchell 所说的 GCC 新的优化框架主要就是指 Tree SSA(Static Single Assignment),Tree SSA 经过长时间的独立开发,最终整合进了 GCC 的主流(mainstream)中,可见这种设计是意义非凡的。Tree SSA 是什么?为什么要采用 Tree SSA? 使用了 Tree SSA 的 GCC 有什么不同?新的 GCC 编译和优化框架是什么样的?等等,这些将是本文探讨的主要问题。

这部分文章内容是这样组织的:首先回顾 GCC 4.0 版本之前的编译流程,以便进行对比;接下来介绍 GCC 4.0 的编译流程以及新的优化框架结构,这里先介绍 GENERIC Tree和 GIMPLE Tree,SSA 形式等基本概念,在读者对这些概念和理论有了一定的了解之后,再介绍 GCC 中是如何实现 Tree SSA 框架结构的。

3.1 GCC 4.0 之前的编译流程

这里有必要回顾一下 GCC4.0 之前的版本进行代码优化的框架结构,以便进行对比分析。GCC 的前端在接受了输入的源程序之后,经过分析器(parser)处理得到 Parse Tree(通常是一种抽象语法数,AST, Abstract Syntax Tree),根据这个 Parse Tree 生成程序的RTL(Register Transfer Language)表示,然后在 RTL 表示的基础上进行优化处理,然后生成相应的目标代码,如下图 1 所示。

但是 RTL表示是一个相当接近底层的表示,也就是说它更接近目标代码,适合进行目标相关的优化工作,比如寄存器分配等等。然而,很多的优化转换工作需要更高层的程序信息,比如数组引用、数据类型、控制流结构等等,这些很难用 RTL 表示,或者无法用 RTL表示。


图1 GCC 4.0之前版本的代码编译流程和优化框架



3.2 GCC 4.0 的优化框架(Optimization Infrastructure)

提供一个可移植性强、跨平台以及编译高效代码的编译器,是 GCC 一贯追求的目标,为了使 GCC 能够获得更好的编译性能,高层程序信息级别的优化工作是必须的。Tree SSA设计的主要目的就在于此,它既与前端语言无关,又与后端目标无关,而且能够提供在 RTL表示层很难或者无法进行的高级分析和转换。

Tree SSA 起先是作为 GCC 的一个分支(branch)进行独立开发的,经过两年多的努力开发,终于在 2004 年 5 月 13 日进入了 GCC 的主流版本。在 GCC 的 SSA for Trees 分支的网页上明确说明了,这个项目的目的就是构建一个对基于 SSA 形式的树的优化框架。在学习编译原理的时候我们知道,编译器通常会使用树的形式来描述程序,GCC 也是这样,在接受了输入的源程序后,GCC 驱动其相应语言的前端分析器(parser),处理得到一个 Tree。从图 1 可以看到,4.0 版本之前的 GCC 几乎是立即把这些 Tree 转换成了 RTL 表示。那么现在 GCC4.0 的 Tree SSA 优化框架就是在前端生成 Parse Tree 之后,把这些 Tree 转换成基于 SSA 的 Tree,对这些 SSA 形式的 Tree 进行高层次的优化,然后才把 Tree 转换成 RTL 表示。

3.2.1 GENERIC Tree 和 GIMPLE Tree

这个框架结构看起来比较简单,而且 GCC 的 Parse Tree 能够提供足够的信息来实现SSA,但是在真是实施的时候,还是有很大的困难的,最主要的两个困难是这样的:

1. GCC 中的树没有统一的表示形式,每一个前端都定义了自己的树。这就意味着要得到基于SSA形式的树必须要对每种前端分析生成的树都进行处理。

2. GCC 前端得到的 Parse Tree 的复杂度是无法估量的。把这些树转换成基于 SSA形式的树需要进行复杂的处理工作。

为了解决以上两个问题,GCC Tree SSA 分支的开发小组引入了 GENERIC Tree 和GIMPLE Tree 两个概念:

1. GENERIC Tree 是特意创造出来的 GCC通用的树的表示形式,它能够表示不同的前端所需要的所有的结构,而且又能够去除语言相关性。

2. GIMPLE Tree是取自GENERIC Tree和SIMPLE两个短语的。因为GENERIC Tree的复杂性导致实现SSA形式的困难,需要把GENERIC Tree进行简化,这种简化的GENERIC Tree就称之为GIMPLE Tree。

好了,了解了这些内容之后,我们可以看看 GCC 4.0 的编译流程和优化框架,如下图2所示:


图2 GCC 4.0 的编译流程和优化框架



3.2.2 Single Static Assignment Form 的基础介绍

到现在为止,我们基本搞清了新的 GCC 的编译过程,也大概了解了所谓的新的 Tree SSA 优化框架。上面提到的 GENERIC Tree 和 GIMPLE Tree 都是为了实现 SSA 而做的准备工作,那么 SSA 本身究竟是什么?为什么 GCC 要把 Parse Tree 转换成基于 SSA 形式的 Tree 再做优化工作呢?为弄清楚这些问题,我们有必要多花一些篇幅对SSA的基本概念和理论做一些介绍。

SSA 的全称是 Static Single Assignment,直译过来就是静态单一赋值,它是IBM公司在上个世纪 80 年代研究的成果。从前面的讨论可以看出,Tree SSA 与 RTL 一样,也是一种中间表示形式,不过相比 RTL 要更高层一点。SSA 形式是一种相对而言比较新颖的中间表示形式,早期的讲编译原理或者编译器的课本中大多没有提及。

简单的说,SSA 形式就是每个变量只能被赋值一次。这样,非 SSA 形式的程序在转换成 SSA 形式的时候,其中的部分变量就会被分割成很多版本,通常使用下标来表示这些不同的版本。下面举一个简单的例子来说明,如下图 3 所示:


图3 程序的非 SSA 形式和 SSA 形式



上图中所示的代码片段,由于 y 变量被赋值两次,所以在转化成 SSA 形式的时候,y变量被分割成两个版本 y1 和 y2,这样就保证了每个变量仅仅被赋值一次。

由于 SSA 形式中每个变量只能被赋值一次,那么 SSA 形式就能有效地把程序中所操作的数值和这些数值的存储位置这两者分开,这样就能方便一些优化工作。比如我们刚才看的图 3 中的代码片段,我们可以通过肉眼分析发现在非 SSA 形式中的第一条语句y := 1是一条无效的冗余语句,真正决定 y 变量值的是第二条 y := 2 赋值语句。那么在代码优化的时候,第一条语句就应该被删除掉。但是这是我们人工发现的优化结果,如果想要编译器来完成这个优化工作,需要进行复杂的分析,在编译原理中称之为"定义可达性分析"(reaching definition analysis)。而在 SSA 形式中,显然,做出这样的优化决定则无需进行太多分析。

这只是 SSA 形式诸多优点中的一个而已,使用 SSA 形式可以利用更多的编译器优化算法或者是提高这些算法的效率,比如 constant propagation, sparse conditional constant propagation, dead code elimination, global value numbering, partial redundancy elimination 以及register allocation 等等。这里涉及太多编译理论和算法,本文不作详细讨论。

SSA 形式具有上文所述的优点,当然也会有其复杂和困难的地方了,下面我们通过一个稍微复杂点的程序片段来说明把非 SSA 形式的程序转换成 SSA 形式程序的时候有些什么需要考虑的问题:


图4



图 4-a 所示的非 SSA 形式的程序在转换成了图 4-b 所示的 SSA 形式的程序后,有一个问题很难解决,就是图 4-b 中最下面程序块中 y 的值无法确定。因为在此之前有一条选择语句,导致程序控制流产生了分支,此时 y 的值可能是 y1 也可能是 y2,这是由程序具体执行时经过哪一条控制流来决定的。处理的方法是在最后一块程序片段之前加上一个 Φ 函数,定义一个新的 y3,并从 y1 或者 y2 中选择一个适当的值赋给 y3,如图 4-c 所示。

推而广之,在将非 SSA 形式的程序转换成 SSA 形式后,如果某个变量被分割成了 n个不同版本的变量后,在某一点需要会合,那么在这个会合点 (joint point) 就需要加入一个 Φ 函数来确定应该选择这 n 个不同版本的变量中的某一个值。这样问题就转变成了如何计算这个 Φ 函数以及确定在程序中的什么位置应该插入 Φ 函数,这个可以使用 dominance frontiers 来进行计算,关于如何高效计算这个 Φ 函数的算法研究本文不作深入讨论。
3.2.3 GCC 4.0 中的 Tree SSA 框架的设计和实现

至此,我们已经比较清楚的了解了什么是 SSA,SSA 形式有什么好处,如何将非 SSA表示转换成基于SSA形式的表示以及这个过程中需要解决的主要问题等等,本文将不再对SSA进行深入的讨论了,回到我们的主题,继续讨论GCC 4.0 的 Tree SSA 优化框架是如何设计和实现的,它是怎样把 Tree SSA 融入到 GCC 的编译过程中去的。

回顾上图2表示的 GCC 4.0 编译流程,在得到 GIMPLE Tree 之后,经过 GIMPLE optimizer 和 GIMPLE expander 的处理,得到了程序的 RTL 表示。其实把程序转换成SSA形式和在此基础上进行优化就在这个过程中完成的,下面我们就来具体看看 GCC 4.0 是怎么做的。在 GIMPLE 和 RTL 之间是树优化 (Tree Optimization) 的过程,如下图 5 所示。图 5 中所示的树优化器 (Tree Optimizer) 主要完成这几个功能:

1. 生成一个控制流转换图 CFG(Control Flow Graph)。

2. 将 GIMPLE Tree 转换成基于 SSA 形式的 Tree。

3. 进行 Tree SSA 的优化。

4. 将优化后的基于 SSA 形式的 Tree 转换成 RTL 接口所能识别的非 SSA Tree 的形式。


图5 GCC 4.0 的 Tree Optimizer 结构



结合 GCC 4.0 代码中的部分源文件来说明:图 5 中这个 Tree Optimizer 的行为是由tree-optimize.c 文件驱动的,tree-ssa.c, tree-into-ssa.c 以及 tree-outof-ssa.c 负责完成 SSA 形式的转换、验证以及其他需要与 SSA 形式交互的功能,建立和管理控制流转换图 CFG 的工作由 tree-cfg.c 完成。不同的树分析和优化功能分别在相应的源文件中独立实现,这些分析和优化函数必须向 GCC 注册,然后才能由 tree-optimize.c 进行驱动和管理,结合图 5来看,SSA pass1, SSA pass2 … SSA passn 代表了实现各种不同功能的 SSA 分析器,他们向 Tree Optimizer 注册,在编译时刻 Tree Optimizer 根据编译选项等等选择相应的SSA分析器进行优化或者其他处理工作,并保证在基于 SSA 形式的处理完成之后将 SSA 树转换成非 SSA树,交给下面的 RTL 模块处理。

至此,GCC 4.0 的 Tree SSA 优化框架结构应该比较清楚了。读者可能会注意到,文中多次提到Tree SSA是一个优化框架结构(Optimization Infrastructure),为什么说是一个Infrastructure 呢?其实把 Infrastructure 称作框架结构或许并不贴切,更精确地说 Tree SSA是 GCC 提供的一种进行优化工作的基础设施。图 5 中已经体现出了这一点,Tree SSA 的分析和优化工作不是一成不变的,具体选择哪些优化功能和算法是由Tree Optimizer选择驱动的,而且这个Infrastructure是相当灵活的,我们可以很方便的加入一个 SSA 分析器,只需要如下步骤:

1. 创建一个 struct tree_opt_pass 类型的全局变量。

2. 在 tree-pass.h 头文件中为这个新的分析器添加一个外部声明(extern declaration)。

3. 调用 NEXT_PASS 把这个新的分析器加入到 tree-optimize.c: init_tree_optimization_passes 序列中。

所以这种 Infrastructure 是一个相当灵活和方便的设计,使得我们可以便利地加入新的SSA分析器,或者使用更高效的算法来重新设计完成一些优化功能等等。

4. 总结

GCC 4.0 发布以来得到了引起了广泛的关注,新的 gfortran 前端给 Fortran 程序员带来了福音,但也有很多不尽如人意的地方,比如编译性能的损耗,以及向后兼容问题。比如KDE(K Desktop Environment) 开发小组在发现 GCC 4.0 无法正常编译 KDE 后,迅速的抛弃了 GCC 4.0。而且,出于稳定性的考虑,GCC 3.x 版本的开发仍然在进行中。这些似乎给 GCC 4.0 的前景笼罩上了一层阴影,但我们应该容忍一点,给 GCC 4.0 一些信心,向后兼容的问题应该是能够解决的。

至于编译性能的问题,通过我们这篇文章的讨论,我们可以看到 GCC 4.0 Tree SSA 的优化框架结构并没有充分发挥出其潜能,还可以增加和改进更多 Tree SSA 的优化工作,假以时日,GCC 4.0 的编译性能会得到提高的。

总的来说,这次 GCC 从 3.x 版本跃迁到 4.x 版本更像一个进化 (evolution) 的过程,而非一个革命性 (revolution) 的过程,它采用的 Tree SSA 优化框架代表了编译器发展的方向,我们应该关注 GCC 4.0 的进一步发展。

关于作者

王逸,南京大学计算机科学与技术系在读博士生,对软件安全、Linux、编译器等比较有兴趣,目前主要关注的是隐蔽信道分析。

2005年06月08日

 

===========================
choice.com 应用推广三部曲
Will Sort – 2005/04/18
Updated:2005/05/14
===========================

序言
==============================================================================

  在 choice.com 出现之前,用户控制批处理运行过程的途径通常被限制在命令行参
数和环境变量上,虽然有个别的编程者会使用debug 的汇编脚本或者 date 、time等一
些可以接受键盘输入的特殊命令来实现运行中接受用户输入的参数,但是它们不是使用
方法很复杂,就是适用的场合很有限,或者还有其他一些诸如此类的问题。

  但是,自从 choice.com 出现后,菜单选择立即成为批处理程序交互的重要手段。
程序中开始大量出现由 echo/choice/if errorlevel goto 所组成的结构范式。然而,
随着 choice.com 不断深入的应用,大家在享受它所带来的便利时,也渐渐无法忍受它
所带来的不便。

  这不便中最主要的,就是 choice.com 仅支持字母和数字等可以指定候选字符的按
键,而对经常会在菜单中用到的回车、空格、Esc 、光标移动、翻页等特殊按键却无法
用字母进行指定,也就无法在菜单中使用它们。

  然而,事实真的如此吗?不妨看看下文会给我们带来哪些收获。
==============================================================================

===========================
三部曲之一:自欺欺人
Will Sort – 2005/04/18
Updated:2005/04/23
===========================

==============================================================================

  首先,让我们进入命令行,当出现命令提示符后,尝试敲入并执行下面的语句:

  choice /n/s/c:Q  Press [PageDown] to quit:

  当按下回车后,我们看到了提示,然后根据提示按下 PageDown 键,然后我们发现
了什么?程序运行结束了!并且在提示语的后面出现了一个大写的字母 Q。


  哦,太神奇了,我发现了新大陆!

  不,这只是新大陆的一个小岛而已。事实上,大部分的特殊按键在 choice.com 中
都对应着一个字母、数字或者其他可以显示出来的字符。

  以下是一些常用的特殊按键对应字符的简明列表:
  ————————————————————————
  ; [F1]   < [F2]     = [F3]    > [F4]    ? [F5]
  @ [F6]   A [F7]     B [F8]    C [F9]    D [F10]

  R [Insert]          G [Home]            I [Page Up]
  S [Delete]          O [End]             Q [Page Down]

                      H [UP Arrow]
  K [Left Arrow]      P [Down Arrow]      M [Right Arrow]

  27 [Esc]            8 [Back Space]
  ————————————————————————

  我们可以再根据上表中提供的字符做新的尝试:

  choice /n/s/c:; Press [F1] to quit:

  哈,又成功了!再试:

  choice /n/s/c:< Press [F2] to quit:

  啊,出错了!找不到文件,怎么回事?
  嗯,你用的小于号是系统定义的文件重定向符号,它把 Press当成文件重定向了。
  那怎么办?
  简单,在小于号两边加上双引号,系统不认识小于号就行了,就像这样:

  choice /n/s/c:"<" Press [F2] to quit:

  哈,真是这样呢!
  那么,你可以再多试一些练习一下。
  好,我试,我试,我试试试。

  choice /n/s/c:= Press [F3] to quit: —〉成功!
  choice /n/s/c:> Press [F4] to quit: —〉失败!
  哦,不对,改:
  choice /n/s/c:">" Press [F4] to quit: —〉成功!
  choice /n/s/c:? Press [F5] to quit: —〉成功!

  好了,我会用了,但是,这到底是怎么回事呢?

  原因嘛,深究起来比较复杂;简单的说,是因为这些特殊的按键会产生一种系统可
以识别的扩展编码,一般是由0 或者224 和另一数字编码组成。而 choice.com 接受这
些按键时,会将编码中认为无效的0 或者224 过滤掉,而剩下后面的数字编码,而这部
分数字又恰好会被误认为是某个可显示字符的 ASCII编码。以开始提到的 PageDown 来
说,它的扩展编码就是“0;81”,而与此相对应的,字母 Q的 ASCII编码也是81,于是
choice.com就将这 PageDown 和 Q这两个键混淆了。其它“F1”和分号、“F2”和小于
号等等都是类似的原因。这也是本节篇名“自欺欺人”的由来。
  等等,你说什么“扩展编码”,那是什么东东?
  “扩展编码”实际上是 MS-DOS 从键盘读取按键时,经由键盘扫描码略加修改转换
后的一种按键的内部编码,通过这种转换后的编码形式,它可以兼容地处理 ASCII字符
键和非 ASCII字符键(就是在 ASCII码表中找不到对应编码的按键)。
  呵,似懂非懂啊,还是算了吧。

  等一下,表里面最后一个 Esc ,前面怎么是个二位数?

  哦,你观察的倒真仔细。是这样的, Esc还有 BackSpace,它们与 PageDown 有所
不同。那就是 Esc和 BackSpace它们都是 ASCII字符键,也就是说它们在 ASCII码表中
可以找到对应的编码,正是表中所列的 27 和 8。而这个数字同时也是它们的扩展编码
只有一串数字,没有0 或者 224打头,这就意味着,它们在独立地在 ASCII码表中占用
两个位置,也就没有可能冒充其他的字符。
  同时,因为他们在命令行和各种编辑器中都被赋予了特殊的含义(命令行中 Esc用
来重写命令, BackSpace用来向前删除字符),所以就无法通过按键直接输入字符来指
定候选键。但是,实际上 choice.com 是可以接受 27 和 8这样的编码的,所以我们只
要想办法将这两个键变成字符输进命令行就可以完成任务了。

  唔,那么怎么把它们输进去呢?
  这就要请出我们命令行工具的不老悍将—— edit 了。
  edit,就是那个DOS 下面打字的软件吗?

  对,准确的说, edit 是一个文本编辑器;它有个很实用的功能,就是用 Ctrl+P
输入特殊字符。比如,我们要把 Esc当作字符输进去,就可以先按下Ctrl+P,放开后再
按Esc ,这时一个向左的小箭头就会出现了,它就是Esc 在 ASCII码表中对应的字符;
同样,按下 Ctrl+P 、 BackSpace后就会出现一个带圆孔的长方块,那就是 BackSpace
对应的字符了。

  输进去以后怎么办呢?
  这样,我们可以把上面的 choice.com 语句写进批处理程序中,然后将 /c 后面的
候选字符换成我们刚刚学会输入的那两个字符,就像下面这样:

  @choice /n/s/c: Press [Esc] or [BackSpace] to quit:

  将上面这个句子保存为批处理程序,比如“Esc-BkSp.bat”。然后,在命令行中执
行它。然后按 Esc 和 BackSpace 试试看。

  唉,终于成功了!好累啊,不过也蛮有成功感的。

  嗯,到这里,我们的第一部曲也该就此结束了。现在,我们应该可以解决一些以前
用 choice.com 时无法解决的问题。但是,仍然还有一些问题,比如我们一直没讲到回
车键怎么在 choice.com 中使用,它无论在命令行还是在批处理中都被当作了语句结束
的标志,因而始终无法作为候选字符使用,还有很多其它的一些类似的特殊按键(例如
Tab )因为同样的原因而无法使用。
  而这些,都将留在第二部曲“瞒天过海”中探讨和解决。

  最后,作为本节内容的附赠品,一篇详细标明更多特殊按键扩展编码以及替代字符
的列表以附件形式列于其后,附件中还包括 EscBksp.bat和 PgDw.bat 用以测试是否支
持 [Esc]/[BackSpace]/[PageDown] 等特殊的按键。在 EscBksp.bat中使用了自动获取
 [Esc]/[BackSpace]这两个特殊按键对应字符的方法。
——————————————————————————

编后语:

  其实,就我自己而言,很不习惯也很不喜欢写这样风格的文字,短短的一篇技术短
文硬是被拖拽成了又长又臭的搞笑散文 :< 没办法,为了照顾大多数初学者,只好牺牲
一下我的个人品味了 :) 不过,在后两节文字中,随着技术含量的增加,同时也是为了
照顾我的个人情绪,我将会适当的提高文章的理解水平底线,低于此线的朋友们,在此
先说声抱歉了。
==============================================================================

===========================
三部曲之二:瞒天过海
Will Sort – 2005/04/24
Updated:2005/04/30
===========================

==============================================================================

  在上节中我们留下一个题目,那就是如何让 choice.com 接受并正确处理回车键以
及其他一些系统保留的特殊键。我的答案是键盘重定义,就是将键盘上的特殊键重定义
为我们可以利用的常规键,比如字母或者数字。

  这需要使用从 MS-DOS 时代起便出现的一个设备驱动程序 ANSI.SYS 。它是为了支
持美国国家标准学会(ANSI)所制订的一套关于键盘和屏幕控制的标准而设计的,具有
相当丰富的键盘和屏幕控制功能,详细内容可以参阅附件中的文档 ANSI_SYS.TXT 。利
用它所提供的重定义键(实际上是重定义键的扩展编码)的功能,可以将回车重定义为
字母‘y’,从而实现我们的初衷。但是要使用这个功能,我们需要预先做一些准备。

在 MS-DOS 和 Win98 中准备使用 ANSI.SYS
————————————–

  在 MS-DOS 和 Win98的命令行环境中,我们使用 ANSI.SYS 需要通过一个启动配置
文件来加载它,那就是 config.sys ,它是一个纯文本文件,通常位于系统所在盘的根
目录,可以用“记事本”或者上节提到的“edit”来编辑它。我们可以在这个文件的最
后添加一行如下的文字:

  device=c:\Windows\Command\ANSI.SYS /x

  其中的 C:\Windows\Command 可能需要替换成 ANSI.SYS 文件实际所在的目录。比
如你是 MS-DOS 的用户,那么 ANSI.SYS 很可能在 C:\DOS 下。

  编辑完成后,重新启动计算机就可以在 MS-DOS 或者 Win 98 的命令行环境下使用
 ANSI.SYS 所提供的各种功能特性了。

在 Win2K 和 WinXP 中准备使用 ANSI.SYS
————————————-

  在 Win2K和 WinXP的命令行环境中,已不再使用 config.sys ,但是可以在系统路
径下的 System32 目录中找到一个替代品,那就是 config.nt,它用于配置 MS-DOS 模
拟环境(command.com)。它也是纯文本文件,所以也可以用记事本打开并编辑它。我
们需要给它加上与 MS-DOS 和 Win9x 系统相类似的文字:

  device=%SystemRoot%\system32\ANSI.SYS /x

  其中的 %SystemRoot% 是个全局的环境变量,我们可以直接使用它引用系统路径,
而不需要人为地修改它。

  与 MS-DOS 和 Win9x 的命令行环境不同的是,我们还需要修改 config.nt 文件中
的一个细节,那就是将“REM DOSONLY”一行中的“REM”去除。这样做的原因是,ANSI
是一个基于 MS-DOS 的终止并驻留(TSR)内存的程序,它需要一个纯粹的运行 MS-DOS
程序的环境,否则就会无法正常的使用。而 DOSONLY正是创建这个环境的必要开关,所
以我们去掉它前面的注释命令,以使它在 config.sys 中生效。

  所有的设置修改完成后,就可以立即在运行中输入 “command”(不是 CMD),此
时有 ANSI 支持的命令行环境就被启动了。

开始体验 ANSI.SYS
—————–

  准备了这么久,我们终于可以使用 ANSI.SYS 带给我们的特性了,请将下面的代码
拷贝并保存到批处理文件中。

  @echo off
  echo [13;'y'p-------- Start -------------
  choice Press Enter or Y to quit: /c:y /n
  echo --------- End --------------
  pause

  然后在我们刚才启动的有 ANSI 支持的命令行环境下,执行这个批处理,看到提示
后我们按回车键:此时,程序运行结束,并在提示语后出现一个大写字母 'Y'。

  我们分析一下程序,第一行是将命令回显关闭,第二行就用了 ANSI 的重定义键功
能, echo 后面的向左箭头就是上节中提到的 Esc 键的 ASCII 字符,它和'[' 联合使
用,标志着随后的序列开始使用 ANSI 的功能,它们不会被 echo ,而会被 ANSI 发现
并转变其含义,因此这个序列被称为转义序列,Esc 也被称为转义字符。第三行是供我
们测试的 choice 语句,其中仅仅定义了一个候选字符‘y’;第四行是恢复回车原来
的定义;第五行实现暂停。

  而转义序列“13;'y'p”的意思,我们应该可以猜得出来,那就是将回车重定义为
字母‘y',回车的表示正是上节中提到的扩展编码,而字母‘y'被单引号括了起来,表
示它是一个 ASCII 字符,而非字符的编码;至于紧跟其后的字母‘p’正是重定义键的
功能标志。而后面的 -Start- 并不是转义序列的一部分,将会原样输出。

  我们也可以实现更多地重定义按键,比如制表符键、光标键、删除键、回删键等,
详细的代码请参阅附件中的 Remap2.bat ,我们可以在命令行中执行它,然后测试一
下我们的按键和输出结果是否一一匹配。

如何在我需要时才使用 ANSI.SYS
-----------------------------

  以上对 config.sys 和 config.nt 的修改将会使以后运行所有的 MS-DOS 程序都
可以享用 ANSI.SYS 所带来的特性,但同时也会始终占用命令行环境下约 4K 的内存空
间。我们可能并不想这样,因为我们并不经常需要 ANSI.SYS 的支持,当我们不需要它
时,总是不希望它浪费我们宝贵的内存空间。

  要解决这个问题,我们可以在系统启动以后再加载它。在 MS-DOS 环境中,当需要
使用 ANSI.SYS 的时候,一般使用 device.exe 或者其他有相似功能的第三方程序在命
令行加载 ANSI.SYS ,不需要的时候也同样可以卸载它。

  在 Win9x的命令行环境中,我们多了一种选择,那就是为需要使用 ANSI.SYS 的程
序编辑自己专属的启动配置文件:在程序的右键“属性”菜单中,选择“程序->高级->
MS-DOS方式->指定新的 MS-DOS 配置”,然后在 config.sys 的对话框中添加 device
语句即可。但是这个方法的缺点是启动和退出这个程序时,都会重新启动系统以进入新
的命令行环境。而在WinXP 中,没有了 Win9x的缺点,我们可以为需要使用 ANSI.SYS
的程序创建一个指向 MS-DOS 程序的快捷方式(.PIF文件):在程序所在文件夹的空白
处点击右键,选择“新建->快捷方式”,在文本框中填入 command,然后一路回车,在
出现“MS-DOS方式”之后,在右键“属性”菜单中,修改程序名为 Remap2.bat ,修改
启动配置文件为我们自己制作的启动配置文件,比如附件中的 Remap_XP.PIF 使用的便
是 %SystemRoot%\System32 下的 ANSI_SYS.NT(对应CONFIG.NT)和 ANSI_BAT.NT(对
应AUTOEXEC.BAT),它们可以在附件中 ANSI_XP 文件夹中找到。

  这一节我们讲述了如何通过 ANSI 的重定义键功能来实现 CHOICE 接受任意按键,
除了极个别的按键之外(如 Ctrl+Break 或者 Ctrl+Alt+Del ),我们已经可以处理各
种常用的特殊键。下一节,我们将涉及 choice 的另一个应用需求,那就是默认按键的
优化。
------------------------------------------------------------------------------

编后语:

  可能对于很多人来说,本节是比较枯燥的一节,因为它的文字量远远大于代码量,
以至于让很多兄弟少了很多实践锻炼的素材,也许将 Remap2 贴上是个办法,但我并不
愿意如此做。下一节,这种情况就会完全反过来了,请大家也有所准备。

  另外,本节的内容很久前就筹划好了,大概是在回复了“联合 DOS论坛”的一位站
友关于 choice.com 接受回车键的问题帖之后。只是后来觉得这种方法有很多的缺陷,
我们必须预先配置命令行环境,然后才能在这个环境中运行相应的程序;而对于 XP 等
NT类环境来说,这种配置无法全局有效,对 config.nt的修改只能影响 command.com,
而无法影响直接点击运行的批处理程序,因为它们默认是启用 cmd作为命令解释器的。
直到不久前发现 XP 中也可以借助 .PIF 文件进行启动配置,我才下定决心将此节继续
完成并公布于众。

  因为花了很多时间编写并测试一些代码实例,以及其他一些无关的工作,所以本节
出得稍微晚了一些。如果没有意外,第三节可能会快一些,因为我将不会做太多的文字
说明。
==============================================================================

===========================
三部曲之三:借花献佛
Will Sort - 2005/05/03
Updated:2005/05/15
===========================

==============================================================================

  在上一节中,我们基本完成了 choice 接受任意按键的任务,现在我们将目标定位
在 choice 的其他功能扩展上。而探讨的焦点就在 choice 的缺省按键功能上。

  当我们用 /t 开关指定了缺省按键后,经常会有一个问题,那就是一旦按下了某个
候选字符未指定的按键,就会终止缺省按键的倒计时。这是程序固有的细节,我们很难
通过外部的办法来改变它,于是,我们所能使用的最佳方法就是从内部修改它。

  修改一个可执行程序,并不是一件十分神秘高深的事情,虽然它有可能是一件枯燥
乏味的事情:)但是只要选对合适的工具,这项工作也将会进行的顺利和简单。可执行程
序是一个二进制文件,在 Windows下有很多人选择 UltraEdit或者 WinHex 来修改它,
但是,在这个案例中,为了兼顾 MS-DOS 的使用者,同时也考虑到修改过程的自动化,
我决定使用 DOS平台下的程序调试器—— DEBUG.EXE。

  作为 DOS下的调试工具, DEBUG.EXE有着相当广泛的用途。一方面它是一个二进制
文件编辑器,另一方面它又是一个汇编与反汇编器,同时也担当着用户与底层硬件的交
互平台,特别是在早期的逆向工程领域中,它扮演着举足轻重的角色,因此有着“屠龙
宝刀”的诨号,我们在这里使用它来扩展 choice 的使用功能,只能说是牛刀小试。关
于它子命令的用法说明,可以在网络上找到许多相当优秀的教程文章。

  修改程序之前,我们首先需要熟悉程序的流程,尤其是修改处的细节流程。我们运
用 debug的 d/u/g/t/p/ 等命令可以了解到 choice.com 的流程大致如下:

------------------------------------------------------------------------------
   CS:0100  跳至0211程序区 jump 0211 program area
   CS:0103  数据区 date area
   CS:0211  检查系统版本号 Verify MS-DOS version is 4.0 or later
   CS:0236  获取大写字母表 Get Upper Case Table address/bias
   CS:0262  分析命令行 Parse command line
   CS:03C7  转换小写字母 convert choices and default to Upper Case
   CS:03E9  检查缺省选择 verify specified default is in Choices
   CS:0401  显示选择提示 Display prompt
   CS:0442  等待按键或倒计时结束 wait for timeout or keypress
   CS:0476  接受按键 get key
   CS:04A4  显示按键并设置错误码后退出 got key, set errorlevel, quit
------------------------------------------------------------------------------
 
  进而分析我们关心的倒计时终止部分:
------------------------------------------------------------------------------
   CS:0442  计时秒数若为0 则跳至0476等待按键,否则取当前秒数后继续
   CS:0451  如果有案件则跳至0476,否则继续
   CS:045D  如当前秒数变化,则计时秒数减一后继续,否则跳至0451循环
   CS:046B  若计时秒数为0 则模拟按下缺省键并跳至0485,否则跳至0451循环
   CS:0476  接收按键
   CS:0485  搜索按键是否候选,如果是跳至04A4,如果否跳至049C
   CS:049C  响铃后跳至0476(* 注意此处 *)
   CS:04A4  显示并设置按键后退出
------------------------------------------------------------------------------

  从上面的分析可以看出,问题的关键在于 049C--04A4 之间的错误按键处理段。在
此处,程序响铃(即输出字符 07 )后,跳到了 0476 继续接收按键,从此不再计时。
解决的办法很简单,只需要将这个跳转地址改为 0451/045D的任意一处即可。通过 u命
令反汇编,确定这个跳转命令的地址 04A2 ,这样我们只要通过 a 4a2将汇编代码 jmp
476 替换为 jmp 451即可。

  此外,我们还可以考虑其它的一些功能上的扩展。比如无效按键不能终止倒计时,
在用户需要谨慎选择的时候,就需要一个特有按键(比如 ESC)停止倒计时;又比如,
按下一个常用键(比如回车或者空格),可以直接选择任意设定的缺省按键。

  这些功能的实现都不是很复杂,但前提是操作者要有一定的汇编编程和程序调试经
验。当然,谁也不可能要求所有的 DOS用户都能熟悉甚至了解 DEBUG子命令和 8086 代
码集。所以对于大多数人,本文提供了更为简洁的操作方式——批处理。这个程序可以
在附件的 X3 文件夹下见到,名为 ModChc.bat ,点击它即可以自动地根据可以找到的
choice.com生成一个新的 choicex.com。这个新的程序也可以在附件中的同样位置直接
找到,它业已实现了我在上面所提到的三项扩展。

  关于 xchoice.com更详细准确的描述如下:
------------------------------------------------------------------------------
  1.如果指定缺省选择键,键入无效选择键不会终止取缺省选择时的倒计时
  2.如果指定缺省选择键,可通过键入回车或空格键终止计时并立即返回缺省选择键
  3.如果指定缺省选择键,可通过键入跳出键终止计时并等待至键入有效选择键
  4.指定缺省选择键时,设置等待时间为 0,可实现仅在按回车或空格时取缺省选择
   键而不会限时。如:choice /c:qwert /t:w,0
  5.当指定跳出、回车或者空格键为选择键时,其终止计时的功能将相应失效
------------------------------------------------------------------------------

  如果程序附件的链接失效,或者你无法得到附件,那么可以根据下文提供的修改脚
本自己用 debug修改 choice.com ,命令格式如下:

  debug choice.com < choicex.asd > nul

  注意:在修改前请自己备份原程序
------------------------------------------------------------------------------
:: ChoiceX.asd - Choice 扩展 DEBUG 汇编脚本
:: Will Sort - 14:48 2005-5-15 - Debug
:: Modifition:
::   1.Not terminates timeout when press invalid choice key
::   2.press ESC to terminate timeout
::   3.press CR or SPACE to choose default choice key
a 047D
JNZ     0482        ; Call 0A80 when press control key
CALL    0A80        ; get second byte of scancode of control key
CALL    0A85        ; process event of press ESC, CR, SPACE

a 04A2
JMP     0451        ; Not terminate timeout when press invalid choice key       

a 0A80
MOV     AH,08       ; get char again
INT     21
RET
CMP     AL,1B       ; if press ESC
JZ      0A94        ; YES, terminate timeout
CMP     AL,0D       ; if press CR
JZ      0A91        ; Yes, goto set default choice
CMP     AL,20       ; if press SPACE
JNZ     0A99        ; No, goto return
MOV     AL,[018A]   ; set default choice
MOV     BYTE PTR [0189],00    ; set timeout is zero
RET

n ChoiceX.com
w
q
:: Please reserved this line.
——————————————————————————

编后语:

  第三部曲的准备,其实很早前就开始了,只是中途不耐于繁琐的文档整理而罢手,
直到这次执笔编写应用扩展三部曲,才又将它从故纸堆里翻了出来,花了几天时间重新
整理文档,又删除了一些复杂而不实用的修改(比如用十六进制的 ASCII编码指定候选
按键),简化了许多代码,终于成了现在的模样。

  至此,《 choice.com 应用扩展三部曲》已全部结束。回顾近一个月的编写历程,
仍不免留有一些遗憾,但已基本达成我的初衷,应该可以给自己有所交待了。希望它能
够对大家有所启发和帮助,也希望大家能对它提出自己宝贵的意见和建议,联系地址可
通过帖子上方的邮件功能找到。

  最后,在此真诚地祝愿所有的读者健康、快乐!

2005年05月31日

 

内存组织的三种形式:

    平坦模式:代码空间、数据空间和堆栈空间都存在一个线性连续的4G的地址中。
    分段模式:一般来说会将代码数据堆栈放在不同的segment中,寻址的时候需要指定段和偏移量,由此得到一个唯一的逻辑地址,每个段最大4G。
    实地址模式:为了使原有的8086程序与IA-32兼容,把线性空间分成最大64K的段。
处理器的模式:

        保护模式、实模式和系统管理模式
        保护模式最方便,可以使用以上三种内存组织模式,实模式下只能使用实内存模式,系统管理模式和实模式类似。
寻址范围:
IA-32有两种寻址,16和32,实模式下只能寻址在64K范围内,注意对齐问题。
基本的寄存器:
一般用途的寄存器
   EA/B/C/D-X,ESI,EDI,EBP,ESP.E就是表示Extend.
段寄存器
   C/D/S/E/F/G-S
状态寄存器
   EFLAGS
指令指针
   EIP
内存寻址操作
    基址+索引*Scale+偏移
I/O端口寻址操作
    64K个8Bit端口,可以通过直接对I/O端口地址进行操作或者在DX中操作。

 

代码:
[注:转载请注明出处]

.386
.model flat,stdcall
option casemap:none

include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\comctl32.inc

includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\comctl32.lib

.const
IDD_DIALOG1       equ 1000
IDC_STR_EDIT      equ 1001
IDC_CRC_EDIT      equ 1002
IDC_OPEN_BUTTON   equ 1003
MAXSIZE           equ 512

WndProc           proto :D WORD, :D WORD, :D WORD, :D WORD
MakeTable         proto
GetCrc32          proto

.data
szTemplate  db "%X", 0
Crc32Table  dd 256 dup(0)
szStr db 255 dup(0)

.data?
hInt       dd  ?
szCRC32     dd  4 dup(?) 
szText      db  50 dup(?)

.code
start:
  invoke InitCommonControls
  invoke GetModuleHandle,NULL
  mov hInt,eax
  invoke  DialogBoxParam,hInt,IDD_DIALOG1,NULL,offset WndProc,0
  invoke  ExitProcess,NULL

WndProc proc uses ebx ecx hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
        MOV eax,uMsg
 .if eax == WM_CLOSE
  invoke EndDialog, hWnd, 0
        .elseif eax == WM_COMMAND
          mov eax,wParam        
          .if eax==IDC_OPEN_BUTTON
             invoke GetDlgItemText, hWnd, IDC_STR_EDIT, addr szStr, 255
             invoke MakeTable
             lea ebx, szStr
             invoke GetCrc32
             mov szCRC32, eax
             invoke wsprintf, addr szText, addr szTemplate, szCRC32
             invoke  SetDlgItemText,hWnd,IDC_CRC_EDIT,ADDR szText
          .endif
 .else
          mov eax, FALSE
          ret
 .endif
 mov eax, TRUE
 ret
WndProc endp

;动态生成码表
MakeTable proc
  mov ecx, 256
  mov edx, 0EDB88320h
$OneLoop:
  lea eax, [ecx-1]
  push ecx
  mov ecx, 8
$TwoLoop:
  shr eax, 1
  jnc @F
  xor eax, edx
@@:
  dec ecx
  jne $TwoLoop
  pop ecx
  mov [Crc32Table+ecx*4-4], eax
  dec ecx
  jne $OneLoop
  ret
MakeTable endp

GetCrc32 proc
  mov eax, -1
  or ebx, ebx
  jz $Exit
@@:
  mov dl, [ebx]
  or dl, dl
  je $Exit
  xor dl, al
  movzx edx, dl
  shr eax, 8
  xor eax, [Crc32Table+edx*4]
  inc ebx       
  jmp @B
$Exit:
  not eax
  ret
GetCrc32 endp

end start

资源文件:
#include "resource.h"
#define IDD_DIALOG1             1000
#define IDC_Str_EDIT            1001
#define IDC_CRC_EDIT            1002
#define IDC_OPEN_BUTTON         1003

IDD_DIALOG1 DIALOG DISCARDABLE  -2, 0, 258, 67
STYLE DS_SYSMODAL |DS_FIXEDSYS |DS_SETFONT |DS_MODALFRAME |DS_CENTER |WS_POPUP |WS_CAPTION |WS_VISIBLE |WS_SYSMENU |WS_MINIMIZEBOX
CAPTION "获取字符串的Crc32校验值 By ksaiy"
FONT  9, "宋体"
BEGIN
 CONTROL  "",-1,"Static",SS_ETCHEDHORZ,5, 50, 248, 3
 LTEXT           "字符串:", IDC_STATIC,3, 3, 100, 12
 EDITTEXT         IDC_Str_EDIT,4, 13, 121, 12, ES_AUTOHSCROLL | ES_LEFT |WS_TABSTOP|WS_EX_STATICEDGE
 PUSHBUTTON "获取字符串的CRC32值",IDC_OPEN_BUTTON,158, 30, 92, 14,BS_FLAT
        LTEXT "By ksaiy 欢迎转载但请注明出处。", -1,121, 55, 128, 12, WS_DISABLED | WS_GROUP 
 LTEXT           "字符串的CRC32校验值:", IDC_STATIC,135, 3, 100, 9
 EDITTEXT         IDC_CRC_EDIT,135, 13, 118, 12, ES_AUTOHSCROLL | ES_LEFT |WS_TABSTOP| ES_READONLY|WS_EX_STATICEDGE       
END
[注:转载请注明出处]

 

16位模式与32位模式:
在如下情况下使用16位模式:
实模式;
虚拟86模式;
系统管理模式;
保护模式但代码数据堆栈段都设为16位;
同时含有16位和32位段的保护模式任务;
含有16位指令的32位代码段。

在保护模式下如何将16位模式与32位模式整合,如何同时将16位和32位代码放到32位代

码段中?

ia32用如下方式来区分16位和32位段和操作:
1.代码段描述符的标志位D(缺省操作数和地址大小)。
代码段描述符的D标志置位(置1)则是32位,清除(置0)则是16位。实模式和虚拟86模式

下不使用段描述符,因此缺省就是16位。
2.堆栈段描述符的标志位B(缺省堆栈大小)。
堆栈段描述符B指明了栈顶指针的大小(32位esp或16位sp寄存器),
3.16位和32位调用门,中断门,陷阱门。
当通过调用门,中断门,陷阱门在代码段之间传递程序控制权时,所使用的操作数的大

小(位数)由调用门,中断门,陷阱门的位数决定,而不是由D标志位和传送指令前缀决

定。门的位数决定如何在栈中保存返回信息。
4.16位和32位普通用途寄存器。

对大多数程序来说,32位任务必须使D标志位和B标志位置位,而对于16位程序来说则必

须清除这两个标志。(实模式和v86模式下无上述标志,默认16位)。
控制权由16位段向32位段转移的时候需要通过门。

使用操作前缀(prefixes)可以改写缺省的操作数和地址的大小。(在D标志位和B标志位

清除的情况下使用32位寄存器)。标志位可在任意模式下使用。
操作前缀仅在有限时间内(即前缀所作用的语句)改变操作数和地址的大小。

以下操作前缀可以允许在同一个段中使用16和32位操作。
操作数前缀:66h
地址前缀:67h
使用上述操作前缀会将D标志位和B标志位置位置反。

例如:
将32位操作数从32位寄存器移到一个32位有效地址(EA)的内存里。
使用操作数前缀,可将16位操作数从16位寄存器移到一个32位有效地址(EA)的内存里。
使用地址前缀,可将32位操作数从32位寄存器移到一个16位有效地址(EA)的内存里。
如果同时使用操作数和地址前缀,可将16位操作数从16位寄存器移到一个16位有效地址

(EA)的内存里


2005年04月21日

 

全面的编码标准包含代码结构的所有方面。虽然开发人员在实现标准时应慎重,但只要应用了就应该坚持。完成的源代码应该反映出一致的样式,就像一个开发人员在一个会话中编写代码一样。在开始软件项目时,建立编码标准以确保项目的所有开发人员协同工作。当软件项目并入现有的源代码时,或者在现有软件系统上执行维护时,编码标准应说明如何处理现有的基本代码。

源代码的可读性对于开发人员对软件系统的理解程度有直接影响。代码的可维护性是指为了添加新功能、修改现有功能、修复错误或提高性能,可以对软件系统进行更改的难易程度。尽管可读性和可维护性是许多因素的结果,但是软件开发中有一个特定的方面受所有开发人员的影响,那就是编码方法。确保开发小组生产出高质量代码的最容易方法是建立编码标准,然后在例行代码检查中将执行此标准。

使用一致的编码方法和好的编程做法来创建高质量代码在软件的品质和性能中起重要作用。另外,如果一致地应用正确定义的编码标准、应用正确的编码方法并在随后保持例行代码检查,则软件项目更有可能产生出易于理解和维护的软件系统。

尽管在整个开发周期内执行代码检查的主要目的是识别代码中的缺陷,但检查还可以以统一的方式执行编码标准。只有在整个软件项目中从开始到完成都遵从编码标准时,坚持编码标准才是可行的。在即成事实之后强加编码标准既不切合实际也是不明智的。

编码方法

编码方法合并了软件开发的许多方面。尽管它们通常对应用程序的功能没有影响,但它们对于改善对源代码的理解是有帮助的。这里考虑了所有形式的源代码,包括编程、脚本撰写、标记和查询语言。

不建议将这里定义的编码方法形成一套固定的编码标准。相反,它们旨在作为开发特定软件项目的编码标准的指南。

编码方法分为三部分:

  • 命名

  • 注释

  • 格式

命名

对于理解应用程序的逻辑流,命名方案是最有影响力的一种帮助。名称应该说明“什么”而不是“如何”。通过避免使用公开基础实现(它们会发生改变)的名称,可以保留简化复杂性的抽象层。例如,可以使用 GetNextStudent(),而不是 GetNextArrayElement()。

命名原则是:选择正确名称时的困难可能表明需要进一步分析或定义项的目的。使名称足够长以便有一定的意义,并且足够短以避免冗长。唯一名称在编程上仅用于将各项区分开。表现力强的名称是为了帮助人们阅读;因此,提供人们可以理解的名称是有意义的。不过,请确保选择的名称符合适用语言的规则和标准。

以下几点是推荐的命名方法。

例程

  • 避免容易被主观解释的难懂的名称,如对于例程的 AnalyzeThis(),或者对于变量的 xxK8。这样的名称会导致多义性,而不仅仅是抽象。

  • 在面向对象的语言中,在类属性的名称中包含类名是多余的,如 Book.BookTitle。而是应该使用 Book.Title。

  • 使用动词-名词的方法来命名对给定对象执行特定操作的例程,如 CalculateInvoiceTotal()。

  • 在允许函数重载的语言中,所有重载都应该执行相似的函数。对于那些不允许函数重载的语言,建立使相似函数发生关系的命名标准。

变量

  • 只要合适,在变量名的末尾追加计算限定符(Avg、Sum、Min、Max、Index)。

  • 在变量名中使用互补对,如 min/max、begin/end 和 open/close。

  • 鉴于大多数名称都是通过连接若干单词构造的,请使用大小写混合的格式以简化它们的阅读。另外,为了帮助区分变量和例程,请对例程名称使用 Pascal 大小写处理 (CalculateInvoiceTotal),其中每个单词的第一个字母都是大写的。对于变量名,请使用 camel 大小写处理 (documentFormatType),其中除了第一个单词外每个单词的第一个字母都是大写的。

  • 布尔变量名应该包含 Is,这意味着 Yes/No 或 True/False 值,如 fileIsFound。

  • 在命名状态变量时,避免使用诸如 Flag 的术语。状态变量不同于布尔变量的地方是它可以具有两个以上的可能值。不是使用 documentFlag,而是使用更具描述性的名称,如 documentFormatType。

  • 即使对于可能仅出现在几个代码行中的生存期很短的变量,仍然使用有意义的名称。仅对于短循环索引使用单字母变量名,如 i 或 j。

  • 不要使用原义数字或原义字符串,如 For i = 1 To 7。而是使用命名常数,如 For i = 1 To NUM_DAYS_IN_WEEK 以便于维护和理解。

  • 在命名表时,用单数形式表示名称。例如,使用 Employee,而不是 Employees。

  • 在命名表的列时,不要重复表的名称;例如,在名为 Employee 的表中避免使用名为 EmployeeLastName 的字段。

  • 不要在列的名称中包含数据类型。如果后来有必要更改数据类型,这将减少工作量。

杂项

  • 尽量减少使用缩写,而是使用以一致方式创建的缩写。缩写应该只有一个意思;同样,每个缩写词也应该只有一个缩写。例如,如果用 min 作为 minimum 的缩写,那么在所有地方都应这样做;不要将 min 又用作 minute 的缩写。

  • 在命名函数时包括返回值的说明,如 GetCurrentWindowName()。

  • 与过程名一样,文件和文件夹的名称也应该精确地说明它们的用途。

  • 避免对不同的元素重用名称,如名为 ProcessSales() 的例程和名为 iProcessSales 的变量。

  • 在命名元素时避免同音异义词(如 write 和 right),以防在检查代码时发生混淆。

  • 在命名元素时,避免使用普遍拼错的词。另外,应清楚区域拼写之间存在的差异,如 color/colour 和 check/cheque。

  • 避免用印刷标记来标识数据类型,如用 $ 代表字符串或用 % 代表整数。

注释

软件文档以两种形式存在:外部的和内部的。外部文档(如规范、帮助文件和设计文档)在源代码的外部维护。内部文档由开发人员在开发时在源代码中编写的注释组成。

不考虑外部文档的可用性,由于硬拷贝文档可能会放错地方,源代码清单应该能够独立存在。外部文档应该由规范、设计文档、更改请求、错误历史记录和使用的编码标准组成。

内部软件文档的一个难题是确保注释的维护与更新与源代码同时进行。尽管正确注释源代码在运行时没有任何用途,但这对于必须维护特别复杂或麻烦的软件片段的开发人员来说却是无价的。

以下几点是推荐的注释方法:

  • 如果用 C# 进行开发,请使用 XML 文档功能。有关更多信息,请参见:XML 文档。

  • 修改代码时,总是使代码周围的注释保持最新。

  • 在每个例程的开始,提供标准的注释样本以指示例程的用途、假设和限制很有帮助。注释样本应该是解释它为什么存在和可以做什么的简短介绍。

  • 避免在代码行的末尾添加注释;行尾注释使代码更难阅读。不过在批注变量声明时,行尾注释是合适的;在这种情况下,将所有行尾注释在公共制表位处对齐。

  • 避免杂乱的注释,如一整行星号。而是应该使用空白将注释同代码分开。

  • 避免在块注释的周围加上印刷框。这样看起来可能很漂亮,但是难于维护。

  • 在部署之前,移除所有临时或无关的注释,以避免在日后的维护工作中产生混乱。

  • 如果需要用注释来解释复杂的代码节,请检查此代码以确定是否应该重写它。尽一切可能不注释难以理解的代码,而应该重写它。尽管一般不应该为了使代码更简单以便于人们使用而牺牲性能,但必须保持性能和可维护性之间的平衡。

  • 在编写注释时使用完整的句子。注释应该阐明代码,而不应该增加多义性。

  • 在编写代码时就注释,因为以后很可能没有时间这样做。另外,如果有机会复查已编写的代码,在今天看来很明显的东西六周以后或许就不明显了。

  • 避免多余的或不适当的注释,如幽默的不主要的备注。

  • 使用注释来解释代码的意图。它们不应作为代码的联机翻译。

  • 注释代码中不十分明显的任何内容。

  • 为了防止问题反复出现,对错误修复和解决方法代码总是使用注释,尤其是在团队环境中。

  • 对由循环和逻辑分支组成的代码使用注释。这些是帮助源代码读者的主要方面。

  • 在整个应用程序中,使用具有一致的标点和结构的统一样式来构造注释。

  • 用空白将注释同注释分隔符分开。在没有颜色提示的情况下查看注释时,这样做会使注释很明显且容易被找到。

格式

格式化使代码的逻辑结构很明显。花时间确保源代码以一致的逻辑方式进行格式化,这对于您和必须解密源代码的其他开发人员都有帮助。

以下几点是推荐的格式化方法。

  • 建立标准的缩进大小(如四个空格),并一致地使用此标准。用规定的缩进对齐代码节。

  • 在发布源代码的硬拷贝版本时使用 monotype 字体。

  • 在括号对对齐的位置垂直对齐左括号和右括号,如:

·                for (i = 0; i < 100; i++)

·                {

·                   

}

还可以使用倾斜样式,即左括号出现在行尾,右括号出现在行首,如:

for (i = 0; i < 100; i++){

  

}

无论选择哪种样式,请在整个源代码中使用那个样式。

  • 沿逻辑结构行缩进代码。没有缩进,代码将变得难以理解,如:

·                If … Then

·                If … Then

·               

·                Else

·                End If

·                Else

·               

End If

缩进代码会产生出更容易阅读的代码,如:

If … Then

   If … Then

  

   Else

  

   End If

Else

End If

  • 为注释和代码建立最大的行长度,以避免不得不滚动源代码编辑器,并且可以提供整齐的硬拷贝表示形式。

  • 在大多数运算符之前和之后使用空格,这样做时不会改变代码的意图。但是,C++ 中使用的指针表示法是一个例外。

  • 使用空白为源代码提供结构线索。这样做会创建代码“段”,有助于读者理解软件的逻辑分段。

  • 当一行被分为几行时,通过将串联运算符放在每一行的末尾而不是开头,清楚地表示没有后面的行是不完整的。

  • 只要合适,每一行上放置的语句避免超过一条。例外是 C、C++、C# 或 JScript 中的循环,如 for (i = 0; i < 100; i++)。

  • 编写 HTML 时,建立标准的标记和属性格式,如所有标记都大写或所有属性都小写。另一种方法是,坚持 XHTML 规范以确保所有 HTML 文档都有效。尽管在创建 Web 页时需折中考虑文件大小,但应使用带引号的属性值和结束标记以方便维护。

  • 编写 SQL 语句时,对于关键字使用全部大写,对于数据库元素(如表、列和视图)使用大小写混合。

  • 在物理文件之间在逻辑上划分源代码。

  • 将每个主要的 SQL 子句放在不同的行上,这样更容易阅读和编辑语句,例如:

·                SELECT FirstName, LastName

·                FROM Customers

WHERE State = ‘WA’

  • 将大的复杂代码节分为较小的、易于理解的模块。

1.         是否使用异常来显示错误而不是返回状态或错误代码?

2.         所有的类和公共方法是否使用.NET样式的注释?注意,<summary>注释应该论述公共方法是什么。对于怎么使用则应该放在<remarks>块中或是内嵌于正被讨论的代码中

3.         如果方法的参数不正确,是否使用一个异常来进行确认和拒绝?

4.         Debug.Asserts是否被用来验证关于代码功能的假定?注释例如:"j will be positive"应该被作为断言(Asserts)重写

5.         那些不应该被初始化的类是否有一个私有的构造函数?

6.         那些被声明为值类型并极少使用为方法参数的类是否从方法中返回或是存放在集合(Collections)中?

7.         那些被只应用在一个程序集中的类是否被标记为internal

8.         那些能被多线程访问的单态类(Singletons)是否能够被正确地初始化?参考 the Enterprise       Solution Patterns book, p. 263.

9.         必须被继承类重载的方法是否被标记为abstract

10.     不应该被重载的类是否标记为sealed

11.     “as” 是否可能被不正确的使用?

12.     是否类重载 ToString 而不是定义另外一个方法来输出对象的状态?

13.     是否一个长的信息被发送到日志组件而不是控制台?

14.     一个finally程序块是否紧随一个try构造以用作必须执行的代码?

15.     相对于forint i…..)构造,是否更倾向于使用foreach

16.     是否使用属性而不是实现gettersetter方法?

17.     相对于没有赋值器的属性,是否更倾向于使用只读变量?

18.     被继承类重载的所有方法是否使用了override关键字?

19.     是否倾向于使用接口类而不是抽象类?

20.     是否写代码基于接口而不是一个实现类?

21.     那些资源消耗大的对象是否实现了IDisposable接口?

22.     那些实现了IDisposable的对象是否在使用的时候才初始化?

23.     相对于Monitor Enter 构造,是否更倾向于使用lock关键字?

24.     是否线程被事件或Pulse构造从等待状态激活,而不是调用Sleep()等方式“积极”的等待?

25.     如果重载equals,是否正确地实现了这一方法?重载equals的规则是复杂的,细节请参见Richter p153-160

26.     如果== 和!=被重载,由此他们重定向到了Equals

27.     是否提供了Equals的那些对象也提供了GetHashCode的重载版本?GetHashCode提供了和Equals相同的语义。注意:GetHashCode的重载应该利用对象的成员变量并且必须返回一个不再更改的哈希码。

28.     是否所有的异常类有一个构造函数带有一个字符参数,另外一个构造函数带有一个字符参数和一个异常参数?

29.     是否所有的异常类继承与基本的Matrix异常并正确地适合异常的层次?

30.     是否那些将要被封送或远程调用的类使用了Serializable属性?

31.     是否那些使用了Serializable属性的类,包括ExceptionEventArgsl类型的类,有一个默认的构造函数?

32.     那些实现了ISerializable的类是否即提供了必须的GetObjectData重载也提供了带有一个SerializeInfo和一个StreamingContext参数的构造函数?

33.     在作浮点值运算的时候,是否所有的常量加倍而不是整数?

34.     是否所有的代理有一个void返回类型并且避免使用outref参数?

35.     是否继承于EventArgs的类中的所有成员都是只读?这将阻止一个预订者更改这个EventArgs以免影响另一个预订者。

36.     代理是否被发布为事件?这将阻止预订者引发事件。详细请参见Lowy, p. 102

37.     通常的安装和卸载nUnit代码是否孤立于那些标记了合适属性的安装和卸载方法?

38.     消极的单元测试是否使用ExpectedExceptin属性去显示一定会抛出某一个异常

 

2005年02月18日

   最近看了朱邦复先生1990年所写的<<组合语言之艺术>>一书,收获良多.特别是书中组合语言指今优化一章写得十分精彩,虽然是以8086指今为例讲解,但有些优化方案现在也不过时,特节选如下:

(注:台湾电脑术语和大陆有些区别, 组合=汇编 程式=程序 暂存器=寄存器 回路=循环)

      组合语言可以说是未经整理的、原始的电脑语言,读者们大可下一番功夫,找出其应用的规则,以发挥最高的效率。在下面,我仅就个人的经验,提供一些浅见,以供切磋研讨。
    要写好程式,首先应熟记8088指令的时钟脉冲(Clock )及指令长度,一般组合语言手册中,都详列了与各指令相关的资料。「工欲善其事,必先利其器」,此之谓也。
    本节所讨论的,是一般程式师容易忽略的细节,所有的例子都是从我所看过的一些程式中摘录下来的。看来没什么大了不起,可是程式的效率,受到这些小地方的影响很大。更重要的是,任何一个人,只要有「小事不做,小善不为」的习惯,我敢断言,这个人不会有什么大成就!
    我最近才查到 Effective Address (EA) 的时钟值,我觉得没有必要死记。原则上,以暂存器为变数,做间接定址时为5个时钟,用直接定址则为6个;若用了两组变数,则为7至9个,三组则为11或12个。
    为了便于叙述,下面以“T”表「时钟脉冲」; “B”表字元。其中
    时钟脉冲T = 1 / 振荡频率

一、避免浪费速度及空间

    组合语言的效率建立在指令的运用上,如果不用心体会下列指令的有效用法,组合语言的优点就难以发挥。
  1,    CALL    ABCD
        RET
    这种写法,是没有用心的结果,共用了 4B,23T+20T,完全相同的功能,如:
        JMP     ABCD  或 
        JMP     SHORT ABCD
    却只要 2-3B,15T。
        此外,上述的CALL XXXX 是调用子程式的格式,在直觉认知上,与JMP XXXX完全不同。对整体设计而言,是不可原谅的错误,侦错的时候,也很难掌握全盘的理念。 尤其是在精简程式的时候,很可能会遇到 ABCD 这个子程式完全独立,是则把这段程式直接移到 ABCD 前,不仅能节省空间,而且使程式具有连贯性,易读易用。

  2,    MOV     AX,0
    同样,这条指令要 3B,4T,如果用:
        SUB     AX,AX 或 
        XOR     AX,AX
    只要 2B,3T, 唯一要注意的是,后者会影响旗号,所以不要用在有旗号判断的指令前面。 
     在程式写作中,经常需要将暂存器或缓冲器清为0,有效的方法,是使某暂存器保持为0,以便随时应用。  因为,MOV [暂存器],[暂存器] 只要 2B,2T, 即使是清缓冲器,也比直接填0为佳。 
     只是,如何令暂存器保持0,则要下一番功夫了。 
     还有一种情况,就是在一回路中,每次都需要将 AH 清0,此时对速度要求很严,有一个指令 CBW 原为将一 个字元转换为双字元,只需 1B,2T 最有效率。可是应该注意,此时 AL 必须小于 80H,否则 AH 将成为负数。
  3,    ADD     AX,AX
    需要 2B,3T不如用:
        SHL     AX,1
    只要2B,2T。

  4,    MOV     AX,4
    除非这时 AH 必为0,否则,应该用:
        MOV     AL,4
    这样会少一个字元。

  5,    MOV     AL,46H
        MOV     AH,0FFH
    为什么不写成:
        MOV     AX,0FF46H
    不仅省了一个字元,四个时钟,而且少打几个字母!

  6,    CMP     CX,0
    需要 4B,4T, 但若用:
     OR      CX,CX
    完全相同的功能,但只要 2B,3T。再若用:
        JCXZ    XXXX
    则一条指令可以替代两条,时空都省。不幸这条指令限用于CX ,对其他暂器无效。

  7,    SUB     BX,1
    这更不能原谅,4B,4T无端浪费。
        DEC     BX
    现成的指令,1B,2T为何不用?
        如果是
        SUB     BL,1 
    也应该考虑此时 BH 的情况,若可以用
         DEC     BX
    取代,且不影响后果,亦不妨用之。

  8,    MOV     AX,[SI]
        INC     SI
        INC     SI
    这该挨骂了,一定是没有记熟指令,全部共4B,21T。
        LODSW
    正是为这个目的设计,却只要 1B,16T。

  9,    MOV     CX,8
        MUL     CX
        写这段程式之时应先养成习惯,每遇到乘、除法,就该打一下算盘。因为它们太浪费时间。8位元的要七十多个时钟,16位元则要一百多。所以若有可能,尽量设法用简单的指令取代。
        SHL     AX,1
        SHL     AX,1
        SHL     AX,1
     原来要 5B,137T,现在只要 6B,6T。如果CX能够动用的话,则写成:
      MOV     CL,3
      SHL     AX,CL
     这样更佳,而且CL之值越大越有利。用CL作为计数专 用暂存器,不仅节省空间,且因指令系在 CPU中执行,速 度也快。可是究竟快了多少? 我们做了些测试,以 SHL为例,在10MHZ 频率的机器上,作了3072 ×14270次,

所测得时间为:
    指  令 :SHL   AX,CL         SHL   AX,n
          CL = 0 , 23 秒     n = 0 , 无效
      CL = 1 , 27 秒     n = 1 , 14 秒
          CL = 2 , 32 秒     n = 2 , 28 秒
          CL = 3 , 36 秒     n = 3 , 42 秒
          CL = 4 , 40 秒     n = 4 , 56 秒
          CL = 5 , 44 秒     n = 5 , 71 秒
          CL = 6 , 49 秒     n = 6 , 85 秒
          CL = 7 , 54 秒     n = 7 , 99 秒
        由此可知,用CL在大于2时即较分别执行有效。
        此外,亦可利用回路做加减法,但要算算值不值得,且应注意是否有调整余数的需要。

 10,    MOV     WORD PTR BUF1,0
        MOV     WORD PTR BUF2,0
        MOV     WORD PTR BUF3,0
        MOV     BYTE PTR BUF4,0
        ..
        我见过太多这种程式,一见就无名火起! 在程式中,最好经常保留一个暂存器为0,以便应付这种情况。即使没有,也要设法使一暂存器为0,以节省时、空。
        SUB     AX,AX
        MOV     BUF1,AX
        MOV     BUF2,AX
        MOV     BUF3,AX
        MOV     BUF4,AL

     14B,59T取代了 24B,76T,当然值得。只是,还是不 如事先有组织,考虑清楚各个缓冲器间的应用关系。以前面举的例来说,假定各缓冲器内数字,即为其实际位置关系,则可以写成:
         MOV     CX,3   
  如已知 CH 为0,则用: 
    MOV     CL,3
        SUB     AX,AX
        MOV     DI,OFFSET BUF1
        REP     STOSW
        STOSB   
    这段程式越长越占便宜,现在用10B,37T,一样划算。

 11,子程式之连续调用:
        CALL    ABCD
        CALL    EFGH
        如果 ABCD,EFGH 都是子程式,且调用的次数甚多,则上述调用的方式就有待商榷了。因为连续两次调用,不仅时间上不划算,空间也浪费。
        若ABCD一定与EFGH连用,应将ABCD放在EFGH之前:
        ABCD: 
            ..
        EFGH: 
            ..
        像这样,只要调用ABCD就够了,但这种情形多半是程式师的疏忽所致,如两个子程式必需独立使用,而上述连续调用的机会超过两次以上,则应该改为:
        CALL    ABCDEF
        而ABCDEF则应为:
        ABCDEF: 
               CALL    ABCD
        EFGH: 
            ..
        这样的写法速度不会变慢,而空间的节省则与调用的次数成正比。

 12,常有些程式,当从缓冲器中取资料时,必须将暂存器高位置为0。如:
        SUB     AH,AH
        MOV     AL,BUFFER
     这时应该将 BUFFER 先设为:
        BUFFER  DB  ?,0
     然后用: 
        MOV     AX,WORD PTR BUFFER
        如此,不但速度快了,空间也省了。

 13,有时看来多了一个指令,但因为指令的特性,反而更为精简。如:
    OR    ES:[DI],BH
    OR    ES:[DI+1],BL
    这样需要8B,32T,如果改用下面的指令:
    XCHG    BL,BH
    OR    ES:[DI],BX
    XCHG    BH,BL
    则需7B,28T。

 14,PUSH  及 POP  是保存暂存器原值的指令,都只需一个字元,但却很费时间。  PUSH  占 15T,POP 占12T,除非不得已,不可随便使用。有时由于子程式说明不清楚,程式师为了安全,又懒得检查,便把暂存器统统堆在堆栈上。尤其是在系统程式或子程式中,经常有到堆栈上堆、取的动作。实际上,花点功夫,把暂存器应用查清楚,就可以增进不少效率。    要知道,系统程式及某些子程式常常应用,有关速度的效率甚大,如果掉以轻心,就是不负责任! 保存原值的方法很多,其中较有效率的是放到一些不用的暂存器里。以我的经验,堆栈器用途最少,正好用作临时仓库。但最好的办法,还是把程式中暂存器的应用安排得合情合理,不要浪费,以免堆得太多。 还有一种方法,是在该子程式中,不用堆栈的手续,但另设一个入口,先将暂存器堆起,再来调用不用堆栈的子程式。这两个不同的入口,可以分别提供给希望快速处理,或需要保留暂存器原值者调用。 
      当然,更简单有效的方法,则是说明本段程式中某些暂存器将被破坏,而由调用者自行保存之。

二、程式要条理通顺

  1,在比较判断的过程中,邻近值不必连比。
        CMP     AL,0
        JE      ABCD0
        CMP     AL,1
        JE      ABCD1
        CMP     AL,2
        JE      ABCD2
        ..
    应为:
        CMP     AL,1
        JNE     ABCD0
    ABCD1: 
        ..
    在标题为ABCD0 中,再作:
        JA      ABCD2
    这种做法端视时间效益而定,似此 ABCD1之速度最快。

  2,未经慎思的流程:
        ADD     AX,4
    ABCD:
        STOSW
        ADD     AX,4
        ADD     DI,2
        LOOP    ABCD
        ..
    稍稍动点脑筋,就好得多了:
    ABCD:
        ADD     AX,4
        STOSW
        INC     DI
        INC     DI
        LOOP    ABCD
        ..

  3,错误的处理方式:
        MOV     BX,SI
    ABCD:
        MOV     BX,[BX]
        OR      BX,BX
        JZ      ABCD1
        MOV     SI,BX
        JMP     ABCD
    ABCD1:
        LODSW
        ..
    上例应该写成:
        MOV     BX,SI
    ABCD:
        LODSW
        OR      AX,AX
        JZ      ABCD1
        MOV     SI,BX
        JMP     ABCD
    ABCD1:
        ..

  4,错误的流程:
        TEST    AL,20H
        JNZ     ABCD
        CALL    CDEF[BX]
        JMP     SHORT ABCD1
    ABCD:
        CALL    CDEF[BX+2]
    ABCD1:
        ..
 应该写成:  
        TEST    AL,20H
        JZ      ABCD
        INC     BX
        INC     BX
    ABCD:
        CALL    CDEF[BX]
    ABCD1:
        ..

  5,下面是时间的损失:
        PUSH    DI
        MOV     CX,BX
        REP     STOSB
        POP     DI
        PUSH,POP 很费时间,应为:
        MOV     CX,BX
        REP     STOSB
        SUB     DI,BX
        同理,很多时候稍稍想一下,就可省下一些指令:
        PUSH    CX
        REP     MOVSB
        POP     CX
        SUB     DX,CX
    为什么不干脆些?
        SUB     DX,CX
        REP     MOVSB

  6,有段程式,很有规律,但却极无效率:
    X1:
        TEST    AH,1
        JZ      X2
        MOV     BUF1,BL
    X2:
        TEST    AH,2
        JZ      X3
        MOV     BUF2,DX     ; 凡双数用DX,单数用BL
    X3:
        TEST    AH,4
        JZ      X4
        MOV     BUF3,BL
    X4:
        ..                  ; 以下各段与上述程式相似
    X8:
        ..
        这种金玉其表的程式,最没有实用价值,改的方法应由缓冲器着手,先安排成序列,由小而大如:
        BUF1    DB  ?
        BUF2    DW  ?
        BUF3    DB  ?
        BUF4    DW  ?
        ..
    然后,程式改为:
        MOV     DI,OFFSET BUF1      ; 第一个缓冲器
        MOV     AL,BL
        MOV     CX,4        
    X1:
        SHR     AH,1
        JZ      X2
        STOSB
    X2:
        SHR     AH,1
        JZ      X3
        MOV     [DI],DX
        INC     DI
        INC     DI
    X3:
        LOOP    X1

  7,回路最怕千回百转,不畅不顺,如:
        SUB     AH,AH
    ABCD:
        CMP     AL,BL
        JB      ABCD1
        SUB     AL,BL
        INC     AH
        JMP     ABCD
    ABCD1:
        ..
      以上 ABCD1这个入口是多余的,下面就好得多:
        MOV     AH,-1
    ABCD:
        INC     AH
        SUB     AL,BL
        JA      ABCD
        ADD     AL,BL       ; 还原
        ..

  8,当处理字码时,需要字母的序数,有这样的写法:
        CMP     AL,60H  
        JA      ABCD1 
        SUB     AL,40H      ; 大写字母
    ABCD:
        ..
    ABCD1:
        SUB     AL,60H      ; 小写字母
        JMP     ABCD
        要知道字母码的特色在于大写为 40H 至4AH,小写为60H 至6AH ,以上程式,其实只要一个指令就可

以了:
        AND     AL,1FH
    简单明瞭!

  9,大多数的程式在程式师自己测试下很少发生错误,而一旦换一另个人执,就会发现错误百出。 其原因在于写程式者已经假定了正确的情况,当然不会以明知为错误的方式操作。可是换了一个人,没有先入为主的成见,很可能输入了「不正确」的资料,结果是问题丛生。  要知道真正的使用者,绝非设计者本人,在操作过程中,按键错误在所难免。这种错误应该在程式中事先加以检查,凡是输入资料有「正确、错误」之别者,错误性资料一定要事先加以排除。 这样做看起来似乎程式不够精简,可是正确的重要性远在精简之上。一旦发生了错误,再精简的程式也没有使 用价值。 此外,在程式中常有加、减的运算,这时也应该作正确性检查,否则会发生上述同样的问题。

三、指令应用要灵活

    有一段很简单的程式,其写作的方法甚多,但是指令应用的良窳,会使得程式的效率相去天上地下,难以估计。
    这段程式的用途,是要将一段资料中,英文字符大、小写相互转换。当然,转换的选择要由使用者决定,在下面程式且略去使用介面,假设已得知转换的方式。
    设资料在 DS:SI中,资料长度=CX ,大写转小写时BL=0,反之,则BL=1。
    我见过一种写法,简直无法原谅:
    1: LOOP1:
    2:        CALL    CHANGE
    3:        JC    LOOP11
    4:        ADD    AL,20H
    5:        JMP    SHORT LOOP12
    6: LOOP11:
    7:        SUB    AL,20H
    8: LOOP12:
    9:        MOV    [SI-1],AL
   10:        LOOP    LOOP1
   11:        RET
   12: CHANGE:
   13:        LODSB
   14:        OR    BL,BL
   15:        JZ    CHANGS
   16:        CMP    AL,61H
   17:        JB    CHARET
   18:        CMP    AL,7AH
   19:        JA    CHARET
   20:        STC
   21: CHARET:
   22:        RET
   23: CHANGS:
   24:        CMP    AL,41H
   25:        JB    CHARET
   26:        CMP    AL,5AH
   27:        JA    CHARET
   28:        CLC
   29:        RET
    这种程式错在把由12到29的程式写得太长,共 25B,有共用的价值,于是作为子程式调用。
    试想一下,每一笔资料,都要调用一次,浪费四个字元事小,但每次要费 23+20个时钟脉冲,资料多时,不啻为天文数字。更何况这段程式写得极差,在回路中,又多浪费了几十个时钟。关于这一点,下面会继续讨论。
    照上面这段程式,略加改进,写法如下:
    1: CHANGE:
    2:        LODSB
    3:        OR    BL,BL
    4:        JZ    CHANGS
    5:        CMP    AL,61H
    6:        JB    CHARET
    7:        CMP    AL,7AH
    8:        JA    CHARET
    9:        SUB    AL,20H
   10: CHANG0:
   11:        MOV    [SI-1],AL
   12: CHANG1:
   13:        LOOP    CHANGE
   14:     RET
   15: CHANGS:
   16:        CMP    AL,41H
   17:        JB    CHANG1
   18:        CMP    AL,5AH
   19:        JA    CHANG1
   20:        ADD    AL,20H
   21:        JMP    CHANG1
    这样的写法还是不佳,因为在回路中,用常数与暂存器比较,速度较暂存器相比为慢。应该先将需要比较的值,放在暂存器DH,DL 中,改进如次:
    1:        MOV    AH,20H
    2:        MOV    DX,7A61H
    3:        OR    BL,BL
    4:        JZ    CHANGE
    5:        MOV    DX,5A41H
    6: CHANGE:
    7:        LODSB
    8:        CMP    AL,DL
    9:        JB    CHANG1
   10:        CMP    AL,DH
   11:        JA    CHANG1
   12:        XOR    AL,AH
   13:        MOV    [SI-1],AL
   14: CHANG1:
   15:        LOOP    CHANGE
   16:     RET
    以上这段程式,空间小,速度快,每笔资料,平均仅需不到40个时钟值,以10 MHZ计,十万笔资料,约需半秒钟!
 请注意程式中所用的技巧,由2至6的分支法,就比下面这种写法为佳:
    1:        OR    BL,BL
    2:        JZ    CHAN1 
    3:        MOV    DX,5A41H
    4:      JMP    SHORT CHANGE
    5: CHAN1:
    6:        MOV    DX,7A61H
    7: CHANGE:
    这种分支也可以由另一种技巧所取代,即预设法。事先将所需用的参数放在固定的缓冲区中,此时取用即可:
           MOV  DX,BWCOM   ; 比较之预设值 
    这样程式又简单些了:
    1:       MOV    AH,20H
    2:        MOV    DX,BWCOM
    3: CHANGE:
    4:        LODSB
    5:        CMP    AL,DL
    6:        JB    CHANG1
    7:        CMP    AL,DH
    8:        JA    CHANG1
    9:        XOR    AL,AH
   10:        MOV    [SI-1],AL
   11: CHANG1:
   12:        LOOP    CHANGE
   13:     RET

    以上介绍为变数法技巧,即将所要比较的值,放在暂存器中。由于暂存器快速、节省空间,因此程式效率高。更重要的一点,是程式本身的弹性大,只要应用方式统一,事先把参数设妥,即可共用。

回路中的指令

    回路最重要的是速度,因为本段程式,将在计数器的范围之内,连续执行下去。如果不小心浪费了几个时钟值,在回路的累积下,很可能使程式成为牛步。
    要想把回路写好,一定要记清楚每个指令的执行时钟,以便选择效率最高者。同时,要知道哪些指令可以获得相同的处理效果,才能有更多的选择。
    其次,在回路中,最忌讳用缓冲器,不仅占用空间大,处理速度慢,而且不能灵活运用,功能有限。另外也应极力避免常数,尽量设法经由暂存器执行,用得巧妙时,常会将整个程式的效率提高百十倍。
    还有便是少用 PUSH,POP,DIV,MUL和 CALL 等浪费时钟的指令。除此之外,小心、谨慎,深思、熟虑,才是把回路写好的不二法门。
    在前例中,把比较常数的指令换为比较暂存器,便是很好的证明。如果用常数,两段程式决不可能共用,时、空都无谓地浪费了。
    以下再举数例,乍看这似乎有些吹毛求疵,但是仔细计算一下所浪费的时间,可能就笑不出声了。
 兹假定以下回路需处理五万字元的资料,频率为 10MHZ,其情况为:
    1: LOOP1:
    2:          LODSB
    3:        XOR    AL,[DI]
    4:        STOSB
    5:        LOOP    LOOP1
    本程式计数器等于50,000,每次需
    12T+14T+11T+17T=55T 个时钟脉冲
若以50,000次计,需时 47*50,000/10,000,000 秒,即约四分之一秒。
    只要稍稍将指令调整一下,为:
    1: LOOP1:
    2:             LODSW
    3:        XOR    AX,[DI]
    4:        STOSW
    5:        LOOP    LOOP1
    这样计数器只要25,000次,每次
    16T+18T+15T+17T=66T
    则25,000次需时 66*25,000/10,000,000 秒,约六分之一秒,比前面的程式快了二分之一。
    同理,在回路中加回路,而每个回路需 17T,也是很大的浪费。倘若加调用 CALL 指令,则需 23T+20T=43T,浪费得更多,读者不可不慎。
    当某一段程式用得很频繁时,理应视作子程式,例如下面的 LODAX:
    1: LOOP1:
    2:        CALL    LODAX
    3:        LOOP    LOOP1
    4:        RET
    5: LODAX:
    6:        LODSW
    7:        XOR    AX,[DI]
    8:        STOSW
    9:        RET
    其实这是贪小失大,仅四个字元的程式,竟用三个字元的调用指令去交换,是绝对得不偿失的。
    再如同下面的程式,颇有值得商榷之处。
    1: LOOP1:
    2:        MOV    DX,NUMBER1
    3:        MOV    CX,NUMBER2
    4:    LOOP2:
    5:        PUSH    CX
    6:        MOV    CX,DX
    7: LOOP3:
    8:        LODSW
    9:        XOR    AX,[DI]
   10:        STOSW
   11:        LOOP    LOOP3
   12:        INC     DI
   13:        INC     DI
   14:        POP    CX
   15:        LOOP    LOOP2
   16:        RET
    第二个回路是多余的,这是高阶语言常用的观念,对组合语言完全不适用。
    稍加改动,不损上面程式原有的条件,得到:
    1: LOOP1:
    2:        MOV    DX,NUMBER1
    3: LOOP2:
    4:        MOV    CX,NUMBER2
    5: LOOP3:
    6:        LODSW
    7:        XOR    AX,[DI]
    8:        STOSW
    9:        LOOP    LOOP3
   10:        INC     DI
   11:        INC     DI
   12:        DEC     DX
   13:        JNZ    LOOP2
   14:        RET
这样回路少了一个,程式中将5,6,14,15 各条中原来为15T+2T+12T+17T=46T的时间,省为12,13,14条的

2T+16T+17T=35T。

分支处理

    比较资料后,作条件分支 (Conditional Jump ),是程式中不可避免的手续。程式一长,分支距离超过 128个字元,条件分支就无法到达。当然,精简程式有时可以避免这种情形,但却不尽然。
    处理条件分支的技术很多,其效率端视情况而定。最要紧的是事先规划,要比较些什么?在何种情况下?分支到哪里?做些什么工作等等。
    不仅是写程式,人的各种能力,都可以由工作的方式判断出来。智慧高的人,很快就能抓住重点,再分门别类,钜细无遗的理出完整的系统。经过良好训练的专家,则能根据一套法规,逐步地整理归纳,也能推出合情合理的结果来。
    老实说,电脑程式的写作技术还没有到成熟的阶段,当今所有的从业人员,都只能算是「拓荒者」,并没有真正的「专家学者」。充其量,像我个人一样,比别人机会好些,天天得以与电脑为伍,多一点经验而已。
    因此,目前写程式几乎可以说没有可资遵循的法规,海阔天空,爱怎样写,就怎样写,只要能够使用,程式卖得出去,赚了大钱,就会被人视为大师。
    只是这种情况维持不了多久了,初民的壁画,仅具有历史意义。今天的程式师,如果不认清现实,立刻觉醒,多致力于法规的制定,电脑将永远是个不成熟的孩子。一旦这些法规经得住考验,为未来的专家学者奠定基础,那才能真正的被视为大师。 我不讳言我们正朝着这个方向努力,但是,我却不认为做得到。因为电脑的硬体设计在今后的十年内,必然会有重大的突破,谁都难以预测会有什么结果。软体的制作观念虽然不可能有很大的改变,却难免会受到影响。只有各位年轻朋友,你们成长在电脑时代,肯多一分耕耘,必有收获!
    下面,且介绍一些我对条件分支的处理技巧:

一、资料的分类

  1,位元分类:
        在本书第四章第五节所举的,由输入码作为输出字形的处理依据之例,就是采用位元分类的例证。
        但凡以资料位元作为共同的分类讯息,而且各类皆有独特的处理方式者,皆应以其位元为顺序,用间

接定址或分支技巧,作为程式处理之手段。

  2,字元分类:
        每一个字元具有 256种排列组合,设若有 128种以内的分类项目,应该取双数分类,否则须用连续分类。
        分类之值,立即可以用间接定址执行。但须注意,各分类的入口标题应先行定义。由于定义必须用到双字元,所以,凡采用连续分类者,其值应乘二。

  3,间隔分类:
        在有些情况下,原有资料不容许重新安排,而且其中若干资料已具备分类之特性,这种情况,我们称之为间隔分类。
        在处理此类资料时,应该先将可以作分类处理的资料提取出来,并视为字串,定义在一缓冲区内。当须要类比时,可利用「比对字串」 (SCAS) 的指令以求得其定义位置,再作间接定址。设有
        4700H,4900H,4F00H,5100H,4A2DH,4EABH
    等键盘输入数据。设上述值在AX中,需要作特殊处理,分别进入COD1至COD6等子程式。
    11将资料定义在缓冲器 ABC中,程式则定义在DEF:
      ABC    DW   4700H,4900H,4F00H,5100H,4A2DH,4E2BH
      DEF    DW   COD1,COD2,COD3,COD4,COD5,COD6
    12使DI=ABC,CX=6:
        MOV     DI,OFFSET ABC
        MOV     CX,6
    13由比对字串后,判断是否AX中有上述之值,如有,则用间接定址的方式执行之。
        REPNZ   SCASW               ; 比对六组字串
        JCXZ    NOTHING             ; 没有所比之字串
        SUB     DI,OFFSET ABC+2     ; 得到比对位置值
        CALL    CS:DEF[DI]          ; 或作JMP
          上述之DEF 如果放在DG段中,还可以节省一字元,并可加快速度:
        CALL    DEF[DI]

二、程式的结构

    若在程式规划之初,未先做好准备工作,临时想用前述的方法,并非绝不可能。但是,东添一点,西补一段,这种程式不仅会导致测试的麻烦,更可能影响未来的维护和调整。
    因此,每当瞭解了工作任务后,需要作间接定址的部份,最好能集中在一个模组内。万一性质不同必须分割,也应该将间接定址的程式,置放在模组的起头处。
    这样做的好处很多,一方面便于扩充功能,每次增加定址因素时,不必在程式中寻来找去,立刻可以安排妥当。其次,这种定址的需求,必然与整体功能有关,而且定义表相当于一个目录,把纲领放在前面,按图索骥,一目瞭然。更重要的,是可以表现出程式结构的层次,层次处理是网状流程中最难以掌握的一环,不可不慎。
    还有,就是各子程式的标题安排,其位置的先后应以功能的集中性为准。这样做的好处是,如果有可以共用的程式段,很容易就可合并为一,节省空间。

三、次序与条件「真」「假」

    条件分支的「时钟数」有二个可能,条件符合时,执行分支为 16T,不符合则为 4T ,且继续下一指令。两者相差有四倍之多,我们正该利用这一特点,速度重要的条件,都应该设为主流程,否则为分流程。  尤其是在需要高速的回路中,分支处理得好坏,效率相去甚远。这种分支需要平时多加小心,培养出良好的习惯。
    CDEF: 
        CMP    AL,’?’
        JZ     ABCD       ; 各比较符号中,’?’ 者最少
        LOOP   CDEF       ; NZ条件仅需4T速度较快
    ABCD: 
        ..

四、JMP 与 JMP SHORT

    当程式师专心写作或侦错之时,常无法瞻前顾后。然而侦错完毕程式无误时,最好彻底检查一下所有的JMP 指令,经常会大有斩获!
    因JMP 需要三字元,而JMP SHORT 只要两个,其条件是所跳越的位址不能超过128 字元。
    在程式编译时,若向上JMP 的距离在 128字元以内,编译器会自动译为两字元。往下则不然,如在128 字元内,会再多加一个 NOP指令,不仅浪费一字元且多了两个时钟。
    因此,细心检查一下,凡是向下跳,在128 字元以内,皆应改为JMP SHORT 才是。

首先声明,我不是编译原理领域的高手,我充其量也需只比菜鸟强一点点。

写这篇文章的原因并非我想显耀自己——-再次声明:我不是高手,只是因为不知什么时候我在那个论坛发表了一些关于这方面的言论,并留下了自己的qq号码,于是有一大堆好事者过来询问。这本来是我的荣幸,但从网友的问题中我看到的仍然是空话和废话—-与编译原理的中心并不相关,因此,我发现我应该站出来,把我做的事情讲清楚,也算是对这些热心的人的一种负责任的回报。

首先还是要说一些非话的:所谓的编译器就是对使用某种规则编出来的程序进行翻译,一般而言,特定的语言都必须有特定的编译器,没有什么可以兼容的说法,大家熟悉C语言的编译器,如TurboC。这些规则就纯粹是人为的啦,也正因此,诞生了我们所熟悉的大量的各种各样的高级语言,如Pascal、C、C++、Algol~~~~因此所谓的编译器也就是一堆程序,他们检查你是否按照规则来了,以及充分了解你的要求,并把结果返回给你。因此,编译器要做的事情其实很简单:首先检查语言规则(包括词法规则和语法规则),然后做语义分析,余下的就由计算机里的硬件去执行运算指令了(当然这种说法并不恰当)。

这里还应该讲一讲词法分析和语法分析以及语义分析的大概情况,由于时间关系,我以后再补齐,请见谅。

我这里主要说的是我所作的事情,这也是我的毕业设计:为一个类似C语言的小语言Tiny-C做翻译。词法规则和语法规则都是我自己定的————要求充分接近C语言。本来是雄心壮志,想把他做的完美,一方面由于实力——只能止步于语法分析,一方面由于时间——使得该分析器未能如愿工作。我使用的是自下而上的LR(1)分析法,因此花了我不少的时间和精力啊;为此我写了上10页的项目集,最终得到75个状态,最要命的是画出分析表——一张75×41的表格。其实难度不大,量多了一些而已。

不好意思,我今天只能说到这里了,我会议后把这个补充完整,如果你有什么我可以帮忙的,请E-mail至lgdpeter@163.com,或QQ:23172461

感谢你的阅读!