最近 Matz 合并了我给 Spinel 的一个 PR,借这个机会介绍一下我在做的语言 Egg:https://github.com/femto/egg
Egg 编译成可移植的 C99,几个设计重点:
uses io, alloc,再由 main 把这些值往下传,看函数签名就知道它能碰什么。uses alloc, io
fn greet(io: Io, a: Allocator, name: str) void {
const msg = a.concat("hello, ", name)
defer a.free(msg)
io.print(msg)
}
fn main(io: Io, a: Allocator) void {
greet(io, a, "egg")
}
(GitHub 还不认识 Egg,代码块先借 Zig 的高亮。)
忘了写 defer a.free(msg),内存泄漏直接编译不过:
hello.egg:4:11: error[E135]: 'msg' still owns its buffer when 'greet' ends; free it (a.free(msg)), return it or pass it on
去掉 uses 里的 io,这个能力也用不了:
hello.egg:6:8: error[E130]: 'greet' uses io (Io.print), but its module does not declare it; add `uses io` at the top of the file
切片还要用,原来的缓冲区就不能改:
const path = line[4..]
a.push(&line, '!') // 可能会挪动底层字节
io.print(path)
slice.egg:7:12: error[E149]: 'line' cannot be changed here: the slice 'path' of it is used later
path 用完之后,同样的 push 就可以了:借用在最后一次使用时结束。
uses io
const Shape = union(enum) {
circle: i64
rect: Size
empty
}
const Size = struct { w: i64, h: i64 }
fn area(s: Shape) i64 {
switch (s) {
.circle => |r| { return 3 * r * r }
.rect => |sz| { return sz.w * sz.h }
.empty => { return 0 }
}
}
fn total(shapes: []Shape) i64 {
var sum = 0
for (shapes) |s| {
sum = sum + area(s)
}
return sum
}
fn main(io: Io) void {
const shapes = [3]Shape{ Shape{ .circle = 2 }, Shape{ .rect = Size{ .w = 3, .h = 4 } }, .empty }
io.print(total(shapes[0..])) // 24
}
Spinel 是 Matz 写的 Ruby AOT 编译器:把 Ruby 的一个子集编译成 C,再编成原生程序。
Egg 的编译器 eggc 大约 1.2 万行,全部用 Spinel 能编译的那部分 Ruby 写成。平时它跑在 CRuby 上,也可以用 Spinel 编成原生程序:
用这个子集写编译器的体验很好:写起来就是普通 Ruby,最后又能得到一个原生程序。
所有检查都开着(溢出、越界),在 i7-9700K 上运行,时间是 C 的多少倍:
| 程序 | Egg | Rust(开溢出检查) | Zig ReleaseSafe | CPython | Ruby 3.2 |
|---|---|---|---|---|---|
| fib(40) | 1.53x | 1.62x | 1.51x | 95x | 44x |
| sieve,5000 万 | 1.10x | 1.04x | 1.19x | 27x | 23x |
| collatz,300 万 | 1.07x | 1.37x | 1.34x | 55x | 26x |
| matmul,800×800 | 2.31x | 1.11x | 1.23x | 345x | 164x |
同样的循环,Egg 比 CPython 快 25 到 149 倍,比 Ruby 快 21 到 71 倍。matmul 落后 Rust 和 Zig,是因为 eggc 还没有把内层循环的越界检查优化掉。
为了在真实应用上检验 Egg,我在用 Egg 移植 Basecamp 的群聊应用 ONCE Campfire,代码在 https://github.com/femto/once-campfire-egg 。
它直接用 Rails 版生成的数据库,和 Basecamp 自己的 Rust 移植版 输出逐字节一致的 HTML,再用 Basecamp 官方的 测试工具 对比。每个响应都要通过内容校验,每条写入都会回查数据库。
GitHub Actions 的 ubuntu-latest,应用占 2 个核,每秒请求数,两边都开着缓存:
| 路由 | Rust | Egg |
|---|---|---|
| 房间页 | 12,710 | 41,833 |
| 侧栏 | 14,287 | 51,243 |
| 搜索 | 14,138 | 50,540 |
| 静态 CSS | 48,060 | 108,761 |
| 发消息 | 1,818 | 1,771 |
不开缓存时,Egg 每次都重新渲染并 gzip 整个页面,页面会比 Rust 慢很多;完整数据都在仓库的 docs/benchmark.md 里。
实时聊天(Action Cable over WebSocket):1000 个 WebSocket 客户端在同一个繁忙的房间里,每条消息都检查是否送达了每一个客户端。
| Rust | Egg | |
|---|---|---|
| 满负载下每秒完整送达的消息数 | 178 | 195-222 |
| 送达所有客户端的延迟,p50 | 37 ms | 51-56 ms |
| 没送达全部客户端的消息 | 0 | 0 |
| 内存峰值 | 134 MiB | 35-39 MiB |
两边基本持平,Egg 的内存约为四分之一。为了这一项,Egg 新加了事件循环 net.poller(),底层是 epoll、kqueue 和 WSAPoll。
Egg 还很年轻,语法还会变。Campfire 目前做了官方测试的那些路由,加上完整的实时聊天,还不是整个应用 (正在做)。
欢迎试用,也欢迎提意见:
git clone https://github.com/femto/egg
cd egg
bin/eggc run examples/complex/...
只需要 Ruby 3.0 以上和一个 C 编译器。