欢迎光临散文网 会员登陆 & 注册

【深圳 IO 攻略】番外篇:调整代码执行顺序以节省行数

2022-07-26 23:05 作者:ココアお姉ちゃん  | 我要投稿

本文首发于 B 站《深圳 IO》文集(https://www.bilibili.com/read/readlist/rl569860)。原创不易,转载请注明出处。

通常我们都是使用下面的方式来实现循环的:

如果进入循环前,一定满足【终止条件】,那么我们可以把代码改成下面这样:

这样做的坏处无非就是开头会进行一次必然成立的判断,会多耗费一格电。但是带来的好处可太多了:能省一行代码,很可能省下这一行代码就能把 MC6000 替换成 MC4000(X),节省两块钱成本;而且这样做还能有惊人的省电效果:

  • 如果循环体只执行一次,那么即使修改前的方案也不会执行 jmp 指令,修改后的方案由于多了一次必然成立的判断,要比修改前的方案多耗费一格电;

  • 如果循环体执行了两次,那么修改后的方案因为多执行一次判断,少执行一次 jmp 指令,一正一负完全抵消,所以耗电量和修改前的方案完全一致;

  • 如果循环体执行了三次以上,那么修改后的方案就必然比修改前的方案要省电。修改前的方案里,执行 n 次循环,就需要执行 n-1 次 jmp 指令。修改后的方案相比于修改前,多执行了开头的一次必然成立的判断,但少执行了 n-1 次 jmp 指令。总共会省掉 n-2 格电。

考虑到循环结构几乎不会出现只执行一次循环体的情况,我们可以认为修改后的方案在电量和代码行数的指标上完爆修改前的方案,甚至可能会有惊喜出现——10 行变 9 行,MC6000 换 MC4000(X),连成本都能给你省下去。

示例 1:第 13 关《古钱币付款终端》

jmp 指令最早是在第 13 关《古钱币付款终端》里出现的,循环结构也是从那一关起出现的。我现在要举的第一个改进的例子也正是这一关的例子。原版攻略传送门:【深圳 IO 攻略】第 13 关:古钱币付款终端

右边的找零芯片用了两个小循环,然后我们注意一下第二个小循环:待找零的钱大于 0 时(tcp acc 0)令该钱数 -1(+ sub 1),然后给【找零口 1】发送 1 秒钟的脉冲(+ gen p0 1 1),做完这些后跳回第 7 行继续判断(+ jmp 7),直到钱数变为 0 为止。

而因为呼叫找零芯片前,该芯片的 acc(待找零钱数)一定是 0,所以进入准备工作前一定满足终止条件。因此我们就可以使用上面提到的“终止条件前置法”将第二个小循环前置,以省掉一条 jmp 指令。改动后的代码如下:

如此,准备工作由原先的第 1~2 行移动到了第 4~5 行(+ slx x0, + mov x0 acc),第一个小循环由原先的第 3~6 行移动到了第 6~9 行(+ tcp acc 4, + sub 5, + gen p1 1 1, + jmp 6),第二个小循环由原先的第 7~10 行 移动到了第 1~3 行,且由 4 行代码减少到了 3 行代码(teq acc 0, - sub 1, - gen p0 1 1)。

点击左下角的【模拟】,稍等片刻,便会弹出结算界面:

可以看到三项指标都较上一版方案有所改进。我在第 13 关的攻略里说过这样的话:

碎碎念

右边那块 MC6000 芯片接了两个 p 口和一个 x 口,寄存器也只用到了 acc。如果能省掉一行代码,那么就可以毫不费力地换成 MC4000。但是我苦思冥想,结果却无论如何都省不掉哪怕一行代码。于是这块 MC6000 芯片换不成 MC4000 了,仅仅只是多一行代码而已啊,真的好不甘心。不知道读者们有没有办法能省出来一行代码,有的话请留言告诉我。 作者:ココアお姉ちゃん https://www.bilibili.com/read/cv16915936 出处:bilibili

现在,我使用了“终止条件前置法”省掉了那一行关键的代码,成功把右边的芯片换成了 MC4000,本次优化属于惊喜级别。哦耶!

示例 2:第 17 关《共生环境维护机器人》

原版攻略传送门:【深圳 IO 攻略】第 17 关:共生环境维护机器人

初版方案

上方芯片的循环终止条件(teq dat acc)在准备工作开始前一定满足,所以这块芯片也可以使用“终止条件前置法”去掉 jmp 指令。改进方案如下:

上方芯片的第一条指令加上了 @ 前缀。其实前一版方案里,因为两个 jmp 的存在,第一条指令事实上是只执行一次的,所以无需加 @ 前缀。但是本方案中去掉了 jmp,第一条指令就必须显式用 @ 声明了。

然后,我们将【循环结束后的工作】(mov 0 x2)和【下一次任务的准备工作】(slx x2, mov x2 dat)都放在了【终止条件】(teq dat acc)的后面。这样循环体(tcp 及其之后的所有指令)执行完毕后就会自动跳回第 2 行判断循环是否结束,无需使用无条件跳转指令。

这里要注意,由于开局一定满足【终止条件】,所以即使一次任务都没执行过,上方的芯片也会执行“循环结束后的工作”,给下方芯片发送一个 0。所以下方芯片需要在第一个周期里将上方芯片发送的这个 0 给丢弃掉,以防阻塞(@ mov x1 null)。

还有一点,原先只有一行的睡眠指令(slp 1)变成了两行(+ slp 1, - slp 1)。本方案的准备工作做完后,就直接 tcp 了,而不是像前一版方案那样先 teq 再 tcp。若电机的当前位置已经在目标位置上,我们必须立刻回传 0,而不能睡一秒后等到下一秒再回传。我们在 slp 指令前加上 +、- 号,确保三态判定给出相等的判定结果时,不执行睡眠指令。

点击左下角的【模拟】,稍等片刻,便会弹出结算界面:

成本和代码行数都没有增加,电量却由 206 降低到了 188。本次使用了“终止条件前置法”的方案完爆了上一版方案。而且,由于上方芯片腾出了一行代码空间,我们可以将其中一条 gen 指令展开,这样的话我们多了一行代码,但可以节省更多电量。例如,将第十行的

展开成

后,电量还能从 188 进一步降低到 172。

示例 3:第 25 关《肉食打印机》

原版攻略传送门:【深圳 IO 攻略】第 25 关:肉食打印机

hard code 的版本不涉及循环,也就无法以此法来优化。我们重点说后一个带 ROM 的,最省成本和代码行数的设计方案。

初版方案

循环终止条件(teq x1 0, - teq x1 7)在准备工作开始前一定满足,所以本题可以使用“终止条件前置法”去掉 jmp 指令。改进方案如下:

除了将终止条件(teq x1 0, - teq x1 7)前置之外,本方案还做了两点改动:

  1. 循环结束后的工作(mov p1 x3,同时清除压出机和三路阀信号)和准备工作放在了一起。上一版方案里第一秒是不清空两路输出信号的,因为没必要。本方案为了去掉一行 jmp 指令,第一秒钟做了额外的清零操作,属于空操作,但也没有任何副作用。

  2. 原方案里的第 7~8 行代码(- mul 7, - mov acc x1)也是带有 - 号前缀的,但是这两行代码是属于准备工作的阶段,不属于循环体。本优化方案要求准备工作全部带上 + 号,因此我将这两行代码移动到了判断 acc 的代码之前,且改为了 + 号前缀。也就是无论从小键盘收到的值是不是 3,都统一 ×7 然后设为 ROM 地址(+ mul 7, + mov acc x1)。因为小键盘的值无论如何都会被 ×7,所以后续改为判断 acc 的值是否为 21(+ teq acc 21)。

点击左下角的【模拟】,稍等片刻,便会弹出结算界面:

电量 130→118,代码行数 14→13,又完爆了上一个方案。

无法优化的情形

当循环结束后还有收尾工作,同时第一个周期额外多出来的收尾工作又无法方便地消化掉时,则不可以将终止条件前置。例如第 22 关《加密货币存储终端》:

右边芯片的第 3~5 行是循环的部分,但是第 6~7 行还有个收尾工作(把左边芯片传来的金额也发送给输出端口)。如果强行前置终止条件,那就等于第一秒钟也要从 x3 口读数据发给网络端口。可是左边的芯片在第 1 秒钟里给右边的芯片传什么都是错误的,本题要求第 1 秒钟什么数字都不发送。

终止条件前置,相当于把“本次任务的收尾工作”和“下一次任务的准备工作”合并。你需要特别注意第 1 秒钟的“收尾工作”会不会导致画蛇添足。

另外,如果准备工作的过程中出现了判断,且判断之后出现了 - 号分支或不带前缀的共用代码,则也不能前置终止条件。因为在执行循环的过程中,不满足终止条件时,会关闭 + 号前缀的代码,只执行无前缀及 - 号前缀的代码。所以 - 号部分的代码,以及不带前缀的代码都是属于循环体的,只有 + 号部分的代码才能用作准备工作。而一旦在准备工作中执行了判断,后续的 - 号前缀的,以及不带前缀的代码就会错误地成为循环体的一部分。

例如,上面提到的肉食打印机,因为上一版方案里的准备部分出现了 - 号前缀的代码,所以必须要想办法把它们转换成 + 号前缀才能使用该优化技巧

再比如,阿瓦隆城第 7 关《钛反应堆状态》中,最右侧芯片的第二个小循环就无法前置。因为在此之前出现了 - 号前缀的代码,以及两行不带前缀的,不属于循环体的代码(mul -1, mov x1 dat)。读者可以自己试一下。


【深圳 IO 攻略】番外篇:调整代码执行顺序以节省行数的评论 (共 条)

分享到微博请遵守国家法律