• MerryDeer144

    最近 Matz 合并了我给 Spinel 的一个 PR,顺便介绍一下我在做的语言 Egg: https://github.com/femto/egg

    一句话:编译成可移植 C99,没有 GC,但释放后使用、重复释放、内存泄漏都是编译错误,而且不用写生命周期标注。

    几个设计重点

    单一所有者。 每个缓冲区只有一个所有者,借用在最后一次使用时结束,不需要写 'a。

    没有未定义行为。 整数溢出、除以零、数组越界都会 panic,不会悄悄回绕或踩坏内存。

    能力写在签名里。 语言核心不能做 IO,也不能分配内存。文件要先声明 uses io, alloc,再由 main 把这些值往下传,看函数签名就知道它能碰什么。

    线程之间不共享数据。 有所有者的值通过 channel 在线程之间转移。

    长什么样

    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")
    }

    忘了写 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

    把 io.print(path) 挪到 push 前面,就能编过。

    另外还有带数据的联合类型 union(enum) + switch 解构、defer、泛型容器等,例子在仓库的 examples/ 里。

    编译器是用 Ruby 写的

    编译器 eggc 大约 1.2 万行,全部用 Spinel https://github.com/matz/spinel(Matz 写的 Ruby AOT 编译器,把 Ruby 子集编译成 C)能编译的那部分 Ruby 写成。平时跑在 CRuby 上,也能用 Spinel 编成原生程序:早期的 v0 这样编过,启动时间从约 350 ms 降到约 5 ms,也不再需要装 Ruby。现在的版本还卡在几个 Spinel 的 issue 上(#11078 https://github.com/matz/spinel/issues/11078、#11079 https://github.com/matz/spinel/issues/11079、#11080 https://github.com/matz/spinel/issues/11080 )。

    性能

    小程序:时间是 C 的多少倍(i7-9700K,溢出和越界检查全开)

    程序
    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,800800
    2.31x
    1.11x
    1.23x
    345x
    164x

    matmul 落后,是因为 eggc 还没有把内层循环的越界检查优化掉。

    真实应用:用 Egg 移植 ONCE Campfire

    为了在真实应用上检验,我在用 Egg 移植 Basecamp 的群聊应用 ONCE Campfire https://github.com/basecamp/once-campfire: https://github.com/femto/once-campfire-egg

    它直接用 Rails 版生成的数据库,和 Basecamp 官方的 Rust 移植版 https://github.com/basecamp/once-campfire-rust 输出逐字节一致的 HTML,用官方的 测试工具 https://github.com/basecamp/once-campfire-verification 对比:每个响应都做内容校验,每条写入都回查数据库。

    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 http://benchmark.md。

    实时聊天(Action Cable over WebSocket),1000 个客户端在同一个繁忙房间,每条消息都检查是否送达每个客户端:

    Rust
    Egg

    满负载下每秒完整送达的消息数
    178
    195-222

    送达所有客户端的延迟 p50
    37 ms
    51-56 ms

    没送达全部客户端的消息
    0
    0

    内存峰值
    134 MiB
    35-39 MiB

    基本持平,内存约四分之一。为此 Egg 新加了事件循环 net.poller(),底层是 epoll / kqueue / WSAPoll。

    现状

    还很年轻,语法还会变。Campfire 目前做了官方测试覆盖的路由和完整的实时聊天,其余部分还在做。

    只需要 Ruby 3.0+ 和一个 C 编译器:

    git clone https://github.com/femto/egg
    cd egg
    bin/eggc run examples/complex/dijkstra.egg

    欢迎 star。

    主楼
  • HiddenComet

    这种单一所有者模型在 Rust 的借用检查器实现上应该有借鉴意义。

    #1
  • BerryBlast

    这个设计看起来很巧妙,但跨平台 C99 兼容性可能是个问题,毕竟不同平台的内存对齐和结构体布局规则差异挺大的。

    #2
  • Hiker

    这个设计听起来很棒,但实际项目中如何处理复杂的内存所有权转移?

    #3
  • ImCageSet

    我线上就这样搞过,单一所有者的内存管理确实能避免很多野指针问题,不过编译时检查会不会影响开发效率?

    #4

发表回复

登录后回复