彙編藝術

By xlfancy
多一字節也嫌多,多一時鐘更嫌慢!           
——引自XXX高人語錄


我總是想這樣去做,可每每照貓畫虎反類犬


以下摘自 老羅的繽紛天地之《組合語言之藝術》
這裏下載 http://asp.7i24.com/netcool/laoluo/collections/index.htm
直接下載 http://asp.7i24.com/netcool/laoluo/collections/downloads/eBooks/Assembly%20Art.chm



第四節 指令應用

--------------------------------------------------------------------------------

    彙編語言可以說是未經整理的、原始的計算機語言,讀者們大可下一番功夫,找出
其應用的規則,以發揮最高的效率。在下面,我僅就個人的經驗,提供一些淺見,以供
切磋研討。
    要寫好程序,首先應熟記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。
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章