NetWasm 让 C# 在 Wasm 体积上胜过 Rust
C# now beats Rust on WASM size
独立 .NET WebAssembly 编译器 NetWasm 在相同 print-42 工作负载下产出 47,895 字节的 WASI Preview 2 组件,比 Rust 1.95.0 开启体积优化后的 53,635 字节小 10.7%,经 Brotli-11 压缩后小 19.5%。
面向 WebAssembly 和 WASI 的独立 .NET。
编写 C#。部署小巧、独立的 Wasm。
NetWasm 是一个独立的编译器、CoreLib、运行时和 SDK,可将 C#
和 .NET CIL 直接编译为 WebAssembly。它不会将桌面 CLR、Mono、
dotnet.js 或应用程序 DLL 带入生产环境。
将 C# 编译为 WebAssembly · 在浏览器中试用 · 安装并构建 · 查看支持的 API · 阅读文档
C# 在 Wasm 体积上现已胜过 Rust
NetWasm + 精确 GC:47,895 字节
Rust 1.95.0 面向体积的配置:53,635 字节
对于相同的 print-42 工作负载,NetWasm 的结果包含运行时和
精确的 Compact 垃圾回收器。两个产物都是独立的 WASI Preview 2
组件,均按开发者开箱即得的方式构建:各自
工具链的标准库和常规构建,未手工添加任何工具。
原始体积小 10.7%。经 Brotli-11 压缩后小 19.5%。
| 工具链 | 原始 Wasm | Brotli 11 | 对比角色 |
|---|---|---|---|
| NetWasm,Compact GC | 47,895 B | 13,660 B | WASI P2 组件;包含精确 GC |
| Rust 1.95.0,体积配置 | 53,635 B | 16,963 B | 匹配的 WASI P2 组件参照 |
Kotlin 2.4.0 wasmWasi |
79,293 B | 22,655 B | P1 上下文;GC 由宿主引擎提供 |
| TinyGo 0.39.0 | 93,043 B | 26,931 B | P1 上下文;配置了精确 GC |
Rust 是重点对比对象,因为它使用相同的工作负载和输出 形式。Kotlin/Wasm 和 TinyGo 展示了更广泛的体积格局,但它们的 目标和运行时边界不同。这是实测的工作负载/配置 结果,而非语言层面的体积排名。
NetWasm 的数字是模板应用程序的默认 Release 发布结果。Rust 的
数字则在普通 cargo build 基础上加入了 Cargo 文档记载的体积设置(opt-level = "z"、LTO、一个 codegen
单元、panic = "abort" 以及剥离符号)。
Binaryen 的 wasm-opt 在这里不是后处理步骤:它是 NetWasm 的
编译器流水线的一部分,在每次常规构建中都会运行,无需安装或
启用任何东西。Rust 的 wasm32-wasip2 输出是一个组件,因此对其应用 wasm-opt
意味着提取核心模块、优化它,再手工重新编码该组件。
两种语言都能做到更小,但那属于另一类程序:Rust
使用 no_std,或 C# 使用 bflat 的 zero library 之类的最小库,各自
放弃大部分标准库,对 C# 而言还要放弃垃圾回收器。
比 Mono AOT 更快
NetWasm 运行我们的分配和垃圾回收基准测试 比 Mono AOT 更快,二者由相同的 C# 源码编译,并由相同的 Wasmtime 运行:
| 工作负载 | NetWasm,NetWasmGC | Mono AOT,.NET 10 | NetWasm 时间 |
|---|---|---|---|
| 完整事务 | 43.9 ms | 59.1 ms | 少 26% |
| 保留对象图 | 8.8 ms | 14.2 ms | 少 38% |
| UI 频繁变更 | 17.6 ms | 29.1 ms | 少 40% |
这些是在 Apple M1 Max 上、Wasmtime 49.0.3 下各运行 15 次的中位数,每次运行
都与桌面 .NET 的结果进行了核对。Mono AOT 是 Microsoft 的 wasi-wasm
目标在其默认 Release 配置下的结果。NetWasmGC 在前两个工作负载上耗时也少于
桌面 .NET 11 的 JIT(49.9 和 9.7 ms);JIT 在 UI 频繁变更上更快(8.6 ms),而桌面 NativeAOT 在三项上都是最快的。
不止于 Hello World
46.7 KiB 的程序只是基线,而非边界。
C# 15 · JavaScript 模块与 Web Worker · 静态 .a 互操作 · WIT/WASI P2 ·
表达式树 · HTTP/JSON/XML/Regex · 无反射 DI ·
TUnit/VSTest · wasm64 · 四种分配策略
目前 NetWasm 可以:
- 通过普通的
dotnetCLI 构建、运行、发布和测试项目; - 将 C# 导出发布为类型化的 JavaScript 模块, 可在页面中或 Web Worker 中运行;
- 链接兼容的 Wasm
.a归档, 并生成有界的 C ABI 封送; - 从 WIT 生成 C# 绑定,并生成独立的 WASI P2 组件;
- 在不使用通用运行时反射的情况下构造和检查表达式树;
- 使用独立打包的 LINQ、HTTP、JSON、XML、Regex、哈希和 依赖注入库;
- 为
netwasm0.1和桌面 .NET 提供双目标库。
该配置刻意比桌面 .NET 更小。它已经支持 完整应用程序、可移植库、测试项目和浏览器模块。
试一试
安装 .NET SDK 10.0.303 或更高版本, 然后运行:
dotnet new install NetWasm.Templates
dotnet new netwasm-app -n HelloNetWasm
cd HelloNetWasm
dotnet restore
dotnet run
dotnet publish -c Release -o publish/local使用兼容的 WASI Preview 2 宿主运行已发布的组件,例如 Wasmtime:
wasmtime run publish/local/HelloNetWasm.wasm
42
已发布的组件在 .NET 方面是自包含的。仍然需要 WebAssembly 宿主。用 .NET 构建;无需 .NET 运行。
使用 C# 15 的项目选择 .NET 11 SDK;较早的语言版本可以继续 使用 .NET 10。SDK 通过 NuGet 还原其固定版本的 Node、LLD 和 Binaryen 工具, 因此普通应用不需要单独安装 Emscripten、Git 或 Python。有关宿主、 还原、Windows、浏览器和双目标定位的详细信息,请参阅完整快速入门。
如何保持小巧
Roslyn 生成 CIL。NetWasm 将可达程序直接编译为 Wasm, 对其泛型进行特化,并仅链接所需的运行时支持。链接后的 Wasm 组件就是程序。
- 封闭世界编译。 不可达代码、导入和运行时特性 不会进入产物。
- 更小的平台契约。 NetWasm 支持可移植 API,而不是 复现每一个桌面运行时假设。
- 没有广泛的运行时反射。 运行时类型名称、动态程序集加载
和
dynamic关键字不在该配置范围内。 - 生成的边界。 JSON、DI、WIT 和原生互操作使用已知契约, 而不是运行时发现。
- 可选数据和诊断。 时区数据、丰富的堆栈跟踪和其他 部署数据仅在选中时包含。
- 一个收集器。 仅链接所选的分配策略。
这是一个新的 .NET 平台,而不是把桌面 .NET 塞进 Wasm 沙箱。 正是这一边界使原生 Wasm 大小的 C# 成为可能。
选择你的分配策略
Compact 是默认选项:一种大小优先、精确、非移动的标记-清除 收集器。它跟踪编译器发布的根和引用布局,而不进行 保守扫描。其名称指的是实现大小,而不是移动式堆压缩。
| 策略 | 最适合 | 原始组件 | Brotli 11 |
|---|---|---|---|
| Compact(默认) | 小型应用和低变动 UI | 47,895 B | 13,660 B |
| NetWasmGC | 更高的分配吞吐量 | 50,855 B | 15,053 B |
| Bump | 有界的一次性任务 | 43,864 B | 11,959 B |
在可执行项目的 .csproj 中选择策略:
<PropertyGroup> <NetWasmGarbageCollector>NetWasm</NetWasmGarbageCollector> </PropertyGroup>
未设置该属性时选择 Compact。各库保持收集器中立;四种后端均打包为 wasm32 和 wasm64。有关权衡和诊断信息,请参阅选择垃圾收集器。
库应向上兼容
为较小的 NetWasm 配置文件编写的库也应能在桌面 .NET 上使用,而不是被困在仅限 Wasm 的生态系统中。
dotnet new netwasm-lib -n MyLibrary
该模板面向 netwasm0.1 和 net10.0,为两个平台生成 NuGet 资产。这是显式的多目标设定,并不承诺任意桌面包都能反向工作。
NetWasm.Libraries 提供移植的框架和扩展包。TUnit-NetWasm 通过通用 VSTest 桥接提供常规的 dotnet test 集成。
这与传统 .NET Wasm 相比如何?
对于一个打印 42 的空 UI Blazor WebAssembly 应用程序,启用 AOT、托管裁剪、IL 剥离、固定区域性和以大小为重点的本机链接:
| AOT 构建 | 未压缩 Wasm | 相对于 NetWasm |
|---|---|---|
| NetWasm,Compact GC | 47,895 B | 1× |
| Blazor WebAssembly,.NET 10 | 12,276,684 B | 256× |
| Blazor WebAssembly,.NET 11 RC1 | 15,289,241 B | 319× |
Blazor 总计包括其本机运行时和所需的 Webcil 程序集;NetWasm 包括其运行时、垃圾收集器和 WASI 组件。这是控制台组件与空 UI 应用程序的对比,并非等效框架比较,也不是对每种 .NET 工作负载的声明。
当前边界
NetWasm 处于 1.0 之前。其支持的配置文件目前可用,并已通过发布的资格认证,但源、包、目标配置文件和 ABI 契约可能会在次要版本之间发生变化。
常规的可移植输出是 wasm32 WASI Preview 2 组件。原始 wasm32、原始 wasm64 和 JavaScript 宿主集成是高级替代方案;wasm64 组件仍受上游工具链阻碍。
运行时反射、dynamic、运行时程序集加载和托管线程不在配置文件范围内。通用的 System.IO.File/Directory/FileStream、原始套接字和子进程 API 尚未实现。现有 NuGet 包仅当其可达代码和 API 依赖项符合支持的配置文件时才能工作。
通往 1.0 之路
NetWasm 1.0 意味着一个有用、有文档且稳定的更小平台。它并不意味着“桌面 .NET,但在 Wasm 中”。剩余的主要主题是:
- 更广泛的 WASI Preview 2 覆盖范围;
- 全球化模式和可选区域性数据;
- 表达式树完成;
- 更快的增量开发循环;
- 稳定的
netwasm1.0平台和运行时契约; - 生产诊断和更广泛的宿主资格认证。
NetWasm 0.10.0 新增:重建的垃圾收集器。对象不携带收集器头,数组是单一分配,NetWasmGC 通过推进指针进行分配,Compact 拥有为大小和内存而构建的新核心。分配密集型程序运行更快,使用的内存少得多,附带时区数据的应用程序启动时间仅为一小部分。Boehm 收集器已移除。发行说明
进行中:与现有 .NET Standard 2.1 库二进制文件和 NuGet 资产的更广泛兼容性。目标是在其可达代码符合 NetWasm 配置文件时,无需更改即可使用库,而不会将未使用的不受支持路径带入应用程序。
这不会把 NetWasm 变成桌面版 .NET。可访问的通用运行时反射、dynamic、运行时程序集加载、托管线程、不受支持的宿主 API 以及命名区域性全球化仍不在初始兼容范围内,并会在编译时失败。
包
编译器、SDK、运行时和宿主包
NetWasm.Compiler.Browser · NetWasm.Compiler.Tasks · NetWasm.Hosting · NetWasm.Hosting.Build · NetWasm.Ref · NetWasm.Runtime.Pack · NetWasm.Runtime.Wasm32 · NetWasm.Runtime.Wasm64 · NetWasm.Sdk · NetWasm.Templates · NetWasm.Testing.VSTest · NetWasm.Toolchain · NetWasm.Wit.Bindings.Tool
许可证
CoreLib、运行时库、模板和生成的支持代码采用 MIT 许可证,但须保留上游声明。编译器和开发者工具使用 NetWasm Community License 1.0。 Compact 和 NetWasmGC 收集器采用 Apache-2.0 许可证。
社区许可证对符合条件的个人、教育用途、开源工作、评估用途,以及员工少于 250 人且年收入低于 10,000,000 美元的组织免费。匹配的 GitHub Sponsors 层级授予有限的内部商业使用;OEM 和商业产品使用需要单独协议。生成的应用程序可以使用其作者选择的许可证。没有登录、激活、遥测或技术强制执行。
许可指南 · 第三方声明 · 商业许可:zionsatidev@gmail.com
带来一个工作负载
从 Console.WriteLine(42) 开始,然后尝试部署大小、沙箱化或可移植性至关重要的最小真实工作负载。如果缺少某些内容,请提交 issue。具体的工作负载是决定 1.0 支持边界的方式。
来源:Hacker News · github.com