思考的表象与本质六部曲(6册)
上QQ阅读APP看书,第一时间看更新

也许你们已经发现,我前面已多次提到计算机,而在后面的章节,我还会写到更多有关它的内容。今天,计算机无疑已经成为了我们最有力的思考工具,这不只是因为它们承担起了单调繁重的苦差事,解放了智力劳动,而且,计算机科学家们发明出的许多概念也凭借着自身的实力成为了极好的思想工具。

如今,我们整日都在计算机术语(例如硬件、软件、带宽或者千兆赫)的浪潮中浸泡着,所以我们想当然地就会认为自已对这类专业术语的理解绝对准确可靠。然而,直到在课堂上真正列出了几个这样的词语时,我才发现,虽然每个孩子也都装模作样地点着头,但他们对这些词的理解程度却实在是参差不齐,有时这些孩子们甚至会将我所传达的意思曲解到几近诡异的地步,以致我自己都被弄糊涂了。那么接下来,我首先想要教给大家的是,怎样为世界上最简单的计算机编写程序。

现在,如果你肯花费时间和精力去掌握一点基础的计算机编程技巧,那么你将会对我下面所说的这些有更深刻的体会,而如果你现在已经是经验丰富的计算机专业人士,那就希望我解释问题的方法能在你向普通人说明问题时发挥一点儿作用,或者跳过这一段也可以。我曾尝试对数百名有“计算机恐惧症”的本科生讲授下面的内容,效果不错:即使是那些宁肯背诵一整页电话号码也不愿意解决一道难题的人也不得不承认,能让一台超级简单的计算机按照我们的意愿做事是一件让人极有成就感的事。而当你完成了下文中的所有练习后,你就将掌握有关计算机能力的7个秘密。

24
计算机施展魔法的7个秘密
部分之和大于总体的最好实例

算机隐藏的那种巨大能量一定会被几个世纪之前的人们看作神迹,看作“真正的魔法”。不过,尽管很多计算机程序都复杂得吓人,但它们全都是由一些基本的步骤组成的,仅凭几个简单的术语我们就能把这些步骤解释清楚。计算机上的运算从不夹杂任何神秘成分,作为思考工具,这正是它的重要价值之一,而且解释这件事儿本身就是一个很有趣的哲学问题。到底计算机是怎样施展它的“魔法”的?能在一个比较基础的层面上理解这个问题非常必要,这一章会提供给我们一些相关启示。

先从一台我们能想到的最简单的计算机——一台寄存器机开始吧,看看它有什么能力以及为什么它会有这样的能力。然后,我们再去考察图灵机和冯·诺依曼机。其实它们也只是一些更高效率的寄存器机而已,笔记本电脑能做的事情寄存器机也都能做,只是你得用它算上几个世纪。之后,我们才能慢慢地搞清楚,更高级的那些计算机“架构”如何在寄存器机这台本体机上增长了速度、壮大了技能,而人类大脑正是其中最重要也是最有趣的一种架构。

等一下,我从没说过“大脑就是一部庞大的计算机”,没有,一次都没有。我想说明的是,如果大脑真的是一部庞大的计算机,那么我们就一定能够找到一种方法将大脑所有的活动都解释清楚,不留下任何神秘色彩。我们采用的这种方法就是逆向工程:探究一个复杂的系统,分析它能做什么,是怎样做到的。逆向工程让我们知道了,心脏是如何像一个泵那样履行自己的职责,肺又是怎样收集氧气排放二氧化碳的。神经科学是逆向工程应用于大脑研究的一种尝试。我们都知道大脑能做什么:预测、支配、记忆、学习,接下来还需要我们搞清楚的是,大脑是怎样做到这些的。

这是个充满争议的话题。2000年,小说家汤姆·沃尔夫(Tom Wolfe)曾在自己的文章《抱歉,你的灵魂已死》(Sorry, But Your Soul Just Died)中详细描述过这一敏感话题,随即便引起了一场激烈的论辩。如果我们不想把时间都浪费在那些雄辩和谴责上,而是要真正探索这个艰险的领域,我们还需要一些更锐利的思考工具。我们的大脑是否真的具有并利用了计算机永远都无法企及的那些神奇能力呢?要想有根据地解决这个问题,我们首先得知道计算机都能做些什么,它们是怎么做到的。而要想圆满地证明“我们的大脑不是,也不会是一台计算机”,可用的方法是:(1)证明大脑某些“活动部件”所参与的信息处理工作是计算机无法模仿的;或者(2)证明我们平日熟悉和喜爱的那些精神壮举并不能由组合、汇总计算机部件的简单工作或通过任何计算机形式的处理方式来实现。

除了哲学家外,还有神经科学家、心理学家、语言学家,甚至物理学家都认为这种人类大脑的“计算机隐喻”极具误导性,因为大脑显然能够做很多计算机不能做的事情。他们通常(但也不绝对)先对“计算机是什么”或者“应该是什么”这个问题规划出一个很天真的预设,最后就得出了这个明显但又不相干的命题,即大脑可以做很多笔记本电脑不能做的事情,因为笔记本电脑没有充足的传感器和效应器,内存低,运行速度也有限。而事实上,关于“通常情况下计算机能做什么”这个问题,如果真要回答,我想我们需要弄清楚的应该是:在通常情况下,计算机的能力来自哪里,是怎样得以发挥的?

1957年,那还是计算机时代的初期,王浩发明了寄存器机这个神奇的东西。他是逻辑学家,是库尔特·哥德尔的学生,顺便提一句,他也是一位哲学家。寄存器机是一件简洁的思考工具,每个人的工具箱里都该有这么一件,可惜它至今还仍未被大家所熟知。(37)寄存器机是理想化的、假想出的计算机,它只是由有限数量的寄存器和一个处理单元构成的,具有十足完美的合理性。

寄存器是一些存储单元,每个寄存器都有自己各自的地址,比如寄存器1、寄存器2、寄存器3,以此类推,它的内容包含一个整数,如0、1、2、3,……。寄存器就像是一个一个的大盒子,里面可以放置任意多的(从0到无限多个)豆子。不管这些盒子有多大,而我们总是希望这些盒子能无穷大,因为那样它们才能装上无限大的整数。但我们对大盒子的这些设定是有目的的。

处理单元只具备三种简单的能力:它只能“遵循”三种“指令”,一次执行一条。每一串指令构成的序列都是一个程序,而每一条指令由一个数字来标记,即给每条指令分配一个ID号。这三条指令分别是:

结束。表示将自己停止或者关闭。

增量。将寄存器n中的数字加1,或者说在盒子n中再填入一粒豆子,然后进入下一步,步骤m。

减量。将寄存器n中的数字减1,或者说从盒子n中拿走一粒豆子,然后进入下一步,步骤m。

减量指令与增量指令的运行原理基本相同,只是减量指令中多了一个非常重要的难点:如果寄存器n中的数字是0该怎么办?我们已经无法再从中减1,因为寄存器中不能持有负的整数,正如你无法从一个空盒子中再拿走一粒豆子,面临如此窘境只得另寻它路。办法是:跳转到其他地方运行下一步指令。每一个减量指令都需要在程序中列出那个接受跳转的位置,以应对当前寄存器数字为0的情况。所以,减量的完整定义应该是:

减量。将寄存器n中的数字减1,如果可以执行则继续进行步骤m,如果寄存器n不能减1,则跳转至步骤p。

一部寄存器机能做的所有工作,简单表示就是:结束、增量、减量(或者跳转)。

乍一看,你会觉得这台机器的工作实在有些无聊,只做着一些把一粒豆子放进盒子里或者从盒子里拿出一粒豆子的工作(只要它能找到一粒豆子的话,如果找不到,它会跳转到另一条指令)。但事实上,它可以完成一台计算机能做的所有运算。

让我们从简单的相加开始吧。假设你想让寄存器机将某一个寄存器(比如寄存器1)里的数字加到另一个寄存器(寄存器2)里的数字上。如果寄存器1中的数字是“3”,寄存器2中的数字是“4”,那么,我们要设计出的程序就是:相加结束后,寄存器2中的数字为“7”,因为3+4=7。这个程序用简单的RAP(Register Assembly Programming,寄存器汇编程序)语言编写出来就是:

程序1:相加[1,2]

前两个指令构成了一个循环,寄存器1每减一次,寄存器2就加一次,直至寄存器1中的值变成0,处理单元能“注意”到寄存器中数值为0这一点,继而会将运行步骤跳转到3,即寄存器机停止工作。处理单元并不知道寄存器中的数字,它只能识别出寄存器中数字为0的情况。用盒子和豆子来打个比方:把处理单元想象成是一个盲人,它看不见盒子(寄存器)里面有多少豆子,但是通过探测,它可以识别出盒子空了。所以,尽管事实上处理单元不知道寄存器里确切的内容,但只要步骤1开始运行,寄存器1的内容(不管寄存器1中的数字是几)就会累加到寄存器2里(不管寄存器2中的数字是几),然后停止。(你能知道它为什么会这样运作吗?多试几个例子看看。)我们甚至可以这样说:即使不知道哪两个数相加,不知道数字是什么,甚至也不知道加法是什么,寄存器机也能完美地完成累加计算!

练习1

a.运用程序1,想一下寄存器机需要几步可以计算出2+5=7?(“结束”也算是一步。)

b.要计算出5+2=7需要几步完成?(你从中可以得出什么结论?)(38)

有一种图解可以很好地说明这一运算过程,我们称之为流程图。圆圈中的数字代表将要接受运算的寄存器的地址(它不是寄存器中的内容),“+”代表增量,“-”代表减量。程序从α(alpha)开始,至Ω(omega)处停止。箭头表示进入下一指令。请注意,每个减量指令处都会向外引出两个箭头,可以减量时有箭头指向一个步骤,而无法减量时(当寄存器中的内容为0时,遇0跳转)则有另一个箭头指向另外一个步骤。

现在,我们就来编写一个程序,将一个寄存器中的内容移动至另外一个寄存器中。

程序2:移动[4,5]

流程图如下所示:

可以看到,第一个循环是清空寄存器5。这样一来,不管一开始寄存器5中的数字是多少,它都不会影响到在第二个循环中要移进来的内容——第二个循环正是增量运算的环节,将寄存器4中的内容添加到已清空的寄存器5中去。这种初始化步骤被称作寄存器清零。它是一项非常有用的标准化运算,我们会不断用到它,好让寄存器能重新投入使用。

第三个程序是,简单地将一个寄存器中的内容复制到另一个寄存器中,被复制的那个寄存器中的内容保持不变。仔细观察这个流程图,思考它的程序设计:

程序3:复制[1,3]

显然,这是完成拷贝的一条迂回路线。首先,我们要将寄存器1中的内容移动到寄存器3中,同时比照3中的内容做一个副本,将它复制到寄存器4中。最后,再将寄存器4中的内容重新移动到寄存器1中——也就是将寄存器4中的内容复制回了寄存器1。程序就这样按部就班地运作,不管一开始寄存器1、3、4中的内容是什么,到程序结束时,寄存器1中一定保留着自己原来的内容,复制的内容保存在了寄存器3中。

如果你认为自己对这个程序的运作方式还不是很清楚,那么你可以尝试“手动模拟”整个过程。取出几个杯子充当寄存器(用铅笔在每个杯子上写一个数字,这是它们各自的地址),再找一些硬币或是豆子。在每个寄存器里都放置几枚硬币,然后记录下寄存器1和寄存器3中硬币的数目。接下来,依照程序按部就班地执行完毕,你会发现,寄存器1中硬币的数量与先前一致,而寄存器3中也拥有了与寄存器1数目相同的硬币。能将累加寄存器的基本运作过程都吸收内化是非常有必要的,我们得做到无需再去费劲思量它们。那么现在,就让我们先用几分钟去变成一部寄存器机吧(就像一个演员要去饰演哈姆雷特那样),因为接下来还要用到这种技能。

我发现好多学生常会犯这样的错误:他们认为,在做寄存器减量时,硬币从一个寄存器里取出之后就一定得放入到另一个寄存器中去。这是不对的。减量取出的硬币只需放回到那一大堆硬币中去即可,在简单的加减法套路中,有“无限多”的硬币可供你使用。

掌握了移动、复制还有归零这些技能之后,我准备重新回到之前的加法程序,对它进行改进。之所以要这样做是因为,程序1虽然成功地将加法运算的结果存进了寄存器2中,但在运行过程中,寄存器1和寄存器2中原有的内容却受到了破坏。我要设计出一个更加专业的加法程序,它能保留住原有的那些数值以备后用,将得出的结果放到另外一个地方。现在就让我们来思考这个程序,将寄存器1的内容与寄存器2的内容相加,两个寄存器中的数值保持不变,将结果显示在寄存器3中。

下面是完成这一程序的流程图:

现在,我们就来分析一下各个循环,看看每一步是怎样运行的。首先我们要将显示最终答案的寄存器3归零,同时清零那个空闲的寄存器4,让它充当临时的接受器或者缓冲器。然后,我们把寄存器1中的内容复制到寄存器3和寄存器4中,随后将寄存器4,也就是缓存器中的内容再次移动到寄存器1,让它恢复原有的数据;在这个过程中,作为缓冲器的寄存器4再次被清零,以备后用。接下来,我们将上一步的寄存器1替换为寄存器2再执行一遍,将寄存器2的数据加到寄存器3的数据上。待整个操作完成后,缓存器(寄存器4)清零,加法答案显示在寄存器3中,同时,我们选用的两个加数也保留在了原来的出处——寄存器1和寄存器2里。

将这套由13个步骤构成的RAP程序改写成下面的流程图,可供处理单元读录:

程序4:无损加法[1,2,3]

我不建议你拿杯子和硬币去手动模拟这个程序。人生苦短,一旦你把这些基本步骤都刻入脑海,那就可以切实享受一下“义肢”给你带来的便利了,这副义肢叫RodRego,你可以从网站http://sites.tufts.edu/rodrego/上下载到这部寄存器机。

最初的寄存器机RodRego的网站首页,1986年。

RodRego有PC、Mac(39)两个版本可在您的计算机上运行。20多年前,我和乔治在课程软件工作室里研发了这件思考工具,如今,成百上千的学生和其他行业的人员通过它成为了熟练的寄存器机思考者。你可以将自己编写的RAP程序键入电脑,观察它们如何运行,不管寄存器中的内容是数字还是豆子。另外,有的多媒体软件还可以顺着流程图将处理单元的程序路径淋漓尽致地展现出来,让你可以清楚地看到RAP指令与流程图各环节之间的相互对应。

接下来,我们再来看看减法。这是我的第一次尝试,从寄存器1中减掉寄存器2中的内容,将答案显示在寄存器4中。你能看出我这个流程图存在的问题吗?

这个流程图只有在寄存器1中的内容大于寄存器2的内容时才能正常工作。但如果实际情况不是这样,又该怎么办呢?寄存器1在不断减量的过程之中就已被“清零”,那接下来会发生什么?此时我们不能中止电脑运行,因为那样的话,寄存器4中会留下一个错误的答案“0”。事实上,在寄存器1归零时,我们可以开启一个新的程序,它会先倒退半个循环,将寄存器2中最后减去的1再加回来。此时,将寄存器2中(而不是寄存器1)的数字转化成负数,所得的结果就是正确答案。所以我们只需把此时寄存器2中的数字移动至寄存器4中(之前,其中的内容已清零),再将答案标注为负数即可。我们还需要另外一个寄存器,比如说寄存器3。与寄存器4一样,寄存器3也要首先清零,然后,通过程序设计,它要像一面“旗帜”那样为答案做标记,内容为0时表示正号“+”,为1时表示负号“-”。下面就是这个程序的流程图,各个循环和步骤都有相关注释。(这些注释也可以写入RAP程序中供你以及其他人参考阅读,你要用“#”在其首尾做出标记,这样RodRego就会自动忽略掉它们)。

练习2

a.请为这个流程图写出它的RAP程序。(请注意,由于程序有不同的分支,你可以采用不同的顺序为各步骤编号,只要“下一步”对应的步骤编号填写正确即可。)

b.如果从3中减3,或者从4中减4,这个程序会出现哪些不同?

c.将寄存器3在第三步之前就清零而不是推延到完成第四步之后,这样会避免哪些可能的错误?

熟练掌握了加法和减法的编程以后,再设计乘除运算就易如反掌了。n乘以m表示m个n相加。我们可以这样来设计程序:用一个寄存器作为计数器,从m递减到0,每完成一次加n的运算就减去1。

练习3

a.画一个流程图或者写一个RAP程序,让寄存器1中的数字与寄存器3中的数字相乘,将答案显示在寄存器5中。

b.(可选)(40)利用复制和移动改进你上面写出的乘法编程,使寄存器1与寄存器3在程序停止后都能保持原有的内容,方便你在运算完成后清楚地检查输出和输入的内容正确与否。

c.(可选)画一个流程图或者写一个RAP程序,要求它能识别寄存器1与寄存器3中的内容(同时保证这些内容不受损坏),将内容较大的寄存器地址(1或3)显示于寄存器2;当两个寄存器中的内容相等时,寄存器2中显示2。(程序完成后,寄存器1和寄存器3中的内容要保持不变,寄存器2显示出所含数字较大的寄存器地址,当内容大小相等时,寄存器2将报出数字2。)

与乘法类似,除法运算只需要从被除数中一次一次不断地减去除数并记录下相减的次数即可。如果有余数,我们可以留出一个专门的寄存器储存它。另外,我们还要小心地添加一个必要的检查项目:看看除数是不是0(0不能作除数,请问这是为什么?)。所以,在进行除法运算之前,我们得对除数作一个简单的核查——看看它能否被减量。如果可以,我们还必须再安排一次增量,以便使除数恢复原值,让除法运算继续进行。如果在进行减量核查时,除数真的是0,那就需要拉响警报了。为此,我们可以专门准备一个寄存器负责报错:寄存器5里如果有1出现就表示,“快!我就要除以0了!”

下面就是一个除法流程图,用寄存器1中的内容除以寄存器2中的内容,将答案写入寄存器3,余数显示在寄存器4,醒目的寄存器5用来提示“错误信息”(显示1表示“我将要除以0”)。

顺着这幅流程图梳理,你就能看到“除数为0”是如何中止掉整个运算的,报错的旗帜究竟是如何竖起的。另外还需要注意的是,寄存器4身兼双重职责,不仅要复制并恢复除数数据,以便下次继续成功地减量,而且,还要时刻准备着成为一个余数寄存器。如果在寄存器4将自己的内容倾倒回寄存器2以备下次减量之前,寄存器1中的内容就已经为0,那么留在寄存器4中的数字就是余数,它也正好显示在了余数寄存器中。

秘密1

发挥能力,无需理解力:有些东西——如一台寄存器机——它能完成精密的运算,但无需理解自己在做什么。

寄存器机没有头脑,因而它不能理解。但因为一碰上增量、减量、停这三条“指令”,它就会老老实实地执行,所以我们也有理由说,寄存器机是近似于明白这三件事情的。不过这三条指令不是真正的指令——它们只对于我们来说像是指令,但对于寄存器机,它们只是近似于指令而已。

也许你已经看到,减量跳转分支对寄存器机起着至关重要的作用。正是这个指令让计算机(近似)“注意”到了这个世界,继而它开始用这些注意到的事情指导自己之后的“行为”。其实这种条件跳转分支对所有存储程序计算机都是至关重要的。埃达·洛夫莱斯(Ada Lovelace)早在19世纪就认识到了这一点,这从她关于查尔斯·巴贝奇(Charles Babbage)的分析机的精彩论证中就可以看出。而分析机正是所有现代计算机的原型。(41)

其实,只要掌握一定的诀窍,组合这些程序就会变得轻车熟路。更何况,运算程序一经写成便可以多次重复地使用。现在,我们对各个运算程序进行编号,运算0是相加,1为相减,2是相乘,以此类推。复制可以是运算5,移位是6,等等。随后,当我们再次使用寄存器机时,就完全可以用数字来发号施令了。

练习4(可选)

写出一个流程图及它的RAP程序,用来将寄存器机转变成一台便携式计算器。

a.寄存器2用来执行运算:

0=相加

1=相减

2=相乘

3=相除

b.在寄存器1和3中放入参与运算的数据。

那么“306”就表示“3 + 6”,“513”表示“5 - 3”,“425”表示“4×5”,“933”表示“9÷3”。然后,我们用寄存器4、5、6、7来显示操作结果:寄存器4用于表示符号,其中0代表+,1代表-;寄存器5显示答案的数值;寄存器6用于存放除法结果的余数;寄存器7表示警示,即对输入的内容进行报错,例如除数为0,或者寄存器2中出现了没有定义的操作的情况。

在这个例子中,寄存器中的内容(其中的数字)起到了四种不同的作用:表示一个数字,表示一种算数运算,表示数字的正负号,还有充当报错旗帜。

秘密2

寄存器中的数字会代表什么取决于我们所编写的是什么样的程序。

利用已经创造出的构造模块,我们可以建造出更厉害的操作程序。所以,只要有信心,我们就能写出一个程序并画出它们的流程图,例如:计算出寄存器7中数值的平方;或者是,算出从寄存器1到寄存器20这20个数字的平均数;或者,分解寄存器6中的整数,如果5是其中的一个因数,就在寄存器5中放入一个1;或者是比较寄存器3和4中数值的大小,将较大的内容放入到寄存器5中,但假如它正好大出一倍,就用寄存器7示警。

我们可以写出一个程序,它能检索100个寄存器,找出具有特定内容的那个,并把所在寄存器的地址写入寄存器101中。这是怎么做到的呢?先将那个特定的数字存入寄存器102,然后将它复制一份存入寄存器103。清零101,然后从寄存器1开始与寄存器103中的数字进行减法运算,被减数是寄存器103中的数字,但每次减量前都首先要在寄存器101中增量记录一次。需要找到的是减去特定数字后结果是0的那个寄存器。不是寄存器1就继而再检测寄存器2,以此类推,直到检验出相减结果为0的那个寄存器,这时减法运算停止,该寄存器地址也正好写入了寄存器101。在减量运算时,寄存器能及时“注意到”0的出现,正是这种基本的“感知力”使我们得以将寄存器机的“目光”转向它自身,让它也尝试检测自己的寄存器单元:来回移动其中的内容,在需要的地方切换操作,等等。

秘密3

寄存器中的数字可以表示任何事物,这说明,寄存器原则上也可以处理任何事物,它们可以“识别”所有用数字(包括用一个数字或是用一系列数字)表示的图案或特征。

举个例子,一个大型寄存器库就可以是一张黑白图片(任何黑白图片,例如这一页书的照片),每个寄存器都占一个像素,内容为0时显示为白点,为1时显示为黑点。现在,请书写这样一个程序:可以从成千上万的图片中搜索出一张白底上画有一条黑色水平直线的图片。先不要着急去做。生命很短暂,请先详细地考虑一下,要完成这项任务会遭遇到哪些困难和费事繁琐的步骤。通过想象,如果你真的设计出了水平线识别器、垂直线识别器或者半圆形识别器,那么,再请思考一下该如何将它们与另外几个有用的识别器融合起来去鉴别出一个大写的A,包括它成百上千种的字体。光学字符识别(OCR)软件可以通过扫描,把页面上的图形非常准确地转换成一个计算机文本文件,这无疑是近代计算机程序的一次胜利。其中,每个字母或者数字符号都由一个美国信息交换标准码(ASCII)中的数字表示,所以我们可以搜索文本,还可以完成那些神奇的文字处理工作。利用的不是别的,就只是算术。那么OCR真的可以读懂些什么吗?不然。它并不理解摆在它面前的东西,它只是近似在读录。但对于我们那收纳丰富、装满了可动部件的工具箱来说,这无疑也是件不错的工具。

秘密4

既然一个数字可以表示任何事物,那么它就一定能表示一条指令或是一个地址。

寄存器中的数字可以表示指令,例如增量、减量、移动或者搜索;也可以表示地址,即寄存器在计算机里的地址。所以,我们可以用一连串的寄存器来存储一整套指令。现在,我们有一个主程序:程序A,它让机器通过一个一个地执行寄存器中的指令来完成要求的任务,然后我们可以在这些寄存器中存入另一个程序B。在我让机器运行程序A的时候,它首先要做的事情是去访问这些寄存器,而这些寄存器发出的是程序B的指令,机器随即执行。我想要指出的是,其实我们可以在寄存器机中央处理机中的一组寄存器中一劳永逸地保存下程序A,使其成为烙印在只读存储器上的“固件”,然后再利用它去运行程序B、C、D,等等。正是因为在寄存器机中设置了这个程序A,我们便把寄存器变成了一台存储程序计算机。

有了程序A,寄存器机才能忠实地执行我们通过数字输入寄存器的各项指令。寄存器机所运行的每一个程序都是由一连串数字组成的,它们顺次排列,程序A会依次访问它们,完成每个数字指定的任务。如果我们设置一套系统使这些指令互不混淆,比如,给每一条指令都以两位数字来命名(或任何相同位数的数字),那么我们就可以将构成程序B的一系列指令,例如,

86,92,84,29,08,50,28,54,90,28,54,90

改写成下面这一大串长数:

869284290850285490285490

这一长串数字既是程序B特有的“名称”,也是程序B本身,它需要通过程序A一步一步完成执行。这里是另外一个程序:

28457029759028752907548927490275424850928428540423

另外还有:

8908296472490284952498856743390438503882459802854544254789653985

比较有趣的程序常常会有更长的名称,由成千上万个数字构成。你笔记本电脑里的那些程序,比如文字处理软件或者浏览器,就是由如此长串的数字写成的,通常由几百万个二进制数字构成。10兆大的一个程序写出来基本就是800万个0和1构成的一长串数字。

秘密5

所有可行的程序都能由一个单独的数字指代,都可以被看作是一串指令,等待通用万能机去执行。

杰出的理论家、哲学家阿兰·图灵想出的是另一种简单的假想机。它在被分成一个一个小方格的纸带上来来回回、嘎嘎作响,根据磁头在当前小方格中读到的数字,即0或者1来决定自己的动作(啊哈,这就是条件分支)。图灵机的工作就是轻轻地敲出比特(清除0把它改写为1,或者相反)或者保持比特不变,然后将纸带向左或者向右移动一个方格,进入下一指令。你可能会认为,仅靠一次移动一小方格,用二进制数字0和1而不是自然数0、1、2、3、4、5等来编写图灵机相加、相减程序或者运行其他功能,这听起来,比我们之前寄存器机的练习还要让人头疼,但事实上,两者的运行原理完全一致。带有程序A的图灵机就是通用图灵机,它可以从纸带上“读取”程序B,并利用纸带上任意位置的数据或输入来执行程序B。我们可以把王浩的寄存器机执行程序的过程分解为数字演算和按条件跳转,图灵的图灵机也是如此。两种机器都有着很强的提取程序数字名称并执行它的能力。我们要建造的是一个自带程序A的多用途通用机,而不是建造成千上万个固定不同功能的计算机器,让它们分别去执行各自复杂的任务。对于这台通用机,我们只要喂食各种程序(软件)它就能按要求办事,这就是虚拟计算机。

换句话说,通用图灵机就是一部通用拟态机。尽管名气稍逊,我们的通用寄存器机也是如此。你的笔记本电脑也如此。笔记本电脑能做的事情通用寄存器机也一样能做,反之亦然。但是先别大惊小怪,我并没有说两者有相同的运行速度。我们已经看出,寄存器机在处理某些问题时极其缓慢,例如在进行除法运算时,它需要很费劲地一遍一遍地做减法,我的天哪!就没有什么方法来提速吗?当然有。事实上,自图灵机出现以来计算机的整个发展进程,专家们所做的一切就是要让寄存器机能做的一切工作变得高效起来,其他别无所求。

秘密6

自图灵的假想纸带机之后,计算机所有方面的改善,提高的都只是运算速度。

例如,冯·诺依曼为第一台严格意义上的可操作计算机所设计的那种架构就是为了提高机器的运算速度。他拓宽了交流窗口,让图灵机的磁头从一次只读取1比特变成了一次读取多比特。早先的一些计算机可以读取8比特或者16比特的“字”,有时甚至还可以读取12比特的字。而今天,一次读取32比特已经司空见惯,虽然这也成为了一个瓶颈(即所谓的冯·诺依曼瓶颈),但与当年的图灵机瓶颈相比,它还是宽多了。简单说来,字会从存储器复制到特定的寄存器(指令寄存器)中,一次复制一个,在那里再接受读录和执行。每个字通常由两部分构成:操作码和地址。前者如相加、相乘、移动、比较、遇零则跳转,后者则告诉计算机所要操作的内容在哪一个寄存器。所以,10101110 11101010101就表示,对地址为11101010101寄存器中的内容执行10101110操作,运算结果会显示在一个叫作累加器的特殊寄存器里。

寄存器机与冯·诺依曼机之间存在着一个巨大的差异:在寄存器机上,你可以在它的任何一个寄存器中进行操作,当然,只能进行增量和减量运算;而在冯·诺依曼机上,所有的算术操作都是在累加器中实现的,寄存器只负责存放复制、移动或者储存过来的内容以构成内存。硬连线可以单独完成各自的基本操作,这能省掉很多额外的移动和复制步骤。也就是说,有专门的电子路线负责加法,也有专门的电子路线负责减法,还有的负责遇零则跳转等等。操作码很像是区号或者邮编:指令要送到正确的地方才能执行。软件和硬件就这样相遇了。

今天的实体计算机中会有多少种原始操作呢?几百或者上千个?或者回归了旧时的美好,成了一台精简指令集计算机(Reduced Instruction Set Computer,即RISC),运作的几乎全是原始操作,但条件是,它们必须得以闪电般的速度运行。要想单纯用增量和减量指令去替代相加硬连线,并在速度上领先,条件是:对于运行步骤小于一百万步的加法运算,增量和减量指令的运行速度要比相加硬连线的运行速度快一百万倍。

那今天的实体计算机中有多少个寄存器呢?数百万甚至数十亿个,但它们每一个容量是有限的,因此那些巨大的数字需要分散储存到许多寄存器中。一个字节是8比特,如果你电脑上有64兆字节的随机存取存储器(RAM),那么你就会有1 600万个32比特的寄存器或者是其他规格的等量物。我们已经知道,寄存器中的数字能代表的并不止是正整数。像π、还有1/3这样的实数是由“浮点”系统来保存的,它将数字拆分为底数和指数两部分,就像科学记数法那样(“1.495×1041”),使计算机除了运算自然数以外还可以近似运算其他数值。浮点运算是以浮点数作为数值的算术运算,突出运用在乘法和除法中。20年前(这一章首版时),我们能买到的最快的超级计算机每秒钟可以完成超过400万次的浮点运算。

如果你还想要更快的速度,可以尝试将多台机器并行的方法,让它们在同一时间一起工作,而不是一个做完再交代给下一个做的串行式。并行机能做的事情与串行机大致相同,只是在速度上,它会略胜一筹。事实上,在过去的20年中,专家们一直都在致力于研究的并行机,大多是在标准的非并行冯·诺依曼机上实现的虚拟机。专用并行硬件开发出来以后,计算机专家们一面要忙着测算拓宽冯·诺依曼瓶颈所需的成本与最终获益,一面又要想方设法地提高数据传输速度,不惜调用协同处理器、高速缓冲存储器以及各种各样其他的途径。今天,日本富士通公司的超级计算机K-computer已经可以运行实现每秒超过一亿亿次的浮点运算。

这个速度基本上要与你大脑的即时运算速度旗鼓相当了。你的大脑是一台最卓越的并行处理机,包含着大约一千亿个神经元,每个神经元都相当于一个复杂的微型行动者,有着各自的行动规划。视觉“神经”有数百万神经元宽度的通道,单凭自己就可以把眼睛看到的视觉信息传入到你的大脑里。但神经元运行起来比计算机电路慢得多了,一个神经元在几毫秒(一毫秒是千分之一秒,不是百万分之一,更不是十亿分之一)之内可以转换状态并发送一次脉冲(可能是它们的增量或减量指令),而计算机使二进制数字移动的速度几乎接近光速。所以说,让机器更快速运转的一个至关重要的方法是要将它们做得更小。光走一尺大致需要十亿分之一秒的时间,如果你想让两个程序之间的通讯比之前更快,那就让它们离得更近些吧。

秘密7

没有再多的秘密了!

也许计算机最美妙的地方就是,它由各部分(操作)简单地组合而成,而各部分本身也很简单地组合,简单到没有什么地方是无法给出解释的。这里没有仙气,没有“形态共振”(morphic resonance),没有无形的力场,没有未知的物理定律,也没有那些神奇的组织。要知道,即使你用计算机成功地模拟出一些情景,完成这一切的也只是一些算术运算而已。

目前流行的量子计算机是什么呢?量子计算机能做普通计算机不能做的事情吗?算是,但也不完全。得益于微妙而奇特的“量子叠加”态,那些不被观测的物质能够同时处于“所有可能的”量子态上,直到观测导致“波包塌缩”。这个性质使量子计算机可以同时计算大量数值,解决诸多问题。(要想了解更多,请查阅你最喜爱的那些物理科普书籍或网站。)基本上,在计算机的提速进程中,量子计算机的发明是最新一次的重大变革,可以说,在提高计算机的处理速度上,它实现了一次巨大的突破。图灵机沿着它的纸带喳喳作响,寄存器机奔波于各个寄存器之间不停地增量减量,但时间紧迫,在几分钟、几个小时或者几天内,它们能做的工作也总是显得那么有限。日本富士通公司的超级计算机K-computer数万亿倍地加快了运行速度,但还是不能解决所有的问题,这在密码学中体现得尤为突出。量子计算机本该弥补这部分的缺陷,只可惜,我们还没有克服工程上的诸多难题,没有让它变得更加稳定、更加实用。就目前来看,每秒钟运行一千万亿次的浮点计算就已经是天方夜谭了。

25
虚拟机
模仿硬件运行的计算机程序

般来说,实体机器由可活动的物质部件构成。设计者们常常会根据其功能去命名它们,如:割草机、开瓶器、咖啡研磨机。它们的形状设计和构造各不相同,利用的是不同的物理定律。但它们也有共同之处:拥有相同名字的机器都在某种程度上做着与名字的描述相差无几的工作。它们基本上都能满足使用者的需求,可能有些会稍微好用一点儿。有私家庭院的人会挑选一款噪音相对较小的割草机,小咖啡馆的老板则更需要那种能精确研磨出不同粗细粉粒的咖啡机,尽管它操作起来非常复杂。有些机器是多功能的,比如一把电钻,插上不同的附件就能变成一把电锯或是一部打磨器。

计算机与这个类似,不同的是它不仅能按要求做几件不同的事,它甚至能做成千上万件事。而且,它也不需要插上不同的附件,你只要点开不同的程序,即点开一串串由0和1组成的超长数字,就能让计算机内部实现一些必要的切换,从而转换成不同的装置去完成不同的工作。每套装置系统都是一台单独的小机器,没有齿轮、轴承、电线或者滑轮,这就是模拟机,“构成它们的是指令”。不像和面团、压纸浆或者造钢坯,计算机处理的只有信息。

所以,对于计算机来说,指令就是那些齿轮和滑轮。所有的信息都要转换成二进制代码0和1,它们是计算机能够和需要“读录”的唯一代码。印在芯片上的电路会数以万亿计地分流0和1这些代码,逻辑门不断地开合,将信息流输送到这条或者那条电路,并以这种方法来下达指令。计算机上唯一“可移动的部件”是硬件上数百万的小空位,它们可以在1态和0态之间任意切换。至于计算机在某一时刻具体是一台具有何种功能的机器,则取决于数千个至数百万个微型元件的设置。

若一台实体机的各部分相互配合可以呈现许多不同的状态,那它就具备极强的可塑性,只需要在其上施加一套特殊的指令,就能把它变成一部虚拟机。由于虚拟机处理的是信息,那么它就能够与实体计算机做相同的工作:后者中的“可移动部件”只是硬件中的状态变化,而前者可以直接操纵状态变化来代表那些“可移动部件”。这也不难理解:你可以在一张纸上用铅笔演算一个长除法,但如果足够熟练,你也可以“在头脑中”进行这种运算,即通过想象,在脑海中的纸张或者黑板上书写过程。两者都会收到很好的效果,原因是,在面对这类情况时,我们想要得到的答案只是一种信息。相比之下,想象出的一个火腿三明治是不能用来充饥的,你得真的去做一个。计算机十分擅长在“头脑中”处理信息事务,所以我们有时真的分不清自己在使用和操作的到底是一台处理专项信息的“专用”“硬连线”机,还是一部在多用途芯片上运行的虚拟机。

今天,几乎所有的升降机、空调,还包括你的汽车、电冰箱和电视机遥控器里,都有那种微小、廉价的计算机芯片,它们实际上就是一些多功能计算机,能运行你笔记本电脑里操作的所有程序。但我们却选择让它们倾其一生只运行刻录在只读存储器上的一个简单的程序,将它们所有杰出的能力都封锁在那一两个技能上,例如点火控制或者周期除霜,等等。原因是,比起去制作那种只负责专项工作的专用芯片,这样做的成本要低多了。

虚拟机的概念是计算机科学中出现的一个最有用的想象支架,在计算机界,这早已人尽皆知。现在,到了将它引入其他领域的时候了。因为要从广义上使用虚拟机这个词(我会在适当的地方说明原因),所以我们有必要先了解一下它最初的意思,也就是说它本身的意思。“虚拟机”是计算机科学家杰拉尔德·波佩克(Gerald Popek)和罗伯特·戈德堡(Robert Goldberg)在1974年引入的一个概念,它最初表示的是“实体机的一种独立有效的复制品”,即各种指令的一种复制品。实体机是实在的硬件,由硅芯片和电线之类的东西组成,我们称它为A。而虚拟机是运行在另一台实体机B上的计算机程序,它能完美地模拟硬件A:速度可能会慢点,毕竟它要首先保障自己硬件上的基本操作有效运行,才能组织起硬件A上的那些操作,但的确都是同样的程序。如果你写了一个在硬件A上运行的程序,那么硬件B在运行模拟硬件A的虚拟机时,该程序应该能在硬件B上畅通无阻地运行。

这是一个非常有用的方法。部分原因是,它显然可以让一些资源免于浪费:假设这里有很多只能在Mac上运行的软件,但是如果你没有Mac,它们就无法使用。这时,如果可以在PC上编写出一个模拟Mac的虚拟机,然后成功运行,那么你电脑上的这些Mac软件就都可以运行了。你的电脑会“假装”认定它是真的Mac,而那些软件则根本就浑然不知!试想一下,一个人胳膊骨折了,然后打上了石膏。石膏可以让骨折处不再活动,而它的重量和形状会让受伤者身体其他部分的动作产生一些相应的调整。现在,有个小丑想要模仿这个手臂上打着石膏的人,如果他技艺足够精湛,他就一定会让自己的肢体行动做出一些相应的调整,“显而易见”的是:他胳膊上打着石膏呢!如此说来,那些运行软件和我们这些旁观者可能就真的看不出此时是一台在运行Mac虚拟机的PC还是一台真正的Mac。

但现实与上面的例子却多少有些出入。虽然人们已经开发出了PC上可运行的Mac虚拟机,但据我所知,这个软件并没有宣传的那么可靠。另一方面,Mac却已经有了一个简单易行又非常可靠的虚拟机,它可以运行PC的操作系统Windows,借用它,Mac持有者可以运行所有自己喜欢的Windows软件。目前,绝大多数编写出的程序都需要专门的操作系统而不是专门的硬件,而相同的硬件上可以运行不同的操作系统。所以,虚拟机的概念还需要继续扩展,还应该包括对操作系统的虚拟模仿。一种操作系统本身就是一部虚拟机,它能让一个程序在略有差异的硬件上同样有效地运行。但一个操作系统毕竟只是一种软件,并没有模拟任何实际存在的硬件,它只是按照实际要求创建出虚构的机器,遵守相关的规定、接收特定的输入等等。

Java虚拟机(Java Virtual Machine,即JVM)已成为当今最流行的一种虚拟机,这是我们扩展虚拟机概念的又一个原因。Java虚拟机不是对任何一种实体硬件机器的模仿,它只作为软件机而存在,比较像一个操作系统。可以说,是Java的出现让今天的因特网变得如此丰富多彩。有了它,你可以从网站上下载一些小程序(Java的应用小程序),让你能够填纵横字谜、玩数独、探索地图、放大照片、与世界各地的玩家一起参加探险游戏。当然,它也能处理很多“严肃”的事务。用Java语言编写程序的网站不需要在意浏览网站的用户手上拿的到底是PC、Mac还是运行Linux操作系统的计算机。因为Java程序只在Java虚拟机上运行,而PC、Mac以及Linux操作系统上都有各自专门的Java虚拟机,匹配的Java虚拟机会自动下载到你的电脑上,几秒钟便可以安装完成。然后,各种Java小程序就都能运行了,犹如魔法一般。可能你有时会注意到Java更新正下载到电脑上,也可能你注意不到。理想状态下,你不用记住自己电脑上安装的是哪一款Java虚拟机,随意浏览网站就可以了,有时网站上的Java小程序能顺利在你的电脑里运行,而不能运行时匹配的Java升级软件就会自动下载安装,以确保能够继续运行。

从我说的这些虚拟机的扩展意义出发,我们可以看到,几乎所有的计算机程序都是虚拟机,因为它们都是由一系列指令构成的软件,一旦开始运行,就将通用计算机转变成了专用计算机,而这种专用计算机本可以专门设计并建造成专门的硬件。“通用”计算机,即今天我们所说的通用图灵机理念的提出,是阿兰·图灵为科学、也是为20世纪中期以来的人类文明做出的一项最杰出的贡献。只需要在上面简单地安装和运行程序,我们就能把它转化成任意一种功能清晰的计算机!(为了防止你想跳过这一部分,我在24章中已详细介绍过了。)你不用再去组装其他的硬件计算机,有一台就足够了,剩下的事情就交给软件吧。图灵机时代的开启让我们实现了一个非凡的想法:只要有一个大块头的硬件,在里面安装无数可塑可调的“记忆”盒子或者寄存器,把需要的指令放进这些记忆盒子里,让它们运行,这样你就把这个大块头的硬件变成了你想要的任何一种计算机。

图灵机,或是笔记本电脑,只能一条一条地执行指令。我们在这个基础上扩展出了“并行”计算机的概念,它可以一次执行多条甚至数百万条指令。硬件中的任何一个地方,只要它们本身保持着一种状态,收到指令后会变成另一种状态或者不变,那这个地方就是寄存器。这种状态可以是你电脑中一个比特的0态或1态,但不限于两种状态。以这种状态为基础,任何寄存器系统都可以完成一些特定的基础操作。例如,通过让寄存器由一种状态变为另一种状态,或者用寄存器的某一种状态判定下一步的操作,寄存器系统就可以按照“计算一个函数”或者“运行一个程序”来配置自己的各个寄存器。所以任意硬件上都可以运行利用了上述基本步骤的虚拟机。一种技巧可以使用一次就可以使用第二次、第三次、更多次,在硬件上部署一部虚拟机,虚拟机中可以再部署一部虚拟机,在其中再部署一部虚拟机……

请考虑一个下国际象棋的程序,它由Common Lisp这种高级计算机语言写成,在一台PC上运行的Windows7操作系统上运行。事实上,这台PC佯装成了一台Windows机器,这台Windows机器又佯装成了一台Lisp机,这台Lisp机又佯装成了一台国际象棋游戏机。对于那些谙熟计算机或者国际象棋的观察者来说,要是从最上面一层观察的话,这个程序的诸多细节还多少可以理解(“啊哈,这个子程序生成了所有移动“象”的合法走法,然后调用另一个子程序来评估每一步……”)。然而,要是让他们去看这个程序实际的机器代码,看着那一串串0和1被吃进硬件的指令寄存器,这倒十足成了一个让人抓狂的好办法。所以,还是让我们把注意力放在那些较高的层次上吧。

其实,无论在哪一层次上,我们都只能只见森林不见树木,较低级层次上的那些细节总是很容易忽略。计算机上虚拟机的层层嵌套和关乎心智的小人儿功能主义中小人儿的层层嵌套,两者之间的雷同并非巧合。虚拟机推动着人类的创造和理解,实质性地完成了一些至今都超越我们想象的任务,比如完成航线预定、下象棋、预测天气、做记录等等,这些惊人的成就鼓励着我们:如果对大脑也采用类似(仅仅是类似)的技巧来实施逆向工程,或许我们也能得到一些收获。

在面对“说法语的人的大脑之间的相似性”这个问题时,排除解剖学上那些显著的差异,也许利用虚拟机的例子能为我们展示出一种最好的解释:所有说法语的人都具备某种版本的法语虚拟机,它藏身于他们大脑数以亿计的寄存器中,是一套由细微的习性和连锁装置构成的系统。说英语的人的大脑里同样也有这么一套固定模式的系统,即英语虚拟机。当你对一个法国人说,“Donnez-moi le sel, s'il vous plait.(请把盐递给我)”你基本上就能预见到法语虚拟机会作出的反应,因为这种反应与你,这样一个说英语的人为自己脑中的英语虚拟机输入“Please pass me the salt.(请把盐递给我)”时所给出的反应是一致的。但现在的问题是,要怎样在人类的大脑中建立起一个法语虚拟机或者英语虚拟机?

人们在下棋或是在说法语时,大脑各个不同的层面上到底都发生了一些什么样的活动,到目前为止我们还不得而知。(42)毫无疑问,我们对此给不出什么精确的描绘,不像是编译器,这种程序可以将高层指令翻译成可在硬件上运行的代码,只要计算机程序员能在最高层面上设计出一个想法,它就有把握吐出一个可执行的程序来。这里出现了一个非常重要的概念验证:至少我们找到了一种方法,它能够清楚地道出“具有数万亿活动部件的计算机,它的那种高等能力到底是什么”,而无需动用神奇组织。

26
算法
有效解决问题的策略机制

曾在《达尔文的危险观念》中提到过,达尔文的那些伟大观念想要引出的是:

地球上的生命经由这种或者那种算法,在上亿年的时间里谱出了一棵布满枝桠的生命之树。

那么,准确来说什么是算法?事实上,这个概念有很多相互抵触的含义,而我取的是最广义上的那种理解。以下内容是我从《达尔文的危险观念》中摘录出来的,略有修改。

是达尔文发现了算法的力量。算法是一种特定类型的形式步骤,只要可以“运行”或者开始具现化,它就能合乎逻辑地产生特定类型的结果。其实,即使是在达尔文时代,算法也并不十分新颖。很多我们熟悉的算数运算,例如进行一个长除法或者结算支票簿,都是算法。另外,像走出井字棋的完美一步,还有按字母顺序排列单词时使用的那些判定程序,它们也属于算法。数学和逻辑学界对算法的本质和威力的理论反思出现得则相对较晚,它们是20世纪发展的产物,让我们开始重新认识达尔文的发现,直接关系到了计算机的诞生,而计算机的出现也让我们对算法的威力有了更加深刻、更加生动的理解。

要查看算法的词源,我们得向前追溯:经由拉丁文的algorismi到早期英语的algorism,之后又错写为algorithm的现代形式。再往上,算法这一术语其实得自于波斯一位数学家Mûusâ al-Khowârizm的名字,他在9世纪写过一本关于算术步骤的书,到11世纪,巴斯的阿德拉德(Adelard of Bath)或是切斯特的罗伯特(Robert of Chester)把它译成了拉丁文。数个世纪以来,算法这个概念表示的一直都只是那种万无一失甚至略显“机械”的程序过程,20世纪30年代,阿兰·图灵、库尔特·哥德尔还有阿隆佐·丘奇(Alonzo Church)这一代人经过了一系列创始性的工作,才多多少少确立起了如今人们对这个概念的一些理解。对我们来说,算法的概念有这样三个至关重要的特征,其中每一个总结起来都有些难度:

(1)底层中立:不管你用的是哪种符号体系,也不管你用的是铅笔还是钢笔,纸张或是羊皮卷,霓虹灯或者空中文字,它们都不会影响到你运算长除法的过程。运算程序依据的是逻辑结构,各种物质材料的因果力量也许能为运算保驾护航,但它们不直接参与运算。

(2)潜在无头脑:过程的整体设计可以非常华丽,结果也可能十分耀眼,但算法的各个既成步骤,包括步骤之间的过渡却出奇地简单。有多简单?一个听话的白痴或者是一个简单的机械装置就能执行。教科书中这样说:算法就像是为新手厨师设计的烹饪说明。一本写给主厨的料理书上可能多是这样的措辞:“放入葡萄酒适量,将鱼煮熟。”而转换成算法之后,它会变成这样:“取一瓶标签上写有‘干’字样的白葡萄酒,用螺旋开瓶器打开,把瓶中的酒倒入锅底至3厘米深,然后打开锅下的燃炉旋钮调到高火,……”整个过程分解得相当仔细,最后都落实到一个个简单得不能再简单的小步骤上。你只需要充当一部食谱阅读器就可以了,不需要做出什么明智的决策或是精准的判断,也不需要发挥直觉。

(3)结果有保障:不管什么样的算法,只要不出意外,它就能每次都按照同样的步骤运作。一种算法就是一份简单的、不易有出入的食谱。

显而易见,计算机就是靠这些特点成就的。每一种计算机程序都是一套算法:根本的组成元素是一些简单的步骤,它们的运行过程惊人地可靠,而运行机制却十分简单。电子电路是计算机常用的选料,虽然电子总是在硅芯片上飞来飞去,但计算机的能力却与它们的因果特性完全无关,运算速度除外。同样的算法,用在使用光纤分流光子的设备上就可以完成得更快些,而通过人力用纸和笔来计算就会变得很慢很慢。

达尔文发现的不是一种算法,而是一大类相关的算法,只是他还没办法对这些算法做出明确的划分。

27
让电梯实现自动控制
机器取代人类的逻辑

离开这段有关计算机的插曲之前,我还想再多介绍另外几个有用的概念——源代码、注释和目标代码,它们对于我们探索“大脑怎样产生意义”这个问题有很重要的作用。一般来说,比起直接去解决一个智力难题,先研究透彻一个简单纯粹的相关实例、抓住概念的一些实质对我们来说可能会更有帮助。在人工智能领域,这种简单实例叫作“玩具问题”。先解决掉玩具问题,顺势再让我们去处理现实中那些恼人的大问题吧!下面所讲的是一个有关升降机操作员如何被计算机芯片取代的故事,我是为了简单纯粹才编造了这个故事,但这一点儿也不影响它的现实性。

在我年轻的时候,那时候升降电梯里还需要有操作员,他们每天的工作就是跟随电梯上上下下,在需要的楼层停下机器,让乘客们出入。在那个年代里,他们操作的是一根很奇怪的手柄,手柄以顺时针或者逆时针方向转动,这样就能使电梯上升或者下降。能让电梯准确地停靠在某一楼层是需要技术的,那时的电梯总是停得要比楼层地面高出或者下陷三五厘米,所以在乘客们出入时,操作员不得不提醒大家要多注意脚下。先到哪一层,到了要说些什么,还有怎样开电梯的门等等,这些都是有规矩的,操作员们要接受训练,记住这些规矩,不断地练习操作,直到把这些规矩都彻底变成了他们的第二天性。规矩本身是发明者们在设计过程中经过多年的苦思冥想才制定出来的,期间还会进行大量的微调和改进。我的设定是这样的:这种电梯的设计已经基本固定,设计师们留下了一本详细的操作手册。无论什么人,只要按照手册正确操作就一定能成为优秀的电梯操作员。

现在,让我们来想象一下,要是操作员所有的工作都能由一个简单的计算机程序来完成,情况又会是怎样?事实上,随着各种自动机械的发明不断出现,纯技术性工作已经开始与人工操作发生分离,我们想象的这种情况也早已逐步实现,但现在我们的设定是,电梯从人工操作到彻底地实现计算机系统控制是一步到位的。

我们猜想,电梯制造商们会召集一批软件设计师,也就是程序员,把操作员一直使用的操作手册交给他们:“这里有我们想要的产品规格。请根据书中的操作规则设计一个电脑程序,我们要求它能实现最棒的人工操作员所有的操作。”程序员们开始仔细对照这本操作手册,并列出了一个包括所有操作的清单,以及在哪些情况下操作必须执行或者禁止执行。在这个过程中,程序员可以避免手册中出现的那些不规范的情况。举个例子,如果给电梯安装上传感器,那么它以后就会在需要的层面准确地停下来,再也不需要操作员们像以前那样要根据不同的情况说“请向上迈步”或“请向下迈步”,升降机每次只需要简单地播放一句录音“第××层到了,请注意脚下”即可。那么接下来,需要程序员们去做的是要用一种名为伪代码的语言写出这个程序的草稿。伪代码是介于人类日常语言和更严格、更系统的源代码语言之间的一种混合语言,一串伪代码写出来大概是这个样子:“如果呼叫楼层>所在楼层,那么上升,直到呼叫楼层=所在楼层即停止;开门。等待……”

如果伪代码条理清晰,能够达到想要的操作效果,那么就可以把它转译成源代码了。源代码是一种更加规范严密、更加结构化的操作体系,而且,它还包含了各种术语的定义,例如变量、子程序等等。人类其实可以轻松地破译源代码,毕竟源代码也是我们写成的。一旦找到突破口,你就会发现,电梯操作手册中的那些规定和术语在源代码里表达得清清楚楚。而且,源代码下面的这两个特点还会让这个过程变得更加简单:一、变量和运算的名称直接显示它们自身的作用,例如呼叫楼层(callfloor),总重量(weightsum),报楼层(tellfloor),等等;二、就像我们在第24章中已经说过的,程序员们可以为源代码添加注释,这些附加说明能让其他人明白他们写这些代码的目的,弄清楚代码各个部分的功能。你在编写一个程序的时候也应该添加一些注释,防止自己忘记当时设计这串代码时的用意,在重新对程序进行纠错时,这些注释也会相当有用。

源代码必须严格按照语法编写,所有的构成元素和标点符号都要写在正确的位置。只有源代码书写格式完全正确,编译器才能读录,然后将它们以目标代码的形式转化成基础的操作序列,供实体机或是虚拟机执行。我们不能要求编译器从源代码中揣测程序员的意图,源代码必须让编译器准确知道该执行什么操作。不过,编译程序有能力利用多种途径去执行操作,它还会根据具体情况选择最有效的一种。编译器也有好有坏,如果将同一个程序的源代码输入到两个不同的编译器中,它们得出的目标代码程序很可能就会以不同的速度在计算机上运行。

举个例子:你编写了一个下棋的程序,然后将它的源代码输入到两台不同的编译器中,得出了两个版本的操作程序。将这两个程序输入同一台电脑让它们对抗。你会发现,尽管这两个程序“按照同样的顺序做着同样的思考”(当然会这样,因为它们源自相同的源代码),但也会分出胜负,因为两者的速度不一样,快的一方使用的基础计算周期较少,在有效的时间内它能预想得更多、更周全。

回到我们的电梯上来。编译器编译出目标代码以后,就生成了一个可执行文件,文件名中通常有扩展名“.exe”。这个文件就可以在虚拟计算机上运行了。也许还需要几经调试,比如回到源代码进行微调,甚至重写,它才能最终成为“完成”版的程序。我们将这套目标代码“烧”到一块小芯片的只读存储器上,而这块小芯片上通常带有一个通用计算机及其之上的很多层虚拟机,然后,我们将这块芯片安装到电梯上。安装时芯片的输入端要与传感器连接,传感器能接受到按钮发送的信息和电梯测量到的乘客们的总重量等;芯片的输出端连接的是效果器,它可以指挥电机转动来开门闭门、升高或者降低箱体,更新显示器,播放录音等等。

再见了!机器已经彻底把真实的人类取代了,而不只是取代了一个我们塑造出来的小人儿,它们就跟人类操作员一模一样,遵守着同样的规则。真的是这样吗?果真如此?好吧,但事实并不如此。它们只是近似于遵守着同样的规则罢了。这是一种很经典的中间状况,它既不像人类那样,能够逐字逐句地把规则保存在大脑中,然后随时查阅那些规则从而指导自己行动;也不同于那些行星,总是能不紧不慢地“按照”设定好的轨道运行。我们人类也常常处于这种中间状况。有时,通过不断训练一些显性规则就会内化或者变成一种惯例,我们甚至都忘记了它们,不自觉地就把它们丢弃了。比如,一般来说英语单词中字母“i”都会位于字母“e”前,除了这两种情况:一种是,前面的字母为“c”;另一种是,在“neighbor”和“weigh”中,发音近似于“a”时。一些还处于调试阶段的规则也似乎是可以遵守的:例如英语的语法规则,它对语言学家们来说一直是一个挑战。时至今日,他们费尽周折也没能写出一本标准的规则手册让我们把英语说得更好,而事实上,几乎每个十岁大的母语是英语的孩子都能调试建立起一些相当不错的目标代码,供他们脑中的英语虚拟机使用。(43)

在结束这一章之前,我还想提醒大家一点:植入到源代码中的那些注释可以帮助程序员们去理解软件中相互啮合的各个部分的意图,但是,在我们为了方便描绘人类大脑所创作出的那些硬件、固件和软件中,它们却并不出现。自然选择在我们的大脑中安装的那些功能结构就像是一些没有注释的代码,这些功能是有目的的,但由于代码并没有对结构的目的有所注释,大脑也就无从理解这些意图了。(在第40章中,我们会有更详细的讨论。)在发育和学习的过程中也存在着未加注释的调整或者是未说明意图的调整。就像是语言学家们那样,我们还在奋力实施着逆向工程来破译所有那些“规则”和“程序”,虽然它会比用逆向工程目标代码来复原源代码更有难度,但理论上的确可行。