组合语言之艺术
第四节 指令应用

  汇编语言可以说是未经整理的、原始的计算机语言,读者们大可下一番功夫,找出
其应用的规则,以发挥最高的效率。在下面,我仅就个人的经验,提供一些浅见,以供
切磋研讨。
    要写好程序,首先应熟记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。

上一页    下一页