Skip to content
MeoKit Blog
Go back

一个为 iOS 优化的 Linux x86 容器

Edit page

English Version (英文版)

Podish 图标

TL;DRPodish 是一个面向 iOS / Apple Silicon 专门优化的高性能 Linux x86 用户态容器。它用 C++ 写了一个 i686 解释器核心,用 C# 写了 Linux 兼容层,在 iPhone 17 (A19) 上跑出 CoreMark ~3400,比 iSH 快一倍。

Web Demo: https://podish.meokit.com

GitHub: https://github.com/meokit/podish


项目速览

Podish 的目标:在禁止 JIT 的 iOS 上,尽可能高效地运行 x86 Linux 程序。类似 iSH,但代码完全从头重写,并在多个维度上性能翻倍。

它能做什么

类别代表程序状态
Shell / 基础工具链busybox / bash / vim稳定可用
脚本运行时python3 / LuaJIT已验证,可稳定跑 benchmark
构建工具链gcc / make已验证,make compile 跑通
网络 / 开发工具git / OpenSSH手工跑通过,git clone 可用
重型运行时Node.js / Gemini CLI能启动,偶有 crash(V8 JIT 相关)

运行截图

iPhone 上的终端环境

iPhone 上的 Fastfetch

iPhone 上的 Vim

iPhone 上的 Yazi

浏览器演示(无需安装):

浏览器演示

iPhone 跑 CoreMark

iPhone 上的 CoreMark

跑 Gemini CLI

iPhone 上的 Gemini CLI

macOS 上的 Gemini CLI

性能速览

WorkloadPodish (A19)iSH (A19)倍数
CoreMark 1.034471692~2.0×
python primes.py78.3 s684.4 s~8.7×
luajit -joff primes14.5 s27.5 s~1.9×
sh -lc true warm start20 ms30 ms~1.5×

完整基准测试与测试环境说明见文末「性能数据与优化历程」。


以下章节进入技术实现细节。 如果你只想了解项目现状,上面的速览已经够用了;下面是枯燥的技术细节。


动机与背景

iOS 上有一个限制:不允许 JIT。具体来说,系统禁止 WX(Write-XOR-Execute)内存页映射,未经签名的代码无法执行。这意味着你无法在 App Store 下载带 JIT 加速的 UTM,只能使用极其缓慢的 UTM SE;也无法运行 LuaJIT 的 JIT 模式。

在这种背景下,我想知道解释器最快能有多快? 能否在不做 JIT 的前提下,靠极致的解释器设计和硬件感知优化,逼近 JIT 的性能?

解释器的主要灵感来自于 LuaJIT Remake。我从它接触到了 Clang 的 preserve_nonepreserve_all[[musttail]] 等 ABI 特性——这些工具有可能让编译器生成堪比手写汇编的解释器热路径。

项目从 hello world 开始(第一周),到跑通 CoreMark(约一个月),再到如今能稳定运行 Busybox、Bash、Python、LuaJIT、GCC,甚至启动 Node.js 和 Gemini CLI。


整体架构

Podish 并非单层设计,而是采用了明确的分层架构:

flowchart TB
    subgraph Guest["Guest x86 Linux Binary"]
        A["i686 ELF / libc / app"]
    end

    subgraph Core["Emulation Core (C++)"]
        B["Predecode / Block Builder"]
        C["Interpreter Dispatch + Semantics"]
        D["MMU / MicroTLB / SoftTLB"]
    end

    subgraph Runtime["Linux Runtime (C#)"]
        E["Syscall Layer"]
        F["VFS / Process / Signal / Network"]
        G["Netstack (smoltcp)"]
    end

    subgraph Orchestration["Container Layer (C#)"]
        H["Podish.Core / OCI"]
        I["Podish.Cli"]
    end

    subgraph Host["Host Platform"]
        J["iOS / macOS / Browser"]
        K["SwiftUI / SDL"]
    end

    A --> B
    B --> C
    C --> D
    D --> E
    E --> F
    F --> G
    F --> H
    H --> I
    I --> J
    J --> K
%%endmermaid

仓库里有:

为什么我用C#写系统调用层

选 C# 的原因:我需要快速实现约 200 个 Linux syscall 语义、VFS 层、网络栈和容器生命周期管理。C# 的跨平台 IO、字符串处理、异步模型和丰富标准库让我能在一个月内跑出一个shell。如果全程用 C++,我将不得不直接直接对接大量不同系统的 API。

控制跨语言开销:为了避免 P/Invoke 成为瓶颈,并简化生命周期管理,我这样做设计:

实际 profile 显示,在计算密集型负载(如 CoreMark)中,跨语言边界开销占比不到 1%;在 IO 密集型负载(如 git clone)中,瓶颈在网络和 VFS,也不在 P/Invoke。


解释器设计

解释器的核心目标是:在不使用 JIT 的前提下,尽可能降低开销,下面列出几个关键设计。

1. 预解码 + Tail-call Dispatch

传统解释器通常有一个中央 dispatch 循环(while (1) { decode; dispatch; execute; }),每次执行完一条指令都要回到这个循环。 我选择尽可能简化分发,用尾递归把指令串起来。

flowchart LR
    A["x86 Binary"]
    B["BasicBlock"]
    C["Handler: mov"]
    D["Handler: add"]
    E["Handler: jmp"]
    Ret["Return to X86_Run"]
    A -->|"Predecode"| B
    B -->|"Entry"| C
    C -->|"musttail call"| D
    D -->|"musttail call"| E
    E -->|"Block end"| Ret
%%endmermaid

具体做法:

  1. 预解码:先将变长 x86 指令解码成固定长度的中间表示(DecodedOp,32 字节),按基本块顺序存放。
  2. Tail-call 链:每条 IR 自带 handler 函数指针。执行完当前指令后,不回到统一 dispatcher,而是通过 [[musttail]] 直接尾调用到下一条指令的 handler。
  3. 缓存:解码结果被缓存在 BasicBlock 里,避免重复 decode。

感谢 Justine Tunney 的 blink 项目,我靠它了解到了 Intel 的 XED,并最终得到了表驱动的 i686 解释器。

DecodedOp 固定 32 字节,对齐到 16 字节边界:

FieldTypeOffsetSize说明
handlerHandlerFunc* (函数指针)08执行该指令语义的入口
next_eipuint32_t84下一条指令的 PC
lenuint8_t121指令字节长度
modrmuint8_t131ModRM 字节
prefixesPrefixes(含位域的 union)141前缀信息(lock/rep/segment…)
metaMeta(含位域的 union)151元信息(has_mem / has_imm / ext_kind…)
ext.dataDecodedMemData1616内存操作数描述(imm / ea_desc / disp)
ext.link.next_blockBasicBlock*248block linking 后缓存的后继块指针
ext.controlDecodedControlFlowData1616控制流目标(target_eip / cached_target)

如果拿一条简单内存 ALU 指令举例:

Guest:
  add eax, [ebx+4]

Predecoded IR:
  entry      = op_add_r32_rm32
  next_pc    = 0x...
  operands   = { dst=eax, src=mem(base=ebx, disp=4, size=4) }
  flags_mode = arithmetic
  control    = fallthrough

2. Parity-only Lazy Flags

x86 的 EFLAGS 不好处理。QEMU 用了一套完善的 lazy flags 机制,但我尝试过之后发现,在我的场景里,全 lazy 反而更慢——因为需要频繁把 CC_SRCCC_DSTCC_OP 写入内存,而解释器已经是 memory-bound。

我最后选择只 lazy PF(Parity Flag)

flowchart TB
    A["ALU Instruction"] --> B{"Writes Flags?"}
    B -->|Yes| C["Update CF/ZF/SF/OF/AF/DF in flags_cache[31:0]"]
    C --> D["SetParityState: store result byte to flags_cache[47:40]"]
    B -->|No| E["Skip flags update"]

    F["jcc / cmov / setcc"] --> G{"Cond depends on PF?"}
    G -->|No| H["Read flags_cache[31:0] directly"]
    G -->|Yes| I["EvaluatePF: compute parity from flags_cache[47:40]"]
%%endmermaid

解释器执行期间传递的 flags_cache 是一个 uint64_t

CF/ZF/SF/OF 几乎每个 ALU 指令都要写,而且 jcc/cmov/setcc 几乎每条都要读,做 lazy 的存取开销并不划算。但 PF 比普通 flags 更昂贵,且使用到 PF 的条件跳转仅有 JP/JNP(cond 10/11),比例很低,值得 lazy。

顺带一提,我试了好几种 PF 计算方法,ARM Neon的内置指令,直接位运算,还有查表。结论是查表最快,但是总归有访存延迟。Neon内置指令可能涉及跨SIMD寄存器的数据移动,没有查表快。

静态层:在基本块内做 flags 的 def-use 分析。如果某条指令写 flags,但后续无读取,就直接换成 no-flags Handler 变体。这种变体在 AluAdd<T, false>AluSub<T, false> 等模板参数中体现,编译器会把整个 flags 更新路径完全消除。

动态层:每条指令执行时,flags_cache 作为寄存器参数在 tail-call 链中传递,永不写回内存。只有 PF 是 lazy 的:

CommitCommitFlagsCache 只在 handler chain 退出边界调用一次(ExitOnCurrentEIPExitOnNextEIP、restart/retry、resolver miss),把寄存器中的 flags_cache 写回 state->ctx.flags_state。成功 chaining 的情况下绝不 commit——因为下一条指令的 handler 还会以寄存器参数的形式继续传递同一个 flags_cacheX86_Run()X86_Step() 在 handler 返回后也不再 commit,避免用过时的值覆盖已更新的状态。

对于 CheckCondition 的 LUT 路径:绝大多数条件跳转(cond 0-9, 12-15)不依赖 PF,所以它们走 GetFlags32Raw(flags_cache) 直接读取低 32 位,用 LUT 查表决定跳转方向;只有 cond 10/11(JP/JNP)会单独分支到 EvaluatePF()。这样设计让 PF lazy 的额外成本几乎为零。

3. 访存是瓶颈:MicroTLB 与 SoftTLB

项目早起的构想起于 LuaJIT Remake,第一次 profile 发现做CPU模拟器独有的问题是地址转换

flowchart LR
    nodeA["Guest Address"]
    nodeB{"MicroTLB Hit?"}
    nodeC["host_ptr = guest_addr + addend"]
    nodeD{"SoftTLB Hit?"}
    nodeE["Refill MicroTLB"]
    nodeF["Page Table Walk"]
    nodeG["Permission Check"]

    nodeA --> nodeB
    nodeB -->|Yes| nodeC
    nodeB -->|No| nodeD
    nodeD -->|Yes| nodeE
    nodeD -->|No| nodeF
    nodeF --> nodeG
    nodeG --> nodeE
    nodeE --> nodeC
%%endmermaid

SoftTLB 是一个三路直接映射 TLB,三张固定大小的表:

FieldTypeOffsetSize说明
read_tlbstd::array<TlbEntry, 256>04096读权限映射表
write_tlbstd::array<TlbEntry, 256>40964096写权限映射表
exec_tlbstd::array<TlbEntry, 256>81924096执行权限映射表

TlbEntry 对齐到 16 字节,固定 16 字节

FieldTypeOffsetSize说明
taguint32_t04guest page 的 tag(高 20 位)
permProperty (enum)44权限位(Read / Write / Exec / Dirty …)
addendstd::uintptr_t88host_ptr = guest_addr + addend

通过以下位运算直接索引:

idx = (guest_addr >> PAGE_SHIFT) & 255
tag = guest_addr & ~PAGE_MASK
host_ptr = guest_addr + addend

SoftTLB 的职责是将已经查过的 guest page 映射压缩为一个 tag + addend 的快速表项。命中时,只要检查 tag,再做一次加法就能拿到宿主地址;miss 时,才回到慢路径检查权限、补表项、处理异常。

早期出了一个很低级的 TLB refill 错误,导致性能低下,CoreMark 只有 400。往 dispatch 方向查了一会没发现,修掉 TLB Bug后提升到 800

这让我意识到地址转换路径也挺重要。

于是我往这个方向做了进一步设计:

MicroTLB 常驻寄存器,对齐到 16 字节,固定 16 字节

FieldTypeOffsetSize说明
tag_ruint32_t04读权限 tag,默认 0xFFFFFFFF(未命中态)
tag_wuint32_t44写权限 tag,默认 0xFFFFFFFF(未命中态)
addendstd::uintptr_t88host_ptr = guest_addr + addend

read_tag + write_tag 合并在同一个寄存器,所以这个结构占两个 Host 寄存器。

它的 hit / miss 路径可以用下图表示:

guest address
  -> check read_tag / write_tag
  -> hit: host_ptr = guest_addr + host_guest_addend
  -> miss: full translation + permission check + refill MicroTLB

refill 的时候要检查权限,如果没有 read 权限,清除 read_tag,反之亦然。

这个设计可能有点奇怪,如果读写反复不在同一个页面上,MicroTLB 会 Ping-pong,命中率为0。

但大多数时候有效,我测下来命中率高于50%,实际上只要有一定的命中,能降低从内存的 SoftTLB 读的频率,就有性能提升。

4. Block Linking 与 Superopcode

在这个解释器里,BasicBlock 是内存里的定长头部 + 变长指令流对象。它的头部大致长这样:

BasicBlock 对齐到 16 字节,头部固定 48 字节,后跟变长的 DecodedOp 数组:

FieldTypeOffsetSize说明
chain.start_eipuint64_t (位域: 32 bits)04(嵌入 BasicBlockChainPrefix块起始 guest PC
chain.inst_countuint64_t (位域: 8 bits)41块内指令数
chain.validuint64_t (位域: 1 bit)51 bit块是否有效(用于失效标记)
entryHandlerFunc*88块首条指令的语义入口,解释器直接跳转到这里
end_eipuint32_t164块结束地址(最后一条指令的 next_eip)
slot_countuint32_t204总 slot 数(含末尾 sentinel)
sentinel_slot_indexuint32_t244sentinel 在 slots 中的索引
branch_target_eipuint32_t284分支目标地址(条件跳转/直接跳转时有效)
fallthrough_eipuint32_t324fallthrough 地址
terminal_kind_rawuint8_t361终止类型(None / DirectJmpRel / DirectJccRel / Other)
block_padding0uint8_t371填充
block_padding1uint16_t382填充
exec_countuint64_t408块被执行的次数(用于 profile-guided superopcode)
slots[]DecodedOp[]48变长(每个 32 字节)预解码后的指令流

比较关键的字段:

DecodedOp 自己也不是“只有 handler 指针”的极简结构。它除了 handlernext_eip 之外,还会在扩展区里保存内存操作数信息、控制流目标,或者 block linking 之后缓存下来的 next_block 指针。

所以 BasicBlock 真正做的事情,其实是将三件事整合在一起:

  1. 缓存预解码结果,避免重复 decode
  2. 将一串 DecodedOp 紧凑地排列成可 tail-call 的执行流
  3. 给 block linking / superopcode / profile 这些后续优化提供稳定载体

后续在做 block linking 时,直接在现有 BasicBlock 上进行拼接和复用。

flowchart LR
    A["Block A"] -->|"fallthrough"| B["Block B"]
    A -->|"branch taken"| C["Block C"]

    D["Superopcode"] -->|"fused"| E["pop ebx ; pop esi"]
    D -->|"fused"| F["test eax, eax ; je"]
    D -->|"fused"| G["mov eax, [esp] ; sub reg, eax"]
%%endmermaid

Block Linking:如果一个基本块足够短、指令不跨页,而且控制流关系简单,就将后继块中的指令直接拼接到当前块尾部,减少块间跳转时的间接访存。

Superopcode:围绕高频 anchor instruction 选取种子,再看它与前后指令的 def-use 关系,仅融合存在 RAW 依赖的组合。这一策略最终生成了约 256 个 superopcode。

代表性 superopcode:

候选发现流程很简单,可以看 Podish.PerfTool 工程:

block trace
  -> 热点统计
  -> anchor 选择
  -> def-use / RAW 过滤
  -> 代码生成
  -> 回归验证与收益复核

内存管理机制: Page Cache管理,在支持的平台直接映射文件

Podish 运行时的内存模型被拆成两层:AddressSpace / AnonVma 负责保存 resident page 的语义内容,ProcessPageManager 只负责记录哪些 guest page 目前被安装进原生 MMU;中间的 HostPageManager 则按“每个 live host 指针一条元数据”的方式维护状态。因此 guest→host 的原生映射可以随时拆掉再重建,而不会丢失底层页面本身的内容和所有权。

在原生平台上,file-backed page 会尽量走 直接映射 Host File Page 路径。MappingBackedInode.AcquireMappingPage(...) 通过操作系统API获取一个映射的虚拟内存窗口,随后 EnsureExternalMapping(...) 直接把这个 host pointer 安装进 guest 的 SoftMMU,热路径只做地址转换(guest_addr + addend)和权限检查。相关类在 MappedFilePageCache:如果平台支持文件映射,它会选择 WindowedMappedFilePageBackend,按宿主的页面大小建立映射窗口、复用活跃窗口,并用 lease token 管理窗口生命周期,确保映射移动或引用释放时能安全释放资源。

到了 Browser/Wasm,这条直接映射路径会被关闭(平台也不支持)。HostMemoryMapGeometry.CreateCurrent() 会把 mapped-file backend 标成 unsupported,于是 FilePageBackendSelector 自动回退到 BufferedPageBackend。这时运行时不再依赖操作系统提供的文件映射窗口,而是把文件内容拷贝到自己管理的内部 PageCache 页面里,后续 fault / install 再把这些页面当作普通 resident host page 来处理。这样做比原生平台多一次拷贝,也无法做 mapped-file flush 的快路径,不过 fault / COW / reclaim 这些后续管线几乎不用改,同一套架构在 Wasm 平台上能够轻松跑起来。


SMC(自修改代码)处理

SMC(Self-Modifying Code)是现代 JIT 引擎(V8、LuaJIT、.NET JIT)所必需的:它们先写机器码到内存,再跳转过去执行。模拟器必须正确处理。

这里为了效率复用了 MMU 的权限系机制,将检测成本压进权限位:

flowchart TB
    A["mmap/mprotect<br/>PROT_EXEC \| PROT_WRITE"] --> B["Register External Alias"]
    B --> C{"Live BasicBlock<br/>on this page?"}
    C -->|Yes| D["Set ForceWriteSlow<br/>on guest page entries"]
    C -->|No| E["Normal page"]

    F["Guest Write Instruction"] --> G{"ForceWriteSlow?"}
    G -->|Yes| H["invalidate_code_cache_page"]
    H --> I["Mark blocks invalid"]
    G -->|No| J["Fast write path"]
%%endmermaid

当某个 host page 同时被映射为可执行可写时,MMU 会把它标记为 External Alias。如果该页上已缓存了 BasicBlock,MMU 会给对应 guest page 表项打上 ForceWriteSlow。后续任何对该页的写操作,在 TLB refill 时会命中 ForceWriteSlow,走慢路径调用 invalidate_code_cache_page,将该页关联的所有 BasicBlock 标记为失效。

执行期竞争:仅靠”写的时候失效 block cache”还不够。如果当前 EIP 正好落在被写的页上,解释器 tail-call 跳转到的下一条指令可能已经被改写。为此实现了 ShouldInterceptExecWriteForSmc:一旦检测到当前 EIP 所在页被修改,会立即 yield 并切换到单指令安全模式max_insts = 1),确保“写操作”与“跳转到新代码”之间有一个明确的指令边界,避免竞争。

多 Engine TLB 一致性clone(CLONE_VM) 创建的新线程共享同一个 MmuCore,但每个 Engine 有自己的 SoftTLB。为此实现了 RuntimeTlbShootdownRing(1024 槽 ring buffer),页表变更方把被 flush 的 guest page 写入 ring,其他 Engine 在下次进入 X86_Run 时同步消费。

这套机制足够让 LuaJIT 稳定运行了,Node.js / V8 能启动。但偶发 crash,可能与指令集实现或 syscall 不完整有关。


对 Copy-and-Patch JIT 的尝试

Copy-and-patch 的核心思路:先将 Opcode handler 预编译成 binary stencil,运行时仅做”拷贝模板 + patch 常量/地址/跳转槽位”。理论上可以避开完整后端、寄存器分配和传统机器码生成器的复杂度(相关论文:Copy-and-Patch Compilation)。

由于我已经用 preserve_none + [[musttail]] 将解释器热路径压得很低,我尝试将 handler 变成 stencil 再 patch 参数,连接成机器码。预期 200%+ 的性能提升,实际上比解释器还略慢

原因:direct-threaded interpreter 已经把 dispatch 压得很低了,瓶颈变成了访存、地址转换、状态维护和 I-Cache 压力。stencil 虽然省掉了一些字节码读取,但 patch 后代码膨胀,I-Cache 压力反而更大。

这个失败很有价值:我认为这说明劣质 JIT 还不如好的解释器,于是我继续 profile 访存路径。


多线程模型

flowchart TB
    subgraph Scheduler["KernelScheduler (1 Host Thread)"]
        S["Scheduler Loop"]
    end

    subgraph TaskA["FiberTask A"]
        A1["Engine A"]
        A2["SoftTLB A"]
        A3["block_lookup_cache A"]
    end

    subgraph TaskB["FiberTask B"]
        B1["Engine B"]
        B2["SoftTLB B"]
        B3["block_lookup_cache B"]
    end

    subgraph Shared["Shared (Atomic RC)"]
        M["MmuCore"]
        R["RuntimeTlbShootdownRing"]
    end

    S -->|"yield / switch"| A1
    S -->|"yield / switch"| B1
    A1 --> M
    B1 --> M
    M --> R
%%endmermaid

我实现了 clone/fork/vfork 和基本的 pthread 语义,但实际上:

上限:guest 的多个线程在宿主层面是串行调度的,不能利用多核并行。对于 shell、Python、小型编译任务来说还好;大项目编译,并行计算会受到明显限制。


Linux 兼容层与可用性

Linux 兼容层实现了:

下面这个表列了一些我测试过的程序。

类别代表程序 / 证据当前状态备注
shell/bin/sh -lc true已验证目前正文主要覆盖 shell 启动和短命令
coreutils / archivegrep -R / tar cf / tar xf已验证
scriptingpython (primes.py)已验证workload 当前只有少量样本,容易受手机上温度影响
JIT-heavyLuaJIT (primes_jit.lua)已验证
build-oriented mixed workloadmake compile on CoreMark tree已验证
network / tooling compatgit clone已验证兼容

图形与音频的实验性支持

代码库里已有两条实验性的多媒体管线,但还未成熟到进入日常测试:

Wayland Foot 终端演示

这两个功能目前只在 macOS 上支持,iOS / Wasm 还没做适配。


性能数据与优化历程记录

从 600 到 3000

阶段关键变化CoreMark备注
初始版本baseline interpreter~400TLB bug
修 TLB bugfix address translation path~800修复了 TLB Bug
热路径优化PC / block budget / template specialization~1500不再每条指令都更新 eip 等内存变量
访存整理data layout / paired loads~2000优化重点从 dispatch 转向访存组织
lazy flagsstatic prune + PF lazy~2200实现目前的 lazy flags 方案
block linkingappend simple successor block~2500降低块边界开销
superopcodeprofile-guided fused ops~3000约 256 个 superopcode

目前的基准测试数据

测试环境

启动速度与计算密集任务

WorkloadPodish(A19)Podish(M3 Max)iSH (A19)Podman i386 (QEMU JIT)QEMU TCINative host (M3 Max)测试说明
sh -lc true warm start20 ms20 ms30 ms30 ms30 ms10 ms启动成本;Podish 用 Podish.Cli run;QEMU 显式 qemu-i386 -L <rootfs>
CoreMark 1.03447296716921145632538087
python primes.py78.3 s89.4 s684.4 s40.9 s787.3 s1.8 s测试用例来自 kostya/benchmarks
luajit primes3.1 s4.0 s46.6 s1.7 s39.3 s0.2 s
luajit -joff14.5 s17.3 s27.5 s7.1 s152.7 s0.7 s

文件 IO 和混合任务

WorkloadPodish(A19)Podish(M3 Max)iSH (A19)Podman i386 (QEMU JIT)Native host (M3 Max, arm64)备注
grep -R on CoreMark tree50 ms50 ms70 ms70 ms10 ms排除 .git, GNU grep
tar cf CoreMark tree40 ms30 ms40 ms97 ms10 msGNU tar
tar xf CoreMark tree250 ms250 ms170 ms161 ms30 msGNU tar
make compile on CoreMark tree9470 ms10460 ms11430 ms7330 ms210 msmake compile;native 为单进程 make -j1 compile CC=clang
git clone CoreMark3230 ms3300 ms5190 ms2660 ms2980 ms说明能跑,受网络影响时间没什么意义

以上两张表,除有明确说明的以外,均为 5 次实验的中位数。QEMU TCI 模式在文件 IO 测试中暂缺(太慢,意义不大)。

iSH的奇怪现象

iSH 上 luajit -joff 比开启 JIT 还快

在 iSH 上,luajit -joffprimes_jit.lua 耗时 27530 ms,而开启 JIT 的要 46650 ms。推测原因:

我对 iSH 内部实现没有深入研究过,以上只是猜测。如果你熟悉 iSH 的源码,非常欢迎在评论区分享。

与 Wasm3 的对比

作为另一个纯解释器的参考,我在同一台 MacBook 上用 wasm3 coremark.wasm 跑了 5 次:

也就是说,Podish 的纯计算性能已经可以比肩 Wasm3 了。

如果你对项目感兴趣,欢迎访问 GitHub 或在浏览器里直接体验 Podish Web Demo


Edit page
Share this post on:

Next Post
An iOS-optimized Linux x86 container