二、程式要條理通順
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+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]
第五節 分支處理
比較資料後,作條件分支 (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 才是.