68000双遍汇编子程序:数组均值计算及技术评审问询
背景
为练习编写了一个68000子程序,实现以下功能:
- 计算n元素有符号字数组A的整数均值;
- 统计数组A中严格大于该均值的元素数量。
所有参数(&A、&AvgRes、&CountGT、n)需通过栈传递;主程序入参、调用子程序后一次性清理栈。
测试数据验证程序可正常运行(均值=1,计数=5),现寻求对代码清晰度、健壮性及68k编程惯例的评审。
代码(可在Easy68k中运行)
ORG $8000 ; ================================================================= ; AvgAndCount – integer average of n signed words in A ; + count of elements greater than that average ; Parameters (below return addr): ; 4(A7) = &A (long) ; 8(A7) = &AvgRes (long) ; 12(A7) = &CountGT (long) ; 16(A7) = n (word) ; ================================================================= VectorA DC.W 2,-3,4,-1,0,5,-7,8,-2,6 ; test set (10 words) AvgRes DS.W 1 ; average → here CountGT DS.W 1 ; count → here LEN EQU 10 ; ---------------- main ------------------------------------------------- START: MOVE.W #LEN,-(A7) ; push n (word) PEA.L CountGT ; push &CountGT (long) PEA.L AvgRes ; push &AvgRes (long) PEA.L VectorA ; push &A (long) BSR.S AvgAndCount ; call routine ADDA.L #14,A7 ; pop 3 longs + 1 word SIMHALT ; ---------------- AvgAndCount ----------------------------------------- ; Pass 1: accumulate 32-bit sum → avg = sum / n ; Pass 2: count elements that are > avg ; Uses: A0, A1, D0–D3, D6, D7 ; ---------------------------------------------------------------------- AvgAndCount: ; ----- Pass 1 – sum ---------------------------------------------- MOVE.W 16(A7),D7 ; D7 = n SUBQ.W #1,D7 ; DBRA counter needs n-1 MOVEA.L 4(A7),A0 ; A0 = &A CLR.L D0 ; 32-bit accumulator SumLoop: ADD.W (A0)+,D0 ; add next word to long sum DBRA D7,SumLoop ; ----- integer average = sum / n --------------------------------- MOVE.W 16(A7),D3 ; D3 = n MOVE.W D3,-(A7) ; push divisor (DIVS needs mem op) DIVS (A7),D0 ; 32 ÷ 16 ⇒ quotient in D0.low ADDQ.L #2,A7 ; pop divisor MOVE.W D0,D1 ; D1 = avg MOVEA.L 8(A7),A1 MOVE.W D1,(A1) ; save avg in AvgRes ; ----- Pass 2 – count > avg -------------------------------------- MOVE.W 16(A7),D7 SUBQ.W #1,D7 MOVEA.L 4(A7),A0 ; reset A pointer CLR.W D2 ; counter = 0 CntLoop: MOVE.W (A0)+,D6 CMP.W D1,D6 ; A[i] > avg ? BLE.S Skip ADDQ.W #1,D2 ; ++counter Skip: DBRA D7,CntLoop ; ----- store count ----------------------------------------------- MOVEA.L 12(A7),A1 MOVE.W D2,(A1) RTS END START
技术问询解答
1. 正确性与溢出:当前用D0.L存储10元素的和是安全的,若n更大是否应改用64位(D0:D1)存储?
是的,必须切换到64位累加。单个有符号字的范围是-3276832767,32位有符号长字的范围是-21474836482147483647。当n超过65535,或者数组元素普遍接近极值时,32位累加必然溢出。
68000支持64位累加操作,实现方式是用D0:D1组成双寄存器:
- 初始时
CLR.L D0、CLR.L D1清零; - 每次循环执行
ADD.W (A0)+,D0,再执行ADDX.L #0,D1处理进位(ADDX会把之前ADD产生的进位加到D1中); - 后续除法若需处理64位被除数,可先用
EXT.L D3把n扩展为32位,再用DIVL指令(68020及以上支持),或手动实现64÷16的除法逻辑。
2. DIVS用法:将除数压栈是实现32÷16除法最简洁高效的方式吗?还是将n存入静态内存更好?
压栈是简洁的实现方式,但不是最高效的——栈操作涉及内存读写。更高效的方案是利用栈帧临时单元:用LINK A6,#-2建立栈帧,把n存入-2(A6),然后用DIVS -2(A6),D0,最后用UNLK A6清理栈帧。这种方式避免了额外的栈push/pop操作,且保持子程序的可重入性。
绝对不推荐用静态内存:静态变量是全局的,会导致子程序无法重入,不符合模块化编程的惯例,在多任务或递归场景下会出问题。
3. 可读性与注释:现有的头部块及行内注释是否足以让其他阅读者理解代码?
现有注释已经达标,但可以做以下优化提升可读性:
- 修正参数列表里的HTML转义字符,把
&A改成&A,更直观; - 在
SumLoop和CntLoop前增加一句注释,明确循环的核心作用(比如; 遍历数组,累加所有元素到32位寄存器D0); - 给DIVS操作加注释说明:
; DIVS将32位被除数D0除以16位除数,商存在D0低16位,余数在高16位; - 在寄存器使用说明里,明确标注哪些寄存器会被修改(比如
; 破坏寄存器:A0,A1,D0-D3,D6,D7),方便调用者提前保存需要保留的寄存器。
4. 经典优化点:是否遗漏了经典优化手段(如向上取整、无需复杂除法的单遍计数等)?
(1)单遍遍历优化
无法实现真正的单遍遍历:因为均值依赖总和,必须先遍历一次计算总和才能得到均值,所以两次遍历是必要的。不过可以优化数组指针的重置:如果在Pass1结束时把A0的初始地址存在另一个寄存器(比如A2),Pass2就不用重新从栈里加载地址,直接用MOVEA.L A2,A0即可,减少一次内存访问。
(2)整数均值的向上取整
当前代码用DIVS实现的是向零截断的除法,对于负总和且有余数的情况,结果会偏离数学上的整数均值(比如总和-5,n=2,-5/2=-2,但实际均值应该是-3)。可以增加余数判断修正:
; 在DIVS之后添加 TST.W D0 ; 检查商的符号 BPL.S NoAdjust ; 正数或零,无需调整 TST.W D0+2 ; 检查余数是否非零 BEQ.S NoAdjust SUBQ.W #1,D1 ; 余数非零的负数,均值减1实现向负无穷取整 NoAdjust:
(3)循环效率优化
你已经用到了68000高效的DBRA循环指令,这是最优选择,无需替换。另外,Pass2里的CLR.W D2可以保持,比SUBQ.W #0,D2更直观。
内容的提问来源于stack exchange,提问作者Pato

