← 返回信息流
AI 资讯Hacker News·3 小时前

局部推理实现全局属性

原标题:Local Reasoning for Global Properties

速览

该研究提出一种局部推理技术,通过分析系统局部行为来推断全局属性,避免全系统验证的计算开销。方法结合抽象与组合推理,可应用于软硬件系统验证。实验表明,该方法在保持精度的同时显著提升效率,为大规模系统验证提供新思路。

AI 深度解读

背景

近年来,作者经常被问到同一个问题:AI 是否会从新型编程语言中受益?他过去的回答一直是“大概不会”,而且到目前为止这个判断站得住脚——AI 已经能够用人类能想到的几乎所有编程语言生成大量代码。

然而,随着技术逐渐成熟、特性愈发清晰,他的回答变了。他发现,至少就现状而言,AI 通常能生成高质量的局部代码片段(比如单个函数),但在需要全局理解程序整体时,往往力不从心。最容易观察到的现象是:代码中充斥着大量不必要的防御性检查。这些检查看似无害,却会导致后续读者认为可能出现的状态数量呈指数级增加,进而带来各种不良影响。

也许这个难题很快会被攻克,但如果不能,我们或许会再次向编程语言设计寻求帮助。作者写这篇文章的目的,并不是预测编程语言将会或应当以何种具体方式去解决这个问题,而是想回答一个更基本的问题:我们是否已经拥有一个成功的编程语言设计范例,能让局部推理为我们提供关于某个令人惊讶的全局属性的可靠保证?

作者靠编程语言谋生,自然有动力强调其重要性。但他也承认,编程语言确实会影响生产力和软件可靠性,却并没有多少证据表明它能带来深刻的差异。这不仅仅是因为没人能做出证明差异的良好实验,更因为很多“好”软件是用“坏”语言写的,很多“坏”软件也是用“好”语言写的。编程语言本身似乎并不是决定结果的主要因素。

最简单的论点是:要创造出功能完备、清晰可靠的软件,需要的更多是共情能力,而不是对复杂编程语言特性的精通。不过,这并不意味着编程语言毫无影响。当作者从汇编语言转向 Python 和 C 这样的高级语言时,生产力大幅提升,也敢于应对更大的软件项目。原因很简单:汇编迫使他处理大量低级细节,以至于不断遗忘更重要的高层图景。这种差异是巨大的。

但作者逐渐意识到,如此巨大的提升不太可能再次重现。他缓慢而笨拙地重新发现了 Fred Brooks 的“没有银弹”思想:过去软件生产力的巨大提升,大多来自消除那些让偶然性任务异常艰难的人为障碍,比如严苛的硬件约束、笨拙的编程环境、缺乏机器时间。除非偶然性任务占据了工程师 90% 以上的精力,否则把它们全部压缩到零也不会带来数量级的改进。

核心内容

然而,有一个例外彻底改变了作者的想法。很多年后,在某个特定情境下,他再次经历了生产力的深刻飞跃,以至于一开始几乎没有察觉。当他终于意识到并试图向他人解释时,对方也感到困惑。这个情境就是:用 Rust 进行多线程编程。正是这段经历塑造了他对未来编程语言发展方向的看法,因此他需要说服读者:Rust 让多线程编程变得异常简单,其背后有着深刻的原因。

作者举了一个具体的例子。他编写了自己目前这个网站的生成软件,原本是普通的单线程代码。因为比较懒,而且网站规模不大,每次运行时都会重建整个网站。

后来他发现,重建网站的停顿时间已经长到影响编辑某些页面的效率。他很快做了一些单线程优化,但效果不够。他猜测,如果能用多线程重写,就能把停顿降到可接受的范围。

在几乎所有其他编程语言中,重写为多线程都会是一项令人望而生畏的任务。他过去的多线程经验告诉他,自己会立刻遇到难以调试的崩溃,而且几乎肯定会在未来几周甚至几个月里,不断遭遇这类恐怖故事。这正是他停止尝试多线程编程的原因。

但这一次,重写——确实解决了性能问题——只花了不到 5 分钟。它一次运行就正确,并且一直保持正确;而他对这两点有着十足的信心。

这怎么可能?作者很喜欢 Rust,自 2015 年以来一直将其作为主要语言,但它并非完美无缺。他可以(也确实)不厌其烦地详述它的缺陷。但在多线程方面,Rust 做到了一件他过去连想都不敢想的事:数据竞争(即两个线程意外互相干扰的未协调读写)变成了静态错误。这绝非小事:在此之前,数据竞争一直是他编写多线程程序时最大的错误来源。

Rust 通过所有权类型以及 SendSync 这两个 trait 来防止数据竞争。所有权类型的核心很简单:一个对象有一个所有者,所有者可以读写该对象;对象可以被移动给其他所有者,移动后旧所有者失去访问权,新所有者获得访问权。

Send 意味着“这个结构体的实例可以从当前线程移动到另一个线程”(移动后当前线程不能再访问该对象)。Sync 意味着“多个线程可以同时读取这个结构体的实例”。为便于理解,我们可以假设 Rust 会自动判断一个结构体何时是 Send 和/或 Sync,并自动为我们实现这些 trait。

作者用一段简单的 Rust 代码来说明:

fn main() {
    let x = vec![1, 2];
    println!("{x:?}");
}

vec! 创建的向量是 Vec 类型,它实现了 Send。因此我们可以把向量发送到另一个线程,让那个线程打印它:

fn main() {
    let x = vec![1, 2];
    std::thread::spawn(move || println!("{x:?}")).join().ok();
}

std::thread::spawn(...) 用于创建新线程;move || ... 是一个闭包(匿名函数),新线程启动时会运行它;move 表示新线程成为外层函数所引用数据的所有者(即 x 被移动到新线程)。join 表示主线程等待新线程结束。

我们可以验证主线程确实失去了对向量的访问权,因为下面这段代码:

fn main() {
    let x = vec![1, 2];
    std::thread::spawn(move || println!("{x:?}")).join().ok();
    println!("{x:?}");
}

会导致编译时错误:

error[E0382]: borrow of moved value: `x`

这个错误清晰地表明,数据已经被移动,原线程不能再使用它。正是这种机制,让数据竞争在编译阶段就被杜绝。

关键要点

  • AI 目前擅长生成局部代码,但在需要全局理解的程序上常常力不从心,表现为不必要的防御性检查泛滥。
  • 编程语言对软件质量和生产力有影响,但并非决定性因素;好语言可能写出坏软件,坏语言也可能写出好软件。
  • 从汇编到高级语言的生产力飞跃是消除人为障碍的结果,但类似的飞跃很难再次出现。
  • Rust 的多线程编程体验是一个罕见的例外:通过所有权、SendSync,它将数据竞争从运行时错误转变为静态编译错误。
  • 这种转变使得局部推理(检查单个函数或模块)能够保证全局属性(整个程序不存在数据竞争),从而大幅降低了多线程编程的难度和心智负担。

意义与影响

这篇文章指出了一个重要的方向:当 AI 在全局推理上存在短板时,编程语言设计可以成为弥补这一缺陷的关键工具。Rust 已经提供了一个成功的先例,证明通过精心设计的类型系统,可以让开发者在不理解整个程序的情况下,仅通过局部推理就获得对复杂全局属性的可靠保证。

未来,如果 AI 继续难以处理全局性的程序约束,我们可能会看到更多编程语言借鉴或扩展类似 Rust 的所有权与并发模型,让“局部正确即全局正确”成为常态。这不仅有助于人类开发者编写更可靠的软件,也可能让 AI 生成的代码在结构上更加合理,减少因全局理解不足而引入的隐患。Rust 的实践表明,编程语言设计依然有潜力带来深刻的生产力变革,尤其是在那些过去被认为极其困难且容易出错的领域。

查看原文 →tratt.net