Ohhnews

分类导航

$ cd ..
foojay原文

用 Java 而非 Rust 重写一切:词处理器性能与跨平台对比

#java#rust#codename one#性能对比#跨平台开发

[LOADING...]

Rust 非常适合内核、驱动或数据库引擎。这类代码不能因垃圾回收而暂停,必须精打细算每一个字节,Rust 的所有权模型在其中物有所值。但不知从何时起,“用 Rust 重写”成了万物的默认答案,桌面和移动应用也不例外。对于应用,我认为这是一笔错误的交易。你为编写、审查和构建代码付出更多,却没有换来更好的应用。

Codename One 是什么? Codename One 是一个开源框架,可用单一 Java 或 Kotlin 代码库构建原生 iOS、Android、桌面和 Web 应用。了解更多请访问 codenameone.com。

所以我们不争论,直接测试。Wordcraft 是一个用 Rust 和 egui 编写的开源文字处理器。它是一个严肃的项目,在 GitHub 上有超过 15,000 颗星,具备功能区、样式、表格、修订追踪和邮件合并,并发布了包含 502 项的功能清单,这使它成为一个公平的对比对象。我们用 Java 和 Codename One 构建了同一个文字处理器,实现了 Wordcraft 所实现的每一项功能,并在同一次 CI 运行中、相同的 GitHub 运行器上测量了两个应用。

Java 版本更快、更小、占用内存更少,代码量约为三分之二,运行测试的成本也低数倍。它用起来也更原生:有真正的 macOS 菜单栏,可将带格式文本复制到其他应用,还能在文档与操作系统其余部分之间拖放文本和文件。而且它用同一份源码发布到 iPhone 和 Android。详情见下文。

这也是忙碌一周的开始。文字处理器用一个应用完成了论证;本周余下时间则关乎你今天就能拿什么开始。三个新的全栈模板加入 Wayline:一款实时 3D 策略游戏、一个端到端加密通讯工具和一项外卖服务。每一个都是单个 Java 项目中的应用及其服务器,可自定义品牌并发布。

本周即将发布

今天是 Java 对阵 Rust 的故事。然后每天一个模板,随后是这些模板推动 Codename One 增加的框架功能:

[LOADING...] Skirmish,星期六。 [LOADING...] Parley,星期日。 [LOADING...] Savor,星期一。## 数据

下面的每个数字都来自同一次 CI 运行(39825464193),该运行从源码构建两个应用,并在相同的机器上、针对相同的文档运行它们。工作流位于仓库中,因此你可以重新运行它。桌面端数值是在一份 1,000 段文档下,macOS / Linux / Windows 的中位数。

我们测量的指标Java(Codename One)Rust(Wordcraft)
启动到首次绘制文档370 / 231 / 288 ms821 / 246 / 455 ms
输入一个字符到绘制帧8.9 / 10.7 / 12.4 ms51.9 / 14.9 / 24.9 ms
空闲内存112 / 111 / 90 MiB145 / 187 / 106 MiB
可执行文件大小34.7 / 37.7 / 32.5 MiB55.0 / 75.2 / 57.5 MiB
功能目录行可实现 487/487已实现 487 项,15 个占位
应用代码76,275 行118,659 行
AI 代理通读全部代码所需 token 数1,006,3951,646,847
冷构建与完整测试运行97 秒961 秒
每个测试的 CI 时间86 ms500 ms
同一套源码支持的平台macOS、Linux、Windows、iPhone/iPad、Android、浏览器macOS、Linux、Windows、FreeBSD、浏览器

在 42 项桌面测量中,Java 赢了 38 项。图表以 macOS 上的比值展示差距,因此 2× 表示 Rust 版本需要消耗该资源的两倍:

[LOADING...]

其余四项差距很小,且没有一项能在所有操作系统上胜出:

测量项Rust 领先的地方Java 领先的地方
启动含一段落的文档Linux,领先 4 毫秒(166 对 170 毫秒)macOS(320 对 651 毫秒)和 Windows(189 对 343 毫秒)
保存 DOCXLinux,领先 1.8 毫秒(5.9 对 7.7 毫秒)macOS(7.7 对 12.0 毫秒)和 Windows(7.4 对 10.3 毫秒)
工作负载期间的峰值内存macOS 低 7 MiB,Windows 低 18 MiBLinux(190 对 241 MiB)

启动近乎空白的文档时,4 毫秒的差异是人的感知不到的;而在另外两个系统上,同一项测量中 Java 领先数百毫秒。

它更快

输入延迟是用户能感受到的数字。用户在 Java 应用中输入一个字符,在 macOS 上 8.9 毫秒即可到达屏幕;Rust 应用则需要 51.9 毫秒。在 60 Hz 下,一帧持续 16.7 毫秒,因此一个应用能在一帧之内完成,另一个则要超过三帧。撤销也是同样的情况,8.9 毫秒对 52.3 毫秒。

打开文档也体现了这一点。重新打开测试 DOCX 在 macOS 上耗时 31.6 毫秒,Wordcraft 为 132 毫秒;在 Windows 上为 31.5 毫秒对 261 毫秒。

Java 在这里如何击败 Rust?Codename One 通过 ParparVM 对应用进行提前编译,先剔除应用永远不会触及的每个类和方法,再把字节码翻译成 C。随后由 C 编译器在剩余代码上进行内联和优化。运行时没有 JVM。垃圾回收让分配变得廉价,并在自己的线程上回收内存,因此应用代码无需做所有权记账、无需防御性克隆,也无需引用计数。UI 在一个线程上运行,而保存等工作在后台运行;保存时还会同时在多个核心上压缩大部分内容,不必围绕生命周期或 Send 约束来设计。我们没有为这个基准测试使用配置文件引导优化或特殊调优。这些都是每个 Codename One 应用默认获得的效果。

它更小,而且扩展性更好

“垃圾回收浪费内存”是我说 Java 应该用于桌面和移动应用时,人们告诉我的第一句话。测量结果却相反。可执行文件在每个操作系统上都不足三分之二大小,在 Linux 上只有一半。三个平台上的空闲内存都更低,而且差距随文档规模扩大:

空闲内存1 段1,000 段10,000 段
macOS,Java / Rust96 / 106 MiB112 / 145 MiB220 / 364 MiB
Linux,Java / Rust94 / 164 MiB111 / 187 MiB198 / 387 MiB
Windows,Java / Rust71 / 82 MiB90 / 106 MiB169 / 308 MiB

对于 10,000 段的文档,Rust 版本需要多 65% 到 95% 的内存。在 macOS 上,打开该文档的启动时间为 917 毫秒对 1,670 毫秒。手动内存管理本身并不会让应用更精简。真正决定这一点的是数据结构,而垃圾回收器让你可以挑选好的数据结构,而不是只能挑选借用检查器接受的数据结构。

它对 AI 和 CI 的成本更低

Java 代码库的代码行数约为三分之二,token 数约为五分之三。如果你使用编码代理,token 就是成本模型:每个任务都从代理阅读代码开始,而它提出的每一处编辑都是由人审查的代码。相同的功能目录下,更小的代码库意味着更少的阅读量、更小的上下文和更小的差异。

这个文字处理器本身就是以这种方式写成的。我先用 Codex,后来改用 Claude;Codename One 内置的 MCP 服务器让代理可以在工作时驱动正在运行的应用并检查它。

CI 也说明了同样的事。在同一台运行器上,冷构建加完整测试套件,Java 应用耗时 97 秒,Wordcraft 耗时 961 秒。Java 测试在 JVM 上针对与原生发布相同的代码运行,因此测试运行不需要原生工具链。Rust 在运行任何东西之前,要先编译整棵依赖树和测试二进制文件。两个测试套件规模不同(1,123 个测试对 1,922 个),因此按每个测试计是 86 毫秒对 500 毫秒。Wordcraft 约有四分之一的测试覆盖滚动视口、弹窗放置和窗口状态这类底层管线,而 Codename One 把这些作为框架服务提供并只测试一次。

它更容易审查

下面是拼写检查器如何在不拖慢启动的情况下加载其 2 MB 词表,代码来自 Proofing.java:

$ java
public static synchronized void preload(final Runnable done) {
    if (preloading) return;
    preloading = true;
    new Thread(
            (Runnable) () -> {
              WordList.get();
              extra();
              done.run();
            },
            "proofing-load")
        .start();
}

任何 Java 开发者都能在几秒内审查这段代码。一个后台线程填充共享结构,内存由垃圾回收器负责。没有生命周期,没有 Arc<Mutex<...>>,也没有 Send 约束。

熟悉程度也很重要。每天阅读 Java 的开发者远多于阅读 Rust 的开发者,因此审查变更的人更可能熟悉这些惯用法。Java 对他们的要求也更少。审查者检查代码做了什么,而不是检查所有权、借用、生命周期和 trait 约束是否以某种恰好能编译的方式得到满足。这种更轻的心智负担是真实的成本差异。当一个代理为你写了 500 行代码时,人类多快能审查完它们,决定了你推进得多快。

它更具可移植性

同一套源码可构建原生 iPhone 和 iPad 应用、Android 应用、浏览器应用,以及 macOS、Linux 和 Windows 桌面应用。CI 在每次运行中都会构建所有这些目标,并附上每个目标的截图。在手机上,功能区会折叠成标签选择器,页面会按屏幕宽度重排,触摸工具栏会取代状态栏。Wordcraft 没有移动端目标。

有一个 Wordcraft 支持而我们不支持的平台:FreeBSD。Codename One 目前不以它为目标。如果对 FreeBSD 支持有需求,我们会加上,所以请在评论中告诉我们。

[LOADING...] iPhone
[LOADING...] Android
[LOADING...] 同一个应用在浏览器中。

它感觉更原生

这是我最在意的部分,因为用户在看到任何基准测试之前就能感受到它。可移植性通常意味着最低共同标准。这里恰恰相反:Java 应用比 Rust 应用更深入地与操作系统集成。在 Mac 上,它有真正的菜单栏和标准快捷键,能复制 Pages 和 Mail 可正确粘贴的格式化文本,还能把选区拖到另一个应用中,或从访达拖入文件和文本:

功能Java(Codename One)Rust(Wordcraft)
macOS 菜单栏原生菜单栏,包含约 40 个命令和标准快捷键没有菜单栏,命令只存在于功能区
复制到其他应用HTML、RTF、Markdown 和纯文本,因此 Pages、Mail 或浏览器会收到格式化文本仅纯文本
拖出到其他应用可将选区作为富文本拖出没有拖拽源
拖入文件和文本,落在光标处仅文件
粘贴HTML、RTF、Markdown、图片和文件HTML、RTF 和图片
高对比度实时跟随操作系统设置不支持
iPhone、iPad 和 Android带触摸选择柄的原生应用没有移动端目标

[LOADING...] macOS 上的原生菜单栏,带有子菜单。
[LOADING...] “关于”、“设置”和“退出”出现在 Mac 用户期望的位置。周三的文章展示了任何应用如何获得这些能力。

它看起来至少一样好

Wordcraft 在左边,Java 应用在右边。两者都是精致的桌面应用,并且都实时跟随操作系统的深色模式。但只有一个在手机上看起来也正确,并遵循高对比度设置。

[LOADING...] Wordcraft(Rust)。截图来自其 README。
[LOADING...] macOS 上的 Java 版本。
[LOADING...] 深色模式,实时跟随操作系统。

它同样安全

内存安全是大多数人选择 Rust 的理由,而且这是个好理由。Java 自 1995 年以来就是内存安全的。边界会检查,没有悬垂指针,也没有手动 free。值得称赞的是,Wordcraft 在自己的 crate 中禁止 unsafe,因此就内存安全而言,这两个应用不相上下。

区别在于供应链。Wordcraft 的 Cargo.lock 引入了 534 个外部 crate。Java 应用依赖 Codename One 框架及其自身模块。每个依赖都是有人必须信任、更新和审计的代码,因此更小的依赖树意味着更小的攻击面。

Rust 能追上吗?

在体积和速度方面,也许能。但这需要大量工作,并要深入重新思考内存布局和内联,而这又意味着更多代码和更慢的构建。要在这些方面赶上 Java,就意味着在 Rust 内部重建 Java 已经提供的东西。这就是我所说的成本。

说点个人感受。我每天都用 Firefox,也喜欢 Rust 相关工作为它带来的速度和稳定性提升。但 Firefox 是个特例,而且我认为它恰好证明了我的观点。Rust 替换的是底层部分:样式引擎、渲染器、媒体解析。这正是 Rust 擅长的领域,而且它在那里表现得很好。浏览器的用户界面,也就是高层部分,是用 JavaScript 写的——一种 JIT 编译、带垃圾回收的语言,给了团队灵活性。Firefox 实际上为自己重新发明了 Java 的高层部分。浏览器必须同时拥有这两部分。而你的应用只有第二部分。

为什么这现在很重要

我在 2018 年写 Create an Uber Clone in 7 Days 时,目标是构建一个 Uber 风格的应用。今天,这应该是你的起点。借助编码代理,一个开发者可以承担过去需要团队才能完成的范围。Hello world 不再只是屏幕上的一个标签。它是一个完整应用,带有后端、管理控制台、支付和推送通知,而你的工作从那里开始。

在这种规模下,语言的开销会不断累积。每一行额外代码都是代理要写、人类要审查、CI 要在每次变更时构建的内容,而且会贯穿产品的整个生命周期。Java 就是为许多人维护的大型代码库而设计的,这一点很明显。它拥有庞大的生态系统、可读的代码、快速的编译器、快速的测试,以及一个把内存管理从你手上接过去的垃圾回收器。把这份预算花在设置界面里的生命周期上,不会为用户带来任何价值。

这就是 Codename One 正在走的方向:完整的起点,加上真实产品在代码之外所需要的一切。

每个模板都是一个可运行的应用程序及其服务器,位于同一个 Java 项目中。客户端和服务器共享 API 类型,因此 REST 调用或 WebSocket 消息在两端都会进行类型检查。增长和变现能力内建在框架中,而不是事后加装:邀请好友链接、分析、推送通知、广告和应用内购买都是 Codename One API,模板已经在使用它们。商务是我们正在做的下一部分。模板源码在 MIT-0 下免费提供,无版税,也无收入分成。## 本周更多内容

一款免版税的实时 3D 策略游戏

多年来,我一直避免把 Codename One 推荐用于游戏。但在我们构建这些模板时,这种情况改变了:一款现代移动游戏主要是一个应用。登录、菜单、商店、好友、任务、推送和服务器,而战斗只占代码的一小部分。Skirmish 是一款《皇室战争》风格的实时策略游戏。两名玩家在由带动画的 glTF 角色组成的 3D 竞技场中面对面,各自从牌组中召唤部队和法术,并试图摧毁对方的塔。对局通过 WebSocket 以锁步方式在线运行,并带有回放、进度系统、宝石包、赛季通行证、广告和邀请。

下载 Skirmish 演示视频。

视频中的每个菜单都是普通的 Codename One 组件,使用布局管理器进行布局,并用 CSS 设置样式。

[LOADING...] 主界面、宝箱和任务。 [LOADING...] 在线对局。 [LOADING...] 商店。

游戏本身只是交付内容的一半。The Forge 是游戏内置的关卡编辑器,玩家和你的设计师可以在其中构建单位、竞技场、任务和完整战役,并将它们作为资源包分享。还有一个管理控制台,像运营在线服务一样运营游戏:实时对局、举报与标记、带可审计审核的玩家历史、游戏数据查询、用户参与流水线和促销活动。它既能在手机上运行,也能在桌面端运行。

[LOADING...] The Forge,关卡编辑器。 [LOADING...] 设计竞技场。 [LOADING...] 管理控制台。 [LOADING...] 桌面端实时运营。 [LOADING...] 用户参与流水线。

向游戏中添加一个单位,只需在共享规则模块中写一行代码:

$ java
add(new CardDef("squire", "Squire", CardDef.TROOP, 3)
        .hp(1450).damage(160).hitTicks(24).range(700).speed(45).radius(500));

没有版税,没有按安装量收费,也没有收入分成。周六的 Skirmish 文章 详细介绍了竞技场、多人模式以及商业化方式。

一款服务器无法读取的即时通讯应用

医院、企业和社区都想要自己的即时通讯工具,但无法为此使用 WhatsApp。Parley 就是这样一款通讯应用:聊天、群组、频道、语音消息、语音和视频通话以及关联设备,均使用 X3DH 和 Double Ratchet 进行端到端加密,这正是 Signal 采用的设计。服务器只存储和转发密文,通话则采用点对点方式。

[LOADING...] 聊天、群组和频道。 [LOADING...] 加密对话。 [LOADING...] 点对点通话。 [LOADING...] 无需阅读消息即可进行审核。

加密代码读起来就像普通 Java,因为这些密码学原语现在已成为 Codename One 的一部分:

$ java
KeyPair ephemeral = KeyGenerator.ec(256);
PrivateKey me = Identity.privateKey();
byte[] dh1 = KeyAgreement.ecdh(me, pub(signedKey));
byte[] dh2 = KeyAgreement.ecdh(ephemeral.getPrivateKey(), pub(identity));
byte[] dh3 = KeyAgreement.ecdh(ephemeral.getPrivateKey(), pub(signedKey));

周日的 Parley 文章 介绍了协议、内容审核以及如何为它打造品牌。

没有平台抽成的外卖服务

外卖平台通常会从每笔订单中抽取 15% 到 30%。Savor 为餐厅、连锁品牌或本地创业公司提供属于自己的服务:一个应用,其首页会根据角色(顾客、餐厅、配送员或管理员)而变化;银行卡预授权在配送时扣款;在地图上实时跟踪,且无需支付 API 密钥费用;订单和配送状态机不会在两个人同时操作时发生混乱。

[LOADING...] 顾客 [LOADING...] 餐厅 [LOADING...] 配送员 [LOADING...] 管理员

周一的 Savor 文章 展示了这一流程,以及从单一餐厅到杂货或鲜花配送的各种重塑方式。

更多 Java 内容,以及一个关于 Unity 的问题

类库新增了 116 个类和接口,包括 java.util.function、流、Optional、java.time 以及读取器和写入器。现有 Java 代码在移植时需要的改动更少,ParparVM 和混淆器会剔除应用未调用的内容,因此开销很小。我们仍然有意不支持反射和类加载。

我们还尝试了一项新事物,从 Unity 做得好的地方学习:简单的 Unity 2D 项目,包括 C# 脚本在内,现在可以构建并作为 Codename One 应用运行。五款开源游戏未经修改即可运行。这是一个概念验证,你并不能拿任意 Unity 游戏来运行。

[LOADING...] Fruitopia [LOADING...] 推箱子

这对你有用吗?周二的 Java 与 Unity 文章 提出了这个问题,还介绍了一个更快的 XML 解析器,以及 API 文档中一个可在浏览器中运行每个示例的按钮。

拥有原生体验的桌面应用

这款文字处理软件需要真正的菜单栏和真正的文件对话框,才能与原生应用相比不落下风,所以现在每个 Codename One 应用都拥有这些能力。只需一次调用,Command 就会进入原生菜单栏,标准命令会出现在各操作系统期望的位置,并使用各自的快捷键:

$ java
Command settings = Command.createDesktopRole(Command.DESKTOP_ROLE_SETTINGS, e -> showSettings());
Command quit = Command.createDesktopRole(Command.DESKTOP_ROLE_QUIT, e -> quit());

在 Mac 上,它是带 Cmd+, 快捷键的“Settings...”;在 Windows 上,它是“Tools”下的“Options”;在 Linux 上,它是“Edit”下的“Preferences”。打开和保存对话框都是操作系统自带的,带有筛选器、选项,并在平台支持时提供预览:

[LOADING...] 带有预览和选项的打开对话框。 [LOADING...] macOS 应用程序菜单中的标准命令。

周三的 桌面端文章 介绍了细节,以及 Skirmish 背后的动画 3D 模型。

通话、加密,以及会让构建失败的 ORM

语音和视频通话现在是一个 Codename One API,在 Android、iOS 和浏览器上基于 WebRTC 构建。信令仍由你掌控,因此同一份代码可与任何服务器配合使用:

$ java
WebRtc.openUserMedia(true, true, true).ready(media -> {
    pc = WebRtc.createPeerConnection(config, listener);
    pc.addTrack(media.getAudio());
    pc.addTrack(media.getVideo());
    pc.createOffer().ready(offer -> pc.setLocalDescription(offer)
            .ready(done -> signalling.send(offer)));
});

将密钥协商结果转换为加密密钥只需两行代码,并且在每台设备和服务器上行为一致:

$ java
byte[] shared = KeyAgreement.ecdh(mine.getPrivateKey(), theirs.getPublicKey());
byte[] key = Hkdf.derive("SHA-256", null, shared, "chat message key v1".getBytes(), 32);

在服务器端,一个要求客户端证书和 HTTP/2 的合作伙伴 API,只需一个构建器:

$ java
WebClient partner = new WebClient()
        .setClientCertificate(ClientCertificate.pkcs12(archive, password))
        .setRequireHttp2(true)
        .setRequestTimeoutMillis(10000);

而在使用 Hibernate 和 Spring 时,大量映射错误都能顺利编译,却会在应用启动时,或在第一个触及该实体的请求出现时爆发。Codename One 的 ORM 会在编译时读取你的实体,因此下面这些问题都会成为构建错误:

@Temporal requires a java.util.Date field: signedUp
Persistent fields must be writable: name
@Entity class com.example.Customer must declare a public no-arg constructor

周四的 通话与后端文章 逐一介绍了这些内容。

这一切将走向何方?

我不希望这被解读为“Rust 不好”。如果你在编写内核模块、编解码器、嵌入式控制器,或任何有硬实时限制的东西,确定性比吞吐量更重要,那么 Rust 是极佳的选择。面向用户的应用则有不同的约束。它可以牺牲确定性,来换取吞吐量、更小的代码库,以及更快的团队节奏。

对于应用而言,Java 是更好的工具,而 Codename One 正在成为一个让你从完整产品而非空白屏幕起步的地方。这款文字处理软件现已出现在 演示页面 上。克隆 代码仓库,重新运行基准测试工作流,并核验我的数字。如果你发现我某项测量有误,请在那里提交 issue。如果你想今天就启动一个项目,Initializr 可以在一分钟内生成任意模板。