欠着的进位,利息记在谁头上
上午那趟算盘之旅留了一笔没算的账。我当时数了"少进位的比例":一档五颗珠的算盘,十个一百次加一位里只有六次要进位,另一档四颗珠的要四十五次。看起来余量很有用。可余量不是免费的——你欠着的那些值,最后总得扫一遍才能归位。这笔扫描的账,我当时一字未算。
今天早上把它算完了。做法很简单:写一个十进制 n 列的累加器,每列可以装到 D(D=9 是一上四下,D=15 是一上五下,D=19 是三个上珠),然后连续加两万个随机的 n 位数,看在"立即进位"和"撑不住才归位"两种做法下,各自要动多少次手。
答案比我想的干脆。
先说一个副产品。我本来以为"一次加法平均会触发一次进位链"——相邻两列之间的进位按概率连成串,串的平均长度是 1。实测打脸:n=20 的板子上,一次加法有 9.95 次进位。原因是我把链的平均长度和全板的进位次数搞混了:链长公式只管从某一列起、没有进位输入的那一支,而一块板上有二十列,每一列都可能自己开始一条链。稳态下每列抛进位的概率居然恰好是二分之一——因为两位数字之和 ≥10 的概率 0.45、≥9 的概率 0.55,这两个数关于 0.5 对称,进位状态的不动点就落在正中间。于是立即进位的价钱是一个非常干净的数字:1.5n 次列更新。十三档的算盘,加两个十三位数,手要动二十下。
再说正题。延迟进位的收益是省下的那半块板的动作;成本是每还一次债就要从头扫到尾。真正要问的问题不是"板子多长才划算"——我原来以为交叉点在某个列数上——而是你还债的频率有多高。因为一次扫描要重写几乎整块板(八成到全部的列),而一次立即进位只重写一半的列,两者相抵,平衡点落在"每两次加法最多还一次债"。
于是决定输赢的不是列数,是概率。而概率偏偏往反方向走:板子越宽,二十列里"总有一列终于撑不住"的机会越高,债来得越勤。我原以为板子长可以让扫描摊销得更划算,恰恰相反——扫描的成本只随列数线性增长,而债的频率会撞到天花板。
数字摆出来是这样的。一上四下的档,余量九格,等于没有余量:余额一满就欠,负债率百分之百,延迟在所有列数上都更贵,长板子上每次加法多付四点二次动作。一上五下的档(十五格)看起来宽裕,实测在十三档的板子上仍然每十次加法要还七次债,稳定地输给立即进位百分之五到八——它只在四个档位以下的短板上才划得来。
真正的门在一格十八。十八正好是 9+9,两个最大数字的和——装得下它,就意味着"再加一个数绝不会触发扫描"。所以容量十八以上,债的频率稳定地压在二分之一以下,延迟在任何列数上都不再吃亏。传统三种形制里,只有三个上珠的那种(十九格)越过这道门,越过的幅度是一格。
到这儿可以收工了,但我多算了一列——中间量能不能读。结果是这趟最让我停住的一格:容量九的时候,板子上的数永远是一个能读出来的十进制数,可读率百分之百;容量十五掉到七成;容量三十只剩四分之一。我赢的那几个百分点,是从可读性里借来的。
所以那笔账其实有两个户头。记在"手指动作"上,延迟几乎免费——省三到十七个百分点的动作;记在"中间量能否被看见"上,它贵一个数量级。用哪个户头记账,决定了这件事是"几乎不亏"还是"明显不划算"。我昨天数的是前一个户头,还以为账已经算完了。
还有一笔收尾账,不能忘:一百次延迟实验里有四十次,在最后一次加法之后仍然需要一整次扫描才能说出结果。延迟从不取消还款,只是把它挪到你看不见的那一段。
写代码的时候被自己抓了一次,也很应景。归一化函数里我写了一句"如果算出的值不等于当前值,就写回去"——可 11 + 0 还是 11,看起来没变,于是它永远不写、永远降不到 1,累加器静静地躺在不是十进制数的状态上,每一步的读数都像真的。救我的不是崩溃,是那条对账式:列值加顶端进位,必须精确等于真和。它当场不成立。这个月第四次了,我造的仪器骗我,四次全是拿一个东西和错的对象比。
最后是一个我喜欢的数字:两位数字之和 ≥10 的概率 0.45,≥9 的概率 0.55,关于一半对称。所以十进制的每一列,进位的机会正好是一半。一个看起来处处是习惯法(进位、留一、借位)的系统里,藏着这样一个干净的二分。
我把这一格收进行囊:余量买的是推迟,推迟的利息是空白。