【深圳 IO 攻略】番外篇:調(diào)整代碼執(zhí)行順序以節(jié)省行數(shù)

本文首發(fā)于 B 站《深圳 IO》文集(https://www.bilibili.com/read/readlist/rl569860)。原創(chuàng)不易,轉(zhuǎn)載請注明出處。
通常我們都是使用下面的方式來實(shí)現(xiàn)循環(huán)的:
如果進(jìn)入循環(huán)前,一定滿足【終止條件】,那么我們可以把代碼改成下面這樣:
這樣做的壞處無非就是開頭會進(jìn)行一次必然成立的判斷,會多耗費(fèi)一格電。但是帶來的好處可太多了:能省一行代碼,很可能省下這一行代碼就能把?MC6000 替換成?MC4000(X),節(jié)省兩塊錢成本;而且這樣做還能有驚人的省電效果:
如果循環(huán)體只執(zhí)行一次,那么即使修改前的方案也不會執(zhí)行 jmp 指令,修改后的方案由于多了一次必然成立的判斷,要比修改前的方案多耗費(fèi)一格電;
如果循環(huán)體執(zhí)行了兩次,那么修改后的方案因?yàn)槎鄨?zhí)行一次判斷,少執(zhí)行一次 jmp 指令,一正一負(fù)完全抵消,所以耗電量和修改前的方案完全一致;
如果循環(huán)體執(zhí)行了三次以上,那么修改后的方案就必然比修改前的方案要省電。修改前的方案里,執(zhí)行 n 次循環(huán),就需要執(zhí)行 n-1 次 jmp 指令。修改后的方案相比于修改前,多執(zhí)行了開頭的一次必然成立的判斷,但少執(zhí)行了 n-1 次 jmp 指令。總共會省掉 n-2 格電。
考慮到循環(huán)結(jié)構(gòu)幾乎不會出現(xiàn)只執(zhí)行一次循環(huán)體的情況,我們可以認(rèn)為修改后的方案在電量和代碼行數(shù)的指標(biāo)上完爆修改前的方案,甚至可能會有驚喜出現(xiàn)——10 行變 9 行,MC6000?換 MC4000(X),連成本都能給你省下去。
示例 1:第 13 關(guān)《古錢幣付款終端》
jmp 指令最早是在第 13 關(guān)《古錢幣付款終端》里出現(xiàn)的,循環(huán)結(jié)構(gòu)也是從那一關(guān)起出現(xiàn)的。我現(xiàn)在要舉的第一個改進(jìn)的例子也正是這一關(guān)的例子。原版攻略傳送門:【深圳 IO 攻略】第 13 關(guān):古錢幣付款終端

右邊的找零芯片用了兩個小循環(huán),然后我們注意一下第二個小循環(huán):待找零的錢大于 0 時(tcp acc 0)令該錢數(shù) -1(+ sub 1),然后給【找零口 1】發(fā)送 1 秒鐘的脈沖(+ gen p0 1 1),做完這些后跳回第 7 行繼續(xù)判斷(+ jmp 7),直到錢數(shù)變?yōu)?0 為止。
而因?yàn)楹艚姓伊阈酒?,該芯片?acc(待找零錢數(shù))一定是 0,所以進(jìn)入準(zhǔn)備工作前一定滿足終止條件。因此我們就可以使用上面提到的“終止條件前置法”將第二個小循環(huán)前置,以省掉一條 jmp 指令。改動后的代碼如下:


如此,準(zhǔn)備工作由原先的第 1~2?行移動到了第 4~5?行(+ slx x0,?+ mov x0 acc),第一個小循環(huán)由原先的第 3~6 行移動到了第 6~9 行(+ tcp acc 4, + sub 5, + gen p1 1 1, + jmp 6),第二個小循環(huán)由原先的第 7~10 行?移動到了第 1~3 行,且由 4 行代碼減少到了 3 行代碼(teq acc 0, - sub 1, - gen p0 1 1)。
點(diǎn)擊左下角的【模擬】,稍等片刻,便會彈出結(jié)算界面:

可以看到三項(xiàng)指標(biāo)都較上一版方案有所改進(jìn)。我在第 13 關(guān)的攻略里說過這樣的話:
碎碎念
右邊那塊 MC6000 芯片接了兩個 p 口和一個 x 口,寄存器也只用到了 acc。如果能省掉一行代碼,那么就可以毫不費(fèi)力地?fù)Q成 MC4000。但是我苦思冥想,結(jié)果卻無論如何都省不掉哪怕一行代碼。于是這塊 MC6000 芯片換不成 MC4000 了,僅僅只是多一行代碼而已啊,真的好不甘心。不知道讀者們有沒有辦法能省出來一行代碼,有的話請留言告訴我。 作者:ココアお姉ちゃん https://www.bilibili.com/read/cv16915936?出處:bilibili
現(xiàn)在,我使用了“終止條件前置法”省掉了那一行關(guān)鍵的代碼,成功把右邊的芯片換成了 MC4000,本次優(yōu)化屬于驚喜級別。哦耶!
示例 2:第 17 關(guān)《共生環(huán)境維護(hù)機(jī)器人》
原版攻略傳送門:【深圳 IO 攻略】第 17 關(guān):共生環(huán)境維護(hù)機(jī)器人
初版方案

上方芯片的循環(huán)終止條件(teq dat acc)在準(zhǔn)備工作開始前一定滿足,所以這塊芯片也可以使用“終止條件前置法”去掉 jmp 指令。改進(jìn)方案如下:

上方芯片的第一條指令加上了 @ 前綴。其實(shí)前一版方案里,因?yàn)閮蓚€ jmp 的存在,第一條指令事實(shí)上是只執(zhí)行一次的,所以無需加 @ 前綴。但是本方案中去掉了 jmp,第一條指令就必須顯式用 @ 聲明了。
然后,我們將【循環(huán)結(jié)束后的工作】(mov 0 x2)和【下一次任務(wù)的準(zhǔn)備工作】(slx x2, mov x2 dat)都放在了【終止條件】(teq dat acc)的后面。這樣循環(huán)體(tcp 及其之后的所有指令)執(zhí)行完畢后就會自動跳回第 2 行判斷循環(huán)是否結(jié)束,無需使用無條件跳轉(zhuǎn)指令。
這里要注意,由于開局一定滿足【終止條件】,所以即使一次任務(wù)都沒執(zhí)行過,上方的芯片也會執(zhí)行“循環(huán)結(jié)束后的工作”,給下方芯片發(fā)送一個 0。所以下方芯片需要在第一個周期里將上方芯片發(fā)送的這個 0 給丟棄掉,以防阻塞(@ mov x1 null)。
還有一點(diǎn),原先只有一行的睡眠指令(slp 1)變成了兩行(+ slp 1, - slp 1)。本方案的準(zhǔn)備工作做完后,就直接?tcp 了,而不是像前一版方案那樣先 teq 再 tcp。若電機(jī)的當(dāng)前位置已經(jīng)在目標(biāo)位置上,我們必須立刻回傳 0,而不能睡一秒后等到下一秒再回傳。我們在 slp 指令前加上 +、- 號,確保三態(tài)判定給出相等的判定結(jié)果時,不執(zhí)行睡眠指令。
點(diǎn)擊左下角的【模擬】,稍等片刻,便會彈出結(jié)算界面:

成本和代碼行數(shù)都沒有增加,電量卻由 206 降低到了 188。本次使用了“終止條件前置法”的方案完爆了上一版方案。而且,由于上方芯片騰出了一行代碼空間,我們可以將其中一條 gen 指令展開,這樣的話我們多了一行代碼,但可以節(jié)省更多電量。例如,將第十行的
展開成
后,電量還能從 188 進(jìn)一步降低到 172。
示例 3:第 25 關(guān)《肉食打印機(jī)》
原版攻略傳送門:【深圳 IO 攻略】第 25 關(guān):肉食打印機(jī)
hard code 的版本不涉及循環(huán),也就無法以此法來優(yōu)化。我們重點(diǎn)說后一個帶 ROM 的,最省成本和代碼行數(shù)的設(shè)計(jì)方案。
初版方案


循環(huán)終止條件(teq x1 0, - teq x1 7)在準(zhǔn)備工作開始前一定滿足,所以本題可以使用“終止條件前置法”去掉 jmp 指令。改進(jìn)方案如下:

除了將終止條件(teq x1 0, - teq x1 7)前置之外,本方案還做了兩點(diǎn)改動:
循環(huán)結(jié)束后的工作(mov p1 x3,同時清除壓出機(jī)和三路閥信號)和準(zhǔn)備工作放在了一起。上一版方案里第一秒是不清空兩路輸出信號的,因?yàn)闆]必要。本方案為了去掉一行 jmp 指令,第一秒鐘做了額外的清零操作,屬于空操作,但也沒有任何副作用。
原方案里的第 7~8 行代碼(- mul 7, - mov acc x1)也是帶有 - 號前綴的,但是這兩行代碼是屬于準(zhǔn)備工作的階段,不屬于循環(huán)體。本優(yōu)化方案要求準(zhǔn)備工作全部帶上 + 號,因此我將這兩行代碼移動到了判斷 acc 的代碼之前,且改為了 + 號前綴。也就是無論從小鍵盤收到的值是不是 3,都統(tǒng)一 ×7 然后設(shè)為?ROM 地址(+?mul 7,?+?mov acc x1)。因?yàn)樾℃I盤的值無論如何都會被 ×7,所以后續(xù)改為判斷 acc 的值是否為 21(+ teq acc 21)。
點(diǎn)擊左下角的【模擬】,稍等片刻,便會彈出結(jié)算界面:

電量 130→118,代碼行數(shù) 14→13,又完爆了上一個方案。
無法優(yōu)化的情形
當(dāng)循環(huán)結(jié)束后還有收尾工作,同時第一個周期額外多出來的收尾工作又無法方便地消化掉時,則不可以將終止條件前置。例如第 22 關(guān)《加密貨幣存儲終端》:


右邊芯片的第 3~5 行是循環(huán)的部分,但是第 6~7 行還有個收尾工作(把左邊芯片傳來的金額也發(fā)送給輸出端口)。如果強(qiáng)行前置終止條件,那就等于第一秒鐘也要從?x3 口讀數(shù)據(jù)發(fā)給網(wǎng)絡(luò)端口。可是左邊的芯片在第 1 秒鐘里給右邊的芯片傳什么都是錯誤的,本題要求第 1 秒鐘什么數(shù)字都不發(fā)送。
終止條件前置,相當(dāng)于把“本次任務(wù)的收尾工作”和“下一次任務(wù)的準(zhǔn)備工作”合并。你需要特別注意第 1 秒鐘的“收尾工作”會不會導(dǎo)致畫蛇添足。
另外,如果準(zhǔn)備工作的過程中出現(xiàn)了判斷,且判斷之后出現(xiàn)了 - 號分支或不帶前綴的共用代碼,則也不能前置終止條件。因?yàn)樵趫?zhí)行循環(huán)的過程中,不滿足終止條件時,會關(guān)閉 + 號前綴的代碼,只執(zhí)行無前綴及 - 號前綴的代碼。所以 - 號部分的代碼,以及不帶前綴的代碼都是屬于循環(huán)體的,只有 + 號部分的代碼才能用作準(zhǔn)備工作。而一旦在準(zhǔn)備工作中執(zhí)行了判斷,后續(xù)的?- 號前綴的,以及不帶前綴的代碼就會錯誤地成為循環(huán)體的一部分。
例如,上面提到的肉食打印機(jī),因?yàn)樯弦话娣桨咐锏臏?zhǔn)備部分出現(xiàn)了 - 號前綴的代碼,所以必須要想辦法把它們轉(zhuǎn)換成 + 號前綴才能使用該優(yōu)化技巧。
再比如,阿瓦隆城第 7 關(guān)《鈦反應(yīng)堆狀態(tài)》中,最右側(cè)芯片的第二個小循環(huán)就無法前置。因?yàn)樵诖酥俺霈F(xiàn)了 - 號前綴的代碼,以及兩行不帶前綴的,不屬于循環(huán)體的代碼(mul -1, mov x1 dat)。讀者可以自己試一下。

