【外评】Rust,你错了

最近,一篇批评 Rust 的文章风靡了 r/rust、r/programming 和 HackerNews。这已经不是第一次有人批评 Rust 了,但根据我的经验,这是我为数不多的几次没有看到典型的对作者 “做错, Rust “大加指责的回应。这篇文章是如此透彻,如此掷地有声,以至于连最狂热的人都闭嘴了。

通过这篇文章,我感受到了一种氛围的转变。可以说,终于有人指出了 Rust 的错误。这是事实–“Rust 游戏开发生态系统靠炒作生存”–现在是我们正视它的时候了。有很多 Rust 玩家的回答是 “只要做好就行了”。但这篇文章正确地指出,对于初创公司、小型团队,以及基本上所有只想出货的人来说,Rust 并不是答案。

为什么我的观点很重要?

一年前,我全职担任 Dioxus 的维护者和 Dioxus Labs 的创始人。Dioxus Labs 是一家由 YCombinator 和 Khosla Ventures 支持的初创公司,其成立的理论基础是,我们可以将 Rust 改造成未来应用程序开发的 “万能 “语言。

在使用 Rust 之前,我一直在为等离子体物理模拟(HPC)编写 Python、C、CUDA 和 JavaScript。我不仅要在昂贵的超级计算集群上求解偏微分方程,还要处理糟糕的 Python 安装、与损坏的 CUDA 驱动程序争得头破血流、在糟糕的 C 构建链中磕磕绊绊,并试图理解 JavaScript 生态系统的荒谬之处。研究领域的工具通常都很糟糕,这是有原因的:研究人员没有时间在自己的核心学科之外涉猎数以百万计的不同配置、工具链和维护不善的文档。

六年前发现 Rust 就像在黑暗中发现了蜡烛。我在不到一周的时间内就移植了我的电子等离子体模拟代码,并在短短几天内将其封装在基于 Yew 的前端中。我花了两年时间才完成的工作,只用了一周时间就用一种语言完成了。这感觉就像终于有了一个足够 “锋利 “的工具来解决下一代不可能解决的问题。

人们对 LogLog 这篇文章的普遍反应是:”Rust 并不是为这种用例而设计的。当然,如果在上面使用正确的脚本语言组合,也许可以更快地开发出游戏。但为什么我们不能努力让 Rust 变得更好,成为下一代等离子体物理学家、生物技术研究人员和人工智能工程师 “足够好 “的工具呢?如果我能用现有的语言知识为人类基因组测序或保护世界互联网免受 DDOS 攻击,我才不在乎 TypeScript 在 UI 开发方面是否能提高 20% 的效率。

我的想法是,Rust 可以而且应该不断改进,直到它在人们提出的大多数用例中都足够优秀–我对放弃不感兴趣。我宁愿 “编程界的哈德拉赫 “出现在这一代,而不是下一代。Rustfmt、Rustdoc和Cargo都是了不起的项目–为什么要抛弃它们呢?这个世界上有真正的问题需要解决,人生苦短,不能再等待另一种语言建立起同样庞大的开发者社区、资金和势头了。

Rust 的成功不是技术成就,而是社会成就

我认为,Rust 的成功是社会性的。我的热门观点Rust 的流行并非源于其技术优势。另一种语言,如 Nim、Odin 或 Crystal,要想崭露头角,就必须大显身手。Rust 巧妙地填补了开发者社区对现代编程语言的需求:速度、类型安全和可移植性。Rust 速度快(不同于 Python)、类型安全(不同于 JavaScript)、可移植性好(基本上可以在 LLVM 目标语言的任何地方使用)。而运行时庞大或编译器基础架构怪异的语言则无法做到这一点。

由于 Rust 在社会上的成功,它鼓励开发者社区将精力投入到为语言带来大规模的现代功能上。Rust-Analyzer, rustfmt, cargo, miri, rustdoc, mdbook 等项目都是 Rust 成功的社会副产品。这些项目之所以出现,是因为现代开发人员希望有一种更好的编程语言。没有什么能阻止其他新语言拥有同样的功能,但这需要大量的工作。我所见过的与 Rust 的开发工具极其相似的语言只有 Gleam。这是有可能实现的,但需要大量人才的社会支持才能完成。

很明显,开发者社区对 Rust 这样的语言非常感兴趣。我在创办 Dioxus 之初就认为,Rust 是我们实现 “应用程序开发圣杯 “的最佳机会,而且我们会下定决心:1)解决语言的缺陷;2)在可能的情况下改进语言。我们已经实施了大量的变通方法……但 LogLog 的帖子清楚地表明,我们需要开始推动语言向前发展。

不要误解我的意思,我认为 Nim、Odin 和 Crystal 本身都是令人难以置信的项目,但 Rust 绝对是成功的佼佼者。谷歌、Facebook、微软、亚马逊、Cloudflare 都在自己的堆栈中使用 Rust。Rust 即将进入 Linux 内核……这是一项了不起的壮举。我建议,与其把孩子和洗澡水一起倒掉,不如认真对待并优先解决阻碍 Rust 发展的重要问题。

Dioxus 和 Rust

与 LogLog 一样,Dioxus 也是 Rust 的全部。我们不仅需要快速编写和发布 Rust 代码,还需要确保 Dioxus 的用户也能快速发布 Dioxus 应用程序。我们需要以某种方式说服初创公司和企业:是的,您应该用 Rust 来启动您的新绿地项目。

然而,我们也在努力解决 LogLog 在其文章中提出的许多相同问题。事实上,我们最近发布的 0.5 版本完全专注于解决 Rust 的纸上谈兵问题,为开发者提供了一种以合理方式组织状态的方法,而且不会出现克隆。Dioxus 的成功与 Rust 编程语言直接相关,坦率地说,我觉得 Rust 在很多方面都阻碍了 Dioxus 的发展。

因此,我想在这个舞台上抛砖引玉,争取社区、核心团队和企业赞助商的支持,让 Rust 成为一种适合快速开发的语言。我完全相信这是可能的,我相信我们不需要彻底改造一切,我个人也愿意投入 Dioxus Labs 的资源来解决问题。Dioxus Labs 并没有庞大的资金,但我坚信我们可以为 “修复 Rust “所需的正确更改提供资金。

我宁愿 Rust 基金会和亚马逊/谷歌/Cloudflare/微软赞助相关领域的工作,也不希望 Dioxus Labs 把有限的资金投入到这些问题上。但是,在某种程度上,我不得不承认一个事实:如果 Rust 阻碍了 Dioxus 的发展,那么它就不会腾飞,所以我们应该尽最大努力解决现有问题。通过这篇文章,我想从六年的经验中汲取灵感,就如何让 Rust 适用于快速开发提出一个具体计划。

快速目录

  • 用于自动廉价克隆的捕获性状
  • 私有方法的自动部分借用
  • 已命名和可选的函数参数
  • 正确的反射
  • 全局依赖缓存
  • 预构建板块的形式化
  • Rust JIT 和热重载
  • 增量链接
  • 并行前端
  • 缓存宏扩展
  • 线程安全发送(ThreadSafeSend)–修复工作窃取非发送任务的问题

让迭代 Rust 代码变得更快

自动廉价克隆的 capture trait–从 Swift 偷师 ARC

不管你喜不喜欢,Rust 发现自己已经进入了一个并非最初设计的使用场景。底层内核工程师们正在将 Rust 拖入内核,并对 rustc 留下了大量修改痕迹。在过去几年中,我目睹了高级应用开发者和有抱负的独立游戏程序员将 Rust 越拉越高。有人可能会说,Rust 并不适合这两种环境:如果你想要高级的,就用 Go 或 C#;如果你想要低级的,就用 C 或 Zig。这样的批评很中肯,但正如我之前提到的,本文的论点是,只有坦诚地指出 Rust 语言的不足之处,我们才能既吃到蛋糕,又吃到它。

Rust 的姊妹语言 Swift 也借鉴了 Rust 的许多功能,但并没有过于复杂的垃圾回收器。基本上,Swift 中的所有内容都是 Arc<Mutex<T>>,没有明确要求对值调用 clone()

// an example of some swift code

在 Rust 中,如果我们想在线程之间共享 Arc,就需要明确调用值的克隆:

let some_value = Arc::new(something);

// task 1
let _some_value = some_value.clone();
tokio::task::spawn(async move {
    do_something_with(_some_value);
});

// task 2
let _some_value = some_value.clone();
tokio::task::spawn(async move {
    do_something_else_with(_some_value);
});

如果这看起来很难看、很乏味,那是因为它确实很难看、很乏味。这很快就会让人厌烦。在 Cloudflare 工作时,我不得不处理一个包含近 30 个 Arced 数据字段的结构。生成 tokio 任务的过程如下:

// listen for dns connections
let _some_a = self.some_a.clone();
let _some_b = self.some_b.clone();
let _some_c = self.some_c.clone();
let _some_d = self.some_d.clone();
let _some_e = self.some_e.clone();
let _some_f = self.some_f.clone();
let _some_g = self.some_g.clone();
let _some_h = self.some_h.clone();
let _some_i = self.some_i.clone();
let _some_j = self.some_j.clone();
tokio::task::spawn(async move {
  	// do something with all the values
});

在这个代码库上工作让人意志消沉。我们想不出更好的架构方法–我们需要根据应用状态过滤更新的监听器。你可以说 “笑死我了”,但这个团队的工程师是我共事过的最聪明的人。Cloudflare 全情投入 Rust。他们愿意在这样的代码库上砸钱。如果这就是共享状态的工作方式,核聚变就无法通过 Rust 来解决。

Rust 需要一种新的可选类型,让我们不必再用克隆来污染我们的代码库。谁会真正关心 Rc/Arc 的硬计数?100 次中有 99 次,我都会使用 Arc/Rc 来解决真正具有挑战性的共享状态问题,而硬计数对我来说根本不重要。

我建议将 Capture trait 作为 CloneCopy 系列的一部分。就像Copy類型一樣,clone邏輯會乾淨地插入幕後。当 Capture 类型在作用域之间移动时,Rust 会简单地将它们强制转换为Owned类型。Capture 只适用于克隆 “cheap “的类型,如 Arc/Rc 和其他生态系统定义的类型(如 Channel/Signal)。这将在许多地方出现:

fn some_outer_fn(state: &Shared) {
	// 1. Calling functions and going from &T to T
  // state is automatically coereced from &T to T via a cheap clone
	some_inner_fn(state);
	
	// 2. When working with closures
	// state is `captured` via an implicit call to ToOwned
	let cb = move || {}
	
	// 3. Working with async
	// state is coerced to an owned type when moving into async move scopes
	task::spawn(async move { some_inner_async_fn(state) });
}

// this inner fn takes an owned version of state 
fn some_inner_fn(state: Shared {}

令人惊奇的是,回调(Rust UI 开发的祸根)立刻变得毫不费力:

// Due to rust supporting disjoint borrows in closures, 
// capture would propagate through structs.
// 
// Not a clone in sight - could you imagine?
fn create_callback(obj: &SomethingBig) -> Callback<'static> {
	move || obj.tx.send(obj.config.reduce("some.raycat.data"))
}

struct SomethingBig {
		config: Shared,
    tx: Channel
}

Capture 将为我们提供Copy类型的人体工学设计,而无需使用像 Dioxus 最近发布的 Generational-Box crate 这样的笨拙而又创新的板条箱。世代盒(Generational Box)通过其复制类型(CopyType)提供了与 Capture 相同的语义,它将数据塞入全局运行时,并将访问隐藏在世代指针之后。我们花了 3 个月的时间将其添加到 Dioxus 中–耗费了我们 3 个月的时间–我非常愿意投入同等数量的资源将 Capture 添加到 Rust 本身中。

私有方法的自动 partial borrows

我在上文提到了我在 Cloudflare 工作时的代码库。数千行代码专门用于clone。但真正拖慢我们进度的问题是缺乏部分借用。我们的代码库非常庞大–近 10 万行跨平台代码,都是 5 年前编写的。说到技术债务。我们的结构嵌套了十几层;因为,你还能用什么方法来表示 DNS、WireGuard、WebSockets、ICMP、健康检查、统计等的复杂性?大型 Rust 项目在自身的重压下挣扎,很快就变得几乎无法开展工作。
我们为缺乏部分借用而苦恼。缺乏部分借用会导致代码无法编译:

// Imagine some struct with some items in it
struct SomethingBig {
    name: String,
    children: Vec,
}

// Also imagine this struct has some methods on it
impl SomethingBig {
    // this method modifies the `name` field
    fn modify(&mut self) {
        self.name = "modified".to_string();
    }
    
    // this method reads and returns a reference to a child field
    fn read(&self) -> &str {
        &self.children.last().unwrap().name
    }
}

// bummer....
// This code doesn't compile because `o2` is borrowed while .modify is called
fn partial_borrow(s: &mut SomethingBig) {
    let o2 = s.read();
    let _ = s.modify();
    println!("o: {:?}", o2);
}

当你的代码库不断扩大时,这种做法很快就会令人厌烦。人们会告诉你 “好好干”,重构你的应用程序。很抱歉,Cloudflare 是 Rust 最大的生产用户之一,我可以告诉你,任何人都不可能重构代码库,并为冲刺阶段提供功能和错误修复。公司就是死在这些东西上。我无法想象马修-普林斯(Matthew Prince)会告诉我,WARP 无法在截止日期前完成功能,因为我们无法在同一范围内 borrow children和修改name

六年来,人们一直在讨论关于部分借用的语法建议。当我开始编写 Rust 时,这种势头已经存在。我以为这个问题会在 2018 年得到解决。现在是 2024 年。

如果我告诉你,只需零代码改动,就能在 Rust 中实现部分借用,你会怎么想?这简直就是一个开关,我们可以(假设)在一个小版本中打开它。

抓紧你的袜子(Hold onto your socks)……上面的代码确实可以编译……如果你使用闭包的话:

fn partial_borrow(s: &mut SomethingBig) {
    let mut modify_something =  || s.name = "modified".to_string();
    let read_something =  || &s.children.last().unwrap().name;

    // This works!!
    let o2 = read_something();
    let o1 = modify_something();
    println!("o: {:?}", o2);
}

从 Rust 2023 开始,闭包可以通过一种名为 “不连接捕获 “的技术捕获结构体的字段。部分借用的机制已经存在!我们在 Rust 编译器中已经拥有了它!

但你要问,为什么方法没有启用呢?骑自行车。可以理解的是,人们想要一种专用语法来描述这里发生的借用。对于闭包,生命周期通常是隐式的,因此没有人真正关心语法语义。闭包不可能有公共 API。

我的具体建议是:只对私有方法启用不连接捕获。让 Rust-Analyzer 来提示我的私有方法发生了哪些部分借用。你可以在未来的六年中继续使用 pub fn 语法,但为了 Cloudflare、LogLog 和 Dioxus 今天的成功,我们需要为私有方法打开这个开关。

为私有方法开启 “不连接捕获 “是一项非破坏性变更,只需几个版本即可推出。同样,如果我们对该功能的需求得到满足,并且 RFC 能够及时被接受,我将非常乐意将 Dioxus 的部分资源投入到 Rust 本身中。

已命名和可选的函数参数

另一个污染 Cloudflare 代码库、Dioxus 代码库和其他无数代码库的垃圾来源:构建器模式。有时候,我觉得 Rust 生态系统就像得了斯德哥尔摩综合症……怎么会有人相信构建器是大量字段的合理默认值?为什么这就是我们最好的选择?

struct PlotCfg {
   title: Option<String>,
   height: Option<u32>,
   width: Option<u32>,
   dpi: Option<u32>,
   style: Option<Style>
}

impl PlotCfg {
    pub fn title(&mut self, title: Option<u32>) -> &mut self {
        self.title = title;
        self
    }
    pub fn height(&mut self, height: Option<u32>) -> &mut self {
        self.height = height;
        self
    }
    pub fn width(&mut self, width: Option<u32>) -> &mut self {
        self.width = width;
        self
    }
    pub fn dpi(&mut self, dpi: Option<u32>) -> &mut self {
        self.dpi = dpi;
        self
    }
    pub fn style(&mut self, style: Option<u32>) -> &mut self {
        self.style = style;
        self
    }
    pub fn build() -> Plot {
      todo!()
    }
}

你知道什么会比数百行的构建模式更棒吗?命名的、可选的函数参数。不是明年或后年实现,而是今天:

// just use a function, like every other language
pub fn plot(
   x: Vec<usize>,
   y: Vec<usize>,
   #[default] title: Option<String>,
   #[default] height: Option<u32>,
   #[default] width: Option<u32>,
   #[default] dpi: Option<u32>,
   #[default] style: Option<Style>
) -> Plot {
  todo!()
}

同样,我知道人们多年来一直在讨论这个问题,但我们需要尽快有所进展。坦率地说,这并不需要比最显而易见的解决方案更复杂–其他语言永远都有 kwargs。我对 JavaScript 和 Python 的厌恶可以说是数以百计,但 kwargs 绝不是其中之一,尤其是当它的替代方案是成千上万行荒谬的构建模式废话时。如果你想用构造模式写一个 Cfg blob,你仍然可以这么做,但我们没有理由再这样生活下去了。

更快的 unwrap 语法

也许你认为这是一个问题,也许你并不这么认为。在使用 Dioxus 编写应用程序时,我一直在克隆和解包的海洋中徜徉。Unwrap 并不坏;老实说,我很喜欢 Rust 有这样的错误处理概念。

但是,对于从服务器获取数据的演示来说,这实在是太愚蠢了:

let res = Client::new()
.unwrap()
.get("https://dog.ceo/api/breeds/list/all")
.header("content/text".parse().unwrap())
.send()
.unwrap()
.await
.unwrap()
.json::<DogApi>()
.await
.unwrap();

为什么不能有一种更简洁的解包语法?我们已经有了用于错误传播的问号语法,为什么不把它与用于解包的 ! 结合起来呢?

let res = Client::new()!
	.get("https://dog.ceo/api/breeds/list/all")
	.header("content/text".parse()!)
	.send()!
	.await!
	.json::()
	.await!;

这部分内容并不一定是我想把!作为精确的解包速记连接符写进语言中–我不是语言设计师–但显而易见的是,如果语言能像对待错误传播一样一致地对待解包,我们就能更快地建立原型。

值得表扬的地方

有几件事

  • 尝试trait
  • 专业化
  • 稳定异步读写特质,使执行器 API 标准化
  • 允许编译类型检查失败的构建文件

我们在编译器层面可以做的事情

全局依赖缓存

想象一下,从 GitHub 克隆一个新的 Rust 项目,其构建速度就像增量构建一样快。如果你系统上的每个新构建都和增量构建一样快呢?我们还会说 Rust 的编译时间太烂吗?

全局依赖关系缓存可以让每个新的 Rust 项目在编译时就像已经处于增量编译状态一样。从本质上讲,全局依赖关系缓存就是整个计算机的 sccache,或者说是整个 Rust 生态系统的 sccache。这与将计算机上的每个目标目录都以 symlink 方式链接到一个目标目录中并无不同,从而确保计算机上的每个项目都能共享相同的增量编译工件。

当然,这不会传播到你刚拉出的图中的每个板条箱–新版本的板条箱和编译器会限制缓存的内容–但 rust 编译器的感知速度会快很多。虽然还有一大堆问题需要解决,比如处理 crate cfg 标志,但所带来的好处是显而易见的。看看 Bun 的全局模块缓存有多快。

预编译 crates 的正式化

Rust 本身可以托管一个板条箱缓存,在你下载板条箱时提供预编译的二进制文件和源代码。这些都将由 crates.io 进行验证,从而使 dtolnay 提供预编译 serde_derive 的整个问题变得毫无意义。我们可以选择是否信任 crates.io,是否信任我们的预编译二进制文件,如果是,我们就能为每个人提供免费的 sccache 实现。

更妙的是,我们可以在发布模式(而非调试模式)下提供预编译依赖项,这样大家的调试项目不仅编译得更快,运行得也更快,而且不增加额外成本。我不是第一个想到这一点的人,GitHub 上有一个问题已经开放了 9 年。

让宏默认在发布模式下运行

如果我告诉你 Rust 中的宏总是在调试模式下运行,你会相信吗?每个 serde #[derive(Serialize,Deserialize)]、每个 rsx! 调用、每个 #[component]注解、每个 #[thiserror]属性–它们都在调试模式下运行。这样速度会慢很多,尤其是对于需要生成大量代码的 crates 来说。在使用 Dioxus 构建程序时,我们建议您将宏调试为在发布模式下运行。

在发布模式下运行 rsx!宏可显著缩短编译时间,在增量编译时可节省数秒时间。对于 Dioxus 用户来说,这一点非常重要,因为我们在编译时进行了大量的宏工作,以换取真正快速的运行性能。

您可能会想,如果我不做大量的宏工作怎么办?您的依赖关系和派生 impls 可能都会这样做。Dioxus 使用的是 typed-builder crate 的分叉,这会产生大量代码!

我认为,宏应该默认在发布版本中运行。如果与预编译的二进制文件搭配使用,就能保证 syn/quote/etc. 立即下载并以最快速度运行。

缓存宏扩展

如果我告诉你,每次增量编译都会重新评估板块中的每个宏,你会怎么想?这会很奇怪吧?
下面是一个演示:

 

请注意,我们传入宏的 tokens 从未改变,但宏会不断向dumps目录输出新的时间戳文件。另外请注意,Rust 代码也从未改变过–我们只是在文件中添加了新注释。当任何跨度发生变化时,Rust 应用的少量缓存就会失效,而跨度的变化会受到空白变化的影响!你想过为什么 rust-analyzer 会对无伤大雅的改动喋喋不休吗?你可能多次重新运行了项目中的所有宏。

如果 Rust 缓存了宏的输入和输出,它就只需要重新运行内容发生变化的宏,从而节省了宝贵的编译时间。尤其是增量编译。就像预编译依赖关系和发布模式宏一样,这也是能在整个 Rust 社区产生巨大积极影响的低悬果实。

并行前端

最近,Nicolas Nethercote 发表了一篇关于在 nightly 中使用并行前端加快编译速度的文章。我推荐你阅读这篇文章,以便更深入地了解这一变化。简而言之,Rust 编译器分为两个阶段:前端和后端。后端基本上是一个 LLVM 工作池,而前端则是为 LLVM 工作池提供类型检查和 MIR 降低。

对 Rust 编译器的这一改动将使类型检查和 MIR 降低工作在不同板块间并行进行,在演示案例中,前端阶段的工作将减少一半,性能提高约 25%。稳定这项工作将为整个生态系统带来巨大的性能提升。当然,您现在就可以通过夜间 Rust 来实现这一点,但我个人还是希望将其作为优先稳定项目。

增量链接

最近在 r/rust 上出现了一个关于 David Lattimore 在 Wild 上工作的项目:Rust 的增量链接器。目前 Wild 实际上并不做增量链接–用 Rust 写一个链接器已经很费劲了–但预计的计划是支持增量链接。

如果你不熟悉:每个提前编译的项目,无论是 C/C++/Zig/Rust 还是 Rust,都要经过两个阶段的编译:代码生成和链接。在代码生成阶段,编译器会浏览你的项目,并为看到的每个函数、结构、枚举等生成汇编代码。当你调入类似 windows-rs 这样的 crate 时,由于需要生成大量代码,这可能会变得很昂贵,但一般来说,你只需为依赖关系支付一次代码生成费用,然后在重新编译主项目时再支付一次。

编译器走完代码后,会将其传递给 LLVM,然后由系统链接器链接到一起。这包括从 “main “到每一个引用符号的二进制走行,然后将生成的代码拉入最终输出的二进制文件。

这里需要注意的重要一点是:编译可能是增量的,但链接却不是。你的所有依赖项(虽然它们的代码根可能被缓存)都会在每次构建时重新链接。在全新构建的情况下,代码根占据主导地位。不过,对于大型项目来说,链接可能会耗费货物构建总时间的 50%(在本例中,10 秒的增量构建耗时 5 秒)。

增量链接器可以避免这个问题。它不会从头开始重新链接整个二进制文件,而是缓存现有的链接,只对在两次构建之间发生变化的项目执行链接。对于大型项目(如 LogLog 的电子游戏)来说,这几乎可以省去 99% 的链接时间来处理小的改动。

Rust JIT 和热重载

我想介绍的最后一项内容是 Rust 的 JIT。目前,Rust 编译器作为超前编译器运行,通过 LLVM 生成最终输出二进制文件。这对于需要最大程度优化和正确性的高性能、可移植、无运行时的编译来说非常好。但对于迭代式游戏和应用程序开发来说,LLVM 就显得有些多余了。当然,它给了我们很多,但也限制了我们的工作。

Dioxus 的同类产品是 Flutter,它是谷歌推出的跨平台应用开发框架,由 Dart 提供支持。Dart 既有像 Rust 一样的 AOT 引擎,也有用于开发的 JIT 引擎。这个 JIT 引擎非常强大,可以在运行中热载函数等。为 iOS 和 Android 构建原生应用程序的开发人员通常会抱怨编译时间太长,而 Flutter 开发人员却能愉快地使用即时加载的应用程序。

迄今为止,Dioxus 团队在为我们的 rsx!宏内容实现热重载方面付出了大量努力,但我们真正需要的是直接为 Dioxus 的组件实现热重载。在 Rust 中获得适当的原生热重载支持,可以让我们放弃大量支持功能较弱的模板重载所需的代码,这也是我个人最感兴趣的资助领域。

JIT 的核心是完全推迟代码生成,直到找到代码路径,基本上完全省去了编译时间。并行前端、发布模式宏和宏缓存所带来的速度提升仍然很重要–如果不运行 rustc 前端,我们就无法生成 JIT 中间体–但我们基本上不会在代码生成和链接上花费时间。我个人认为,一个合适的、支持热重载的 Rust JIT 后端将是 Rust 编译时间迭代的最重要升级,从而使 Rust 更适合应用和游戏开发。

现在,这个领域已经有了工作!勇敢的 bjorn3 似乎是独自一人,一直在努力推进 rustc_codegen_cranelift。在我看来,这项工作应该得到大量的资金支持,并配备专门的编译器工程师团队,将 JIT 引擎与增量链接器紧密集成在一起。根据 cranelift.dev:

据测量,它的代码生成过程比基于 LLVM 的同等系统快一个数量级。

Rust 的编译时间可以忽略不计。

综合考虑 – 编译时间

在增量链接、并行前端、宏扩展缓存、发布模式宏和预编二进制文件之间,我确信我们可以将 Rust 的编译时间缩短 90% 或更多。这些步骤都不是不可能实现的,但它们需要专业的工程师和大量资金的支持。

本文文字及图片出自 Dioxus Labs + “High-level Rust”

你也许感兴趣的:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注