存储是图灵带给人类最糟糕的东西

文摘   2024-10-18 11:39   中国台湾  

存储也许是图灵带给人类最糟糕的东西。图灵机中的存储概念,教科书常说是「obviously」那种简单明了的定义。虽然图灵机与λ演算的等价性有理论证明,但in what sense?to what extent?这些问题很少得到清楚的解释。然而可以确定的是,λ演算中无法表达存储。这让两者的「等价性」变得模糊不清。尽管Haskell和许多函数式编程语言发明了state monad,前端开发中最核心的挑战仍然是state management和维持单向数据流。

存储在pi calculus中同样无法被表达。pi演算也是「纯」的,但它能够表达通讯。对pi calculus的简单甚至粗略描述是,任何一个计算的执行(即表达式的reduce)都需要由message触发,包括发送完成和接收完成,这是pi的最基本单元。它可以通过parallel组合,即a|b的reduce结果(表面上看)与ab哪个先执行无关;也可以通过summation组合,像a+b,意思是如果a(等待的消息先到达)先执行,则b将reduce to nil。这种机制直观上就是进程演算意义上的process之间的OR,而parallel组合更像AND。

summation足够表达任何编程语言中的if结构,因为它可以被看作a+b的组合,即if下的两个代码块,它们等待同一个message(channel)。当这个message出现某些值时,a执行,b消失,或者相反。当然,这似乎仍然是在channel中引入了if判断,用于筛选值,但这也可以看作是一种语法糖。我们可以将值空间完全展开为多个channel,每个值对应一个channel,a等待它认可的一组channel的消息到来,b同样如此。这样,我们就可以用更加原始的方式来理解,去掉语法中看似人为的if结构。

这种理解是正确的,就像丘奇用lambda构建自然数一样。但这里也暴露了pi calculus的一个重要限制,即Robin Milner在其经典书籍中描述的正统版本并没有支持broadcasting。而显然,我们需要broadcasting来实现前面所说的将所有值视作channel,因为channel是pi演算的基本单元。


在现代处理器中,写入存储的过程,暂时不考虑层次化缓存,可以看作pi calculus中的输出等待。原则上,输出完成后才能继续执行,这是pi演算中的标准表达式。而读取存储时,我们可以将代码截断,将前半部分理解为输出一个“load”指令并reduce到nil,然后通过parallel组合后半部分的进程,这部分等待读取值并在读到值后继续执行。程序员称这种读写模式为“阻塞模式”(blocking mode),几乎所有程序员都理解这个概念,即便是自学成才的非计算机科学专业人士,但对其形式化的理解其实并不那么简单。


Blocking I/O model在某些场景下可能会导致效率低下,因为程序可能会因为等待I/O操作而长时间无法执行其他任务。

但很明显,内存和IO是不一样的,对吧?内存总会返回上次写入的值,而读取键盘时,你永远不知道会读回什么值。没错,如果将内存视为一个process,其实从处理器核的角度来看,它也不知道内存的实际形态。处理器只会把load指令、内存地址以及目标寄存器的名称交给硬件,硬件可能会去访问总线,总线再去寻找内存控制器和仲裁者。这一长串的硬件和行为,对于CPU核心来说,都是一个黑盒。因此,如果一个处理器的硬件伪装出一块内存,甚至是一个bit,这种理解是合理的。


那么,如果按照这种方式理解,「读回上次写入」的值意味着什么呢?它实际上意味着处理器和内存之间存在一个协议,对吧?就像我们假设一个非常简单甚至愚蠢的处理器,它没有加法器,但有一个协处理器负责执行加法运算。每次主处理器只需要将两个数交给协处理器,然后协处理器返回计算结果。


题外话:这个结果正确吗?在这个例子中看起来有些荒谬,因为验证和求解的复杂度是一样的,这样验证就毫无意义,自己去算反而更高效。然而,如果一个问题的求解复杂度与验证复杂度不同,情况就大不相同了。在这种情况下,主处理器可以进行防御性验证,就像区块链中的工作量证明 (Proof of Work)。因此,回顾所讨论的一切,可以总结为:从形式化的角度来看,内存可以被视为一个执行特定协议的处理器。


如果要扣回开篇所提的看似惊人命题,可以这么阐述:在现代计算机体系结构和编程语言设计中,process 和 protocol 并没有得到很好的表达,尤其是在顺序上的不确定性问题上。例如,a+b 这样的过程,它依赖的两个事件到达顺序可能不同。a 可能因为 cache 命中而先执行,或者因为 cache miss 而让 b 先执行,进而对结果产生不必要的影响。而实际的计算机体系结构比这更复杂:总线有仲裁,内存有仲裁,复杂的单元有自己的 DMA,甚至 MMU(内存管理单元)也是如此,而 MMU 的内存映射表还存储在内存中。此外,编译器中也充斥着各种设计决策。世界的变化如围棋落子,顺序的不同对结果有决定性影响。Lambda 演算非常巧妙地将无关的部分保持无关(惰性求值),Pi 演算也有类似的属性,但仍有难点。例如在 a|b 的情况下,虽然表面上看是并行的,但 a 和 b 中的子表达式可以通过消息建立顺序依赖。Pi 的并行表达仅意味着它们各自的 reduce 操作是独立的,并不会像求和 (summation) 那样,只有一个进程获胜。


Milner 是非常谦逊的,他曾写过一篇非常有洞察力的小文章,标题是《函数即进程》(Functions as Processes),其中他正式表述了 Lambda 演算可以嵌入 Pi 演算中。这篇文章发表时并未引起太多关注,但近年来研究通讯协议的类型系统 (如 Frank Pfenning 等人提出的 Session Types) 让这篇文章重新引发了兴趣。在某种意义上,显式表达顺序是至关重要的,它可以消除很多硬件和软件中不必要的复杂性。甚至在某些缓存一致性研究中,问题的根源在于缺乏对并发顺序的更好表达。这也是缓存一致性——被称为计算机科学中第二难的问题——产生的原因之一。


由于全球范围内理工科教育的局限,哲学似乎逐渐没落。然而,世界的本质问题依然体现在许多领域。比如 Lambda 演算可以被理解为「世界的本质是运动的」,或者用《易经》中的说法,「静者静动,非不动也」。这是古希腊哲学家赫拉克利特的观点,他选火作为世界的基本元素。与此相对的是结构主义,认为结构是稳定不变的。这可以追溯到埃利亚学派,柏拉图也是其中一员,据吴国盛教授的解读,柏拉图在《蒂迈欧》中首次提出了「存在即是结构」这一振聋发聩的观点。Whitehead 也曾肯定这一点,并指出,从集合论到布尔巴基,数学思想在两千年内并未发生根本变化。


Milner 的想法极具趣味性,他把运动(process)看作实体,并试图找到它们之间的关系(通讯)。类似的观点还有哲学家巴克莱提出的「存在即是被感知」(to be is to be perceived),这句话也是加州大学伯克利分校名称的来源。如今,随着类型系统的发展,通讯协议的形式化研究正逐渐成为新的研究领域。


既然已经列举了这么多杰出人物和思想,不妨再提到毕达哥拉斯的「数」概念。数在现代被发现是最基础和直观的归纳工具。逻辑学派可以被归入这一流派,虽然逻辑与数论方向不同,但从计算的角度来看问题没有太大矛盾。这个学派当然包括亚里士多德、罗素、哥德尔、图灵等人。不能遗漏的还有数学家桑德斯·麦克莱恩 (Saunders Mac Lane),他是范畴论的奠基人之一,并以顶级数学家的身份写了小册子《形式与功能》(Form and Functions)。这一哲学理念可以被看作是二元论,换句话说,麦克莱恩并不认为有一种一元论的方式可以充分表达复杂性。


如果要给图灵加上一个标签,他应该是机械论者。这并不奇怪,毕竟他在二战期间专注于机械破译密码。图灵的老师丘奇,虽然与他同为逻辑学派,却更偏重形式,并试图削弱数学和逻辑的旧有基础。这些都可以用来表达计算的本质。


当然,这些论述纯粹是为了趣味而写。正如哲学家陈嘉映所说,现代哲学家早已放弃了构建统一理论的尝试(杨振宁还未彻底理解这一点),我们应放弃对「本质」的过度追求,因为存在先于本质。AI 的崛起让我们有机会尝试新的形式化理解。就像这篇文章强烈暗示的那样,大脑中的神经网络显然不是基于「图灵机加内存」的计算模式,而是通过广播和通讯实现替代。为什么?因为从数学意义上的图增加边 (edge) 是成本最低且能够逐步进化的。神经细胞的数量增长并不是关键,重要的是突触 (synapse) 的增加,而且它是具有弹性的。


我们对大脑的理解如此浅薄,正是因为我们习惯了结构主义的静态思维。而大脑源于神经网络,其进化路径来自于「动态」「节奏」(同步器)和「通讯」功能,以适应环境,“物竞天择”。

楫山见月
①分享个人兴趣爱好②钻研浙东学问。上山采蕺山阿,衱蕺下山日蹉。浙东之学原有事功、节义、隐逸、经史,虽面目迥殊,授受皆出于一,各有事事也