第三节 程序合并 我所见过的各种组合程序虽不算多,但至少有百余个了。毛病最多的当然是缺乏完 整的规划,其次则是信马游缰,一份不折不扣的流水帐!明明大门口在东边,程序硬要 朝西,直到游完了大观园,天黑了,才出东门! 这种程序我收集了一大叠,可是举来做例子,却心有余而力不足。原因无他,实在 不耐烦照抄一遍,一见到就头痛! 计算机最强的功能,便是处理繁杂重复的工作,为什么一般程序员居然存心与计算 机争风吃醋呢?不说别的,光把程序输入到计算机中,就要花上几个月宝贵的光阴,真 值得这样做吗? 有一份程序,足足有四十多页,我只略作调整,便缩小到十页,处理速度则快了五 倍。为什么会差这样远呢?很简单,有些人不喜欢用大脑,久而久之,习惯成自然,大 脑就生了铁锈!除了等因奉此,什么都不会想了。 要想做一个优秀的程序员,第一个条件是不能偷懒,第二个条件则要有分析观察的 习惯,第三个也是最重要的,则是要有追求完美的精神。程序员要像艺术家,不论是自 己的或是别人的程序,都要一而再、再而三地玩味改良。 我曾见过一个扫地的妇人,她不管在哪里,见不得有任何脏乱。这种人才值得尊敬, 这种精神是伟大的,与她的职业丝毫无关! 程序写得不够精简,有三个原因,第一个是程序员无能,这种程序能够写完,可以 运行,已算相当难得了;第二个原因是不懂技巧,硬桥硬马的干, 不知什么是效率,也 不知道如何达成。自己写的程序都不见得看得懂,遑论他人的?第三则是根本缺乏敬业 精神,敷衍塞责,这种人我最瞧不起。 写程序之初,如果把任务了解清楚,然后分析因素,分割模块。所有类似的情况都 合并到一处,再以变量代替,统一执行。这原本是份内的工作,前述的情况根本不可能 发生! 问题是发生了以后怎么办呢?我建议最好重写,如果一定要改,只好采用程序合并 的技巧,浓缩一下。 合并的目的是为了增进效率,而合并的方法则因情况不同而异,就像人生了病,必 须先查出病因,否则无法下药。我试着以所知道的一些例证,简要地解说如后。 一、过程的合并: 要做过程的合并,首先要查明下列各点: 1,首先找出过程类似的,全部移到一堆,如果找不到,那就没救了。 然而,这种程序要就是太小,根本不可能有类似的情况,再不就是写作时杂乱无章,信 马游缰。分明有类似的过程,但没有共通的原则,无从浓缩。当然,也可能有些程序, 因工作量及处理的细节太多,以致无法浓缩。 2,在类似的程序中,找寻相异的指令或流程,再若没有,那就是重复了,正宜合并。 3,把相异的指令或流程用变量取代,或将不同程序之入口放在寄存器里。 4,将各程序在应用该流程前,设好变量及使用的暂存器。 5,合并相似的程序段,不同处应用变量取代之。 下面举一实例,系一绘图程序之片断,兹改变原用标题,并将分散在各处若干不同 之段,列述如下: 189: MASK PROC NEAR 190: MOV DX,3C4H 191: MOV AL,2 192: OUT DX,AL 193: MOV DX,3C5H 194: MOV AL,PCOLOR 195: OUT DX,AL 196: RET 197: MASK ENDP ... 380: MOV DX,03CEH 381: MOV AL,3 382: OUT DX,AL 383: MOV AL,18H 384: INC DX 385: OUT DX,AL 386: RET ... 490: MOV DX,3CEH 491: MOV AL,3 492: OUT DX,AL 493: MOV DX,3CFH 494: MOV AL,0H 495: OUT DX,AL 496: RET ... 589: CROSS PROC NEAR 590: MOV DX,3C4H 591: MOV AL,2 592: OUT DX,AL 593: INC DX 594: MOV AL,0FH 595: OUT DX,AL 596: RET 597: CROSS ENDP ... 这样的段落有十多处,看来每个都略有不同,似乎不能合并。然而仔细分析,显然 是程序员训练不够,把一个非常有规则的程序,安排得非常紊乱,以致到这个地步。 我们先归纳问题,决定如何合并。第一,上述各段程序,应该统一作为子程序;第 二,全部变量只有四个,其中两个是传送值,两个是输出入埠。后者有连续关系,等于 只有一个。因此,在调用此子程序前,应先令DX为输出入埠,再将变量装入AX中,一次 调用即可。此子程序如下: 300: SUB: 301: OUT DX,AL 302: INC DX 303: MOV AL,AH 304: OUT DX,AL 305: RET 这样简短的子程序,有无必要,端视时空的效益而定。不论怎样整理,都远比原来 的要好。 另外有种情况,更为可怕,就是在键盘输入后,用流程方式,一一比较输入码,再 一一分别处理。 比如说,为了检查游标键的左、右、上、下等八个方向的移动,以便作相应的处理, 程序居然写成: 100: PP1: MOV AH,0 101: INT 16H 102: CMP AX,4800H ;↑键 103: JNE NEXT1 104: CALL MOVDATA ;SET BUFFERS 105: CALL SETDLT ;SET INCREMENT 106: NXT01: 107: CALL DOTUP 108: LOOP NXT01 109: CALL XORDOT ;SET NEW DOT 110: CALL XYDISP ;DISP NEW XXX,YYY : JMP PP1 112: NEXT1: 113: CMP AX,5000H ;↓键 114: JNE NEXT2 115: CALL MOVDATA ;SET BUFFERS 116: CALL SETDLT ;SET INCREMENT 117: NXT02: 118: CALL DOTDOWN 119: LOOP NXT02 120: CALL XORDOT ;SET NEW DOT 121: CALL XYDISP ;DISP NEW XXX,YYY 122: JMP PP1 123: NEXT2: 124: CMP AX,4B00H ;←键 125: JNE NEXT3 ... 这段程序总共要检查八次,才能确定是否有游标移动以及哪个游标在移动。然后, 还要一一检查其它变化,共有十八种有效码。我实在佩服这种程序员,不但有无比的耐 性,还有非凡的想象力,居然能把一段极为简单平凡的程序,写得这样的精彩动人! 如果是我,我会写得毫无趣味: 100: PP1: SUB AH,AH 101: INT 16H 102: OR AL,AL 103: JNZ PP1 ;AL 非0无效 104: MOV BH,AL 105: MOV BL,AH 106: SUB BL,47H ;最小之字标键 107: JLE PP1 ;非处理范围 108: SHL BX,1 109: CALL FUNC[BX] 110: JMP PP1 这是主流程,程序短,速度快,维护容易,一眼看过去,有什么错误立刻分明。 ... 1000: FUNC DW NEXT02 ;↖ 1001: DW NEXT0 ;↑ 1002: DW NEXT04 ;↗ 1003: DW PT ;无效 1004: DW NEXT2 ;← 1005: DW PT ;无效 1006: DW NEXT4 ;→ 1007: DW PT ;无效 1008: DW NEXT12 ;↙ 1009: DW NEXT1 ;↓ 1010: DW NEXT14 ;↘ ... 因为这是子程序,加一段、减一段容易非常。 即使是子程序,也有很大的考究,就以前段来说,在 104至110 之间,就值得三思。 104: CALL MOVDATA ;SET BUFFERS 105: CALL SETDLT ;SET INCREMENT 106: NXT01: 107: CALL DOTUP 108: LOOP NXT01 109: CALL XORDOT ;SET NEW DOT 110: CALL XYDISP ;DISP NEW XXX,YYY 首先,104 和105 会重复多次,109 及110 亦然,为什么不合并为一呢?这也是很 常见的程序合并手法,两次调用合为一次,速度及空间都较为经济。 在子程序 SETDLT 之前,先调用一次 MOVDATA,另XYDISP也是一样,首先备妥: 3000: SETDATA: 3001: CALL MOVDATA ;假设本程序有他用 3002: SETDLT: 3003: ... ... 3100: XYDIDOT: 3101: CALL XORDOT ; 同上 3102: XYDISP: 3103: ... ... 再来设计NEXT0 的子程序: 110: NEXT0: : CALL SETDATA 112: NXT01: 113: DOTUP 应搬至此,无需设为子程序。 ... 120: LOOP NXT01 121: JMP XYDIDOT ; 如有必要,可先 ; 设好参数 这样合并一下,效果决不止高上十倍,等到真正学会了程序的技巧,写作时速度也 可以提高数倍。 二、分支的处理: 分支是程序中不可避免的手段,使用得好,整个程序气势一贯,有行云流水之妙。 前面的例子根本不具分支的条件,故不能算是分支不良,而是程序员观念错误。 下面再举一例,由于分支不良,以致程序支离破碎。这是一则计算拋物线的快速程 序,妙在没有用乘除法,也没有任何函数。其中有几段是这样的: 100: BEG00: 101: CMP BP,BUFY 102:? JLE BE7 103: OR CX,CX 104: JG BE20 105: MOV AX,BP 106:? SHL AX,1 107: DEC AX 108: JL BE10 109: BE2: 110: CALL BE1 : JC BEG00 112: CALL BE3 113: JMP BEG00 ... 120: BE14: 121:? LODSW 122: CMP AH,1FH 123: JGE BE141 124: LOOP BE14 125: POP DI 126: POP CX 127: MOV SI,DI 128: JMP BE142 129: BE141: 130: POP DI 131: POP CX 132: MOV SI,DI ... 150: BE10: 151: CALL BE1 152: JMP BEG00 153: BE20: 154: MOV AX,CX 155:? SUB AX,DX 156: SHL AX,1 157: DEC AX 158: JLE BE2 159:? CALL BE3 160: JMP BEG00 161: BE1: 162:? INC DX 163: ADD CX,DX 164: ADD CX,DX 165: INC CX 166: ADD DI,BUFX 167: CMP DI,BX 168: JLE BE1RET 169: CALL BE01 170: SUB DI,BX 171: BE1RET: 172: RET ... 190: BE01: 191:? MOV AL,1 192: CMP [SI+1],AL 193: JNZ BE011 194: INC BYTE PTR [SI+1] 195: RET ... 200: BE141: ... 全部程序并不大,不过一百多条指令,但是稍加改进,却可以省却廿多条指令,速 度也会加快。重点在于106 到113 的分支错误,以致于多出BE10 BE20 BE3 BE01等段程 序出来。 照理,BE1 BE3 BE01都不该另设子程序,BE14也应改写,如此,整个程序就完全不 同了。 原来由 105为: 105: MOV AX,BP ;★无必要 106: SHL AX,1 ;★无必要 107: DEC AX ;★无必要 108: JL BE10 109: BE2: 110: CALL BE1 ;★合并后,无需调用 : JC BEG00 ;★另作分支 112: CALL BE3 ;★也无必要调用 113: JMP BEG00 现改为: 107: BE1: ;原为DEC AX分支处理 108: INC DX ;原161子程序作主流程 109: ADD CX,DX 110: ADD CX,DX : INC CX 112: ADD DI,BUFX 113: CMP DI,BX 114: JLE BE11 115: ; CALL BE01 ;本子程序重写如下: 116: CMP BYTE PTR[SI+1],1 117: JNE BE1A 118: INC BYTE PTR[SI+1] 119: BE1A: 120: SUB DI,BX 121: JC BEG00 ;原 122: ... ;原BE3 程序 ... 又 125条三个指令也是分支错误,白白浪费。 120: BE14: 121: LODSW 122: CMP AH,1FH 123: JGE BE141 124: LOOP BE14 125: POP DI ;★可以省略 126: POP CX ;★可以省略 127: MOV SI,DI ;★可以省略 128: JMP BE142 ;★可以省略 129: BE141: 130: POP DI 131: POP CX 132: MOV SI,DI ;127移到此 133: JNZ BE142 ;128移到此 134: ...