作者:不详 来源:互联网 酷勤网收集 2007-11-22
第四节 指令应用
组合语言可以说是未经整理的、原始的电脑语言,读者们大可下一番功夫,找出其应用的规则,以发挥最高的效率。在下面,我仅就个人的经验,提供一些浅见,以供切磋研讨。
要写好程式,首先应熟记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,有共用的价值,于是作为子程式调用。
照上面这段程式,略加改进,写法如下:
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。

