Skip to content

perf: post-optimization benchmark/profile findings and next priorities (Map, arrays, property writes) #16

Description

@Eric-Song-Nop

This follow-up investigation identifies the next performance priorities after the optimizations tracked in #6, #7, #8, #9 and #10, building on the profiling work in #4.

本轮建议优先处理 Map 查找、真实数组的键处理、已有属性槽更新。证据主要指向多余的扫描、复制、字符串转换和布局发布;这些应先于更大范围的 VM/调用协议重构逐项验证。

被测版本与方法 / Measurement scope

  • 被测快照:Eric-Song-Nop/quickjs-oxide@cdc35ec,包含静态属性 Atom、非数组提前返回、原始数值转换、引用清理和内建批量初始化等已有优化。发 issue 时该快照比上游 main 56faa471a5f923ef0fe9540e0b12f64b1e7ec6c5 多 38 个提交;本文数字属于这个优化后的快照,不是未经这些改动的上游 main。
  • 机器:eric-83am,AMD Ryzen 7 7840HS,Linux 6.18.44-1-lts;Rust 1.94.1 / LLVM 21.1.8。主要 timing/stat/CPU 采样固定 CPU 2,SMT sibling 为 CPU 3;未锁频或隔离整台机器。构建与正式测量分开串行执行。
  • 普通 timing binary:release、debug=1、strip=none,profiling feature 关闭。另建 profiling binary,以及强制 frame pointer 的采样 binary;CPU 比例来自采样构建,墙钟 benchmark 来自普通构建。
  • 参考:QuickJS 2026-06-04,GCC 16.2.1、默认 make 的 -O2。测量后独立重建的 .text/.rodata/.data/.data.rel.ro 与被测参考 ELF 逐字节相同。V8-v7 外部源码固定在 ahaoboy/js-engine-benchmark@2034d98fc8c5f8044e186267593f5d5ea5232caf
  • 覆盖 50 项固定 microbench、全部 8 项固定 V8 子套件、8 个 Map 规模配置、10 个对象宽度/构建对照配置;另保留原始 V8 单轮筛查和默认五项 microbench 三轮。
  • microbench 抽取原始函数体及必要 helper;单独 pilot 后固定相同 N,两引擎每轮交错运行,各三轮。四项首次过短的转换负载另以 N=500000、五轮重测并替换汇总。固定 V8 保留 base/suite 原文和原断言,执行一次 Setup/run/TearDown,各三轮。原始自校准 harness 另行运行。
  • 固定工作负载全部正式样本成功,完整可观测 stdout 在两引擎间一致;这只是 workload smoke check,不替代 Test262。

Benchmark:当前还差多少

以下均为 整进程墙钟中位数,单位 ms,包含启动、编译、输出和收尾;比例为 Oxide/QuickJS,越大表示 Oxide 越慢。它不是上游 ns/op、不是纯执行阶段时间,也不是预计能取得的加速倍数。* 表示 QuickJS 进程短于 20 ms,启动与调度影响不可忽略。

Case 固定 N Oxide ms QuickJS ms 慢倍数
empty_loop 6084891 941.337 28.004 33.6×
prop_read 846083 962.523 23.904 40.3×
prop_write 314880 988.454 10.025 98.6× *
array_read 258984 974.331 15.199 64.1× *
array_write 62914 1018.251 6.412 158.8× *
array_slice 872 934.589 14.383 65.0× *
typed_array_read 136651 964.113 13.486 71.5× *
arguments_read 19199 1005.923 10.039 100.2× *
func_call 400000 814.395 29.778 27.3×
bigint64_arith 1600 629.182 47.314 13.3×
map_set_string 10 1022.977 4.225 242.1× *
map_set_int 22 946.043 4.220 224.2× *
regexp_ascii 113 975.183 16.314 59.8× *
string_build_large1 80 460.992 67.594 6.8×

CPU profile:时间花在哪里

86 份采用的 profile、70,835 个样本、0 个 lost-event records。使用 cycles:u、499 Hz、frame pointers,最终以匹配 ELF/libc 符号和 perf script --no-inline --max-stack 512 解码。下表 12 项关键 microbench 均为三次 profile 的 period 加权结果。包含子调用的比例互相重叠,不能相加。

Case 样本数 热点 包含子调用 %
empty_loop 1552 read_frame_binding 20.50%
empty_loop 1552 execute_hot_instruction 52.19%
prop_write 1538 prepare_set_property_with_receiver_in_realm 66.77%
array_write 1668 intern_property_key 47.87%
array_slice 1614 intern_property_key 66.87%
typed_array_read 1526 typed_array_canonical_numeric_index 40.40%
arguments_read 1562 create_arguments 74.12%
arguments_read 1562 replace_layout 51.81%
map_set_int 1495 find_map_record 94.79%
map_set_string 1840 find_map_record 96.83%
regexp_ascii 1523 replace_layout 26.45%
func_call 1282 snapshot_function_bytecode 4.04%

普通构建的硬件计数也表明存在大量额外工作:同一固定工作量下,Oxide 的 retired instructions 分别约为 QuickJS 的 20.6×(空循环)、102.8×(属性写入)、261.3×(数组写入)、414.5×(字符串键 Map)。数组写入 IPC 为 3.07 / 2.92,不能把其差距主要归结为 IPC 不足。

两个规模实验:避免只盯单个固定尺寸

Map 每个配置累计 32768 次 set + 32768 次 has,仅改变每个 Map 的容量及 Map 数量。三轮中位数,单位 ms;校验和均为 32768。

容量 整数键 Oxide / QuickJS 字符串键 Oxide / QuickJS
32 115.64 / 5.39 188.15 / 7.21
128 243.33 / 5.87 433.04 / 7.40
512 795.05 / 5.42 1612.88 / 6.72
2048 2825.61 / 4.82 5083.62 / 7.56

同样写已有属性 p0 十万次,改变对象的总属性数。另做相同对象创建和输出、但没有写循环的构建对照。以下仍是直接测得的全进程时间,没有扣除启动时间后冒充独立阶段测量。

属性数 含写循环 Oxide / QuickJS ms 仅构建 Oxide / QuickJS ms
8 96.68 / 3.34 4.21 / 1.67
32 112.43 / 3.35 4.42 / 1.73
128 177.33 / 3.35 5.42 / 1.75
512 411.22 / 3.25 6.04 / 1.77
2048 1352.78 / 2.88 11.47 / 2.23

建议拆开的九个优化点 / Proposed follow-ups

  1. P0 · Map 索引与快照find_map_record):每次先收集并复制全部存活记录,再逐项 root 和 SameValueZero 比较。先删除整表临时 Vec,再引入指向保序记录的哈希索引;只去掉 Vec 仍不能解决线性查找。保留 NaN/±0、字符串/BigInt 相等性、对象身份、删除后重加顺序、活迭代器与 GC 语义。

  2. P0 · Array 数字索引与固定键array_own_key):之前的提前返回解决的是非数组。真数组仍先 intern length 再识别 index,slice 等内建也存在整数→字符串→intern。让立即整数 Atom 先返回,并复用运行时固定键;保留洞、稀疏数组、原型 setter、只读 length 和索引上界。

  3. P0 · 已有槽更新延迟复制store_property_slot):在确认 existing slot 的 flags 未变之前,就复制 entries 和 slots。将同槽同 flags 的替换放在复制之前,只有布局变化才复制;进一步考虑 receiver==object 的 own writable data 路径。保留 Proxy/accessor、异 receiver、VarRef/AutoInit、冻结属性及所有权错误契约。宽度实验与该机制一致,但尚未通过优化后的 A/B 证明它是唯一原因。

  4. P1 · TypedArray canonical-index 快路径typed_array_canonical_numeric_index):立即整数索引仍经过 spelling→ToNumber→NumberToString→spelling comparison。可先处理已确定的非负整数 Atom,再合并无回调区间的 buffer 状态读取。注意 ta[-0]ta["-0"]、非规范字符串、越界和 detached/resizable buffer;值转换可执行用户代码,之后仍需重验。

  5. P1 · 平坦字符串路径Utf16Units):平坦短字符串也建立含 61 个 Option 槽的通用遍历状态。为 Latin1/UTF-16 提供直接 slice 比较、哈希与范围复制,rope 保留通用路径。V8 RegExp 中 Utf16Units::next 自身约 24.8%,replacement 的重复 skip/range 扫描也应检查。维持 surrogate、跨表示相等性与 hash/Eq 一致性。

  6. P1 · Arguments / RegExp 结果批量发布ArgumentsRegExp exec):高频新对象仍逐项安装属性、反复替换布局。复用已有“先准备、一次发布”的思想;保留 mapped arguments 别名/解除映射、strict callee、实际参数数,以及 RegExp lastIndex 转换顺序、compile 重入、groups/indices 和属性顺序。

  7. P1 · VM local/值栈/dispatchexecute_inner):原始数值转换优化后,空循环热点转向直接 binding 读取、dispatch 和 numeric update。先证明 publication/verification 已保证哪些条件,再减少重复检查和中间 Value 搬运,评估安全的直接槽/栈窗口。保留 TDZ、eval/capture、回溯 PC、suspension 与外部字节码契约。

  8. P2 · 函数帧与参数存储take_call_argumentscallee frame):调用方 split_off 参数 Vec,callee 再 clone 参数并建立 locals、flags 和值栈。先在同步非逃逸调用中评估帧/参数存储复用;generator/async、arguments/eval、闭包逃逸和递归需要独立边界。

  9. P2 · 小规模引用事务retain_edges_transactionally):0/1/2 条边也建立 HashMap。短输入可采用小数组预检,复杂情况保留通用路径,不能削弱重复边计数、溢出预检和发布前失败保证。固定 RayTrace/Splay 的 replace_layout 分别约占 38%/21%,表明布局成本也存在于真实程序,不仅是 Context 初始化。

三个 critical 的架构/测量流程问题

  1. 局部操作被放大成全局工作。 单键查找复制全表、单槽写入复制整份布局、确定的整数索引往返字符串,这是可定位的算法与内部流程成本。应区分“不会调用用户代码的内部借用区间”和“可能重入的语义边界”,在必要的边界建立根引用,而不是到处套用完整公共 API 流程。保持 unsafe_code = "forbid",不靠删除所有权/异常检查换性能。
  2. 低分辨率 microbench 不能承担几个百分点改动的验收。 当前原始 microbench 的 Oxide 五项读数连续三次都是 200/500/400/500/400 ns,QuickJS 为 10/10/10/25/10 ns。Date.now fallback 等待下一 tick,并在约 2 ms 窗口取最小值,存在明显台阶。原始成绩保留;细粒度验收应使用足够长的固定工作量、交错重复、分布与指令数。还要注意 -T 的 coverage:本轮 Map 和 RegExp 大负载各只有 9 个 arena backing-storage 事件,不能据此认为临时 Vec 或字符串分配很少。
  3. 只从能完成的 harness 取得反馈,会遗漏最慢的一半套件。 旧报告诚实记录了 timeout,但这些路径仍缺少同等深度的热点反馈。本轮四个原始 V8 harness 在 60 秒内未完成,拆成单次工作却全部完成。上游 harness 有 warmup 和至少 32 次计时调用要求;单次耗时数秒会自然放大总时长。应同时保留原始 harness 状态与所有子套件的固定工作/限时采样,不能以可完成子集推导全引擎成绩。

全部 V8 子套件:单次工作与原始筛查

固定版各三轮中位数;原始版每引擎单轮、60 秒 timeout。固定版不输出上游 score,包含 setup/compile/teardown,不能冒充 steady-state score。

Case 固定 Oxide ms 固定 QuickJS ms 慢倍数 原始 Oxide 状态
richards 134.82 4.94 27.3× ok
deltablue 208.04 8.74 23.8× ok
crypto 4117.14 91.52 45.0× timeout
raytrace 1643.19 30.51 53.9× ok
earley-boyer 3819.20 94.23 40.5× timeout
regexp 6992.77 308.23 22.7× timeout
splay 3935.75 398.19 9.9× ok
navier-stokes 3724.90 59.04 63.1× timeout

采样质量、profiler 开销与验收限制

  • 深栈必须单独解释:固定 Earley-Boyer 约 75.92% 的周期权重触及 127 帧上限;原始 12 秒前缀中 Earley-Boyer 为 92.80%、Crypto 为 48.30%。这些数据适合叶函数/近端热点,不能当完整调用树。固定 Richards/DeltaBlue 只有 82/121 个样本,相邻小热点不宜精确排名。
  • 初次符号路径与 inline 展开设置有问题,最终全部从保留的原始 perf data 用匹配 ELF/libc、关闭 inline 展开重新解码。旧解码不用于本文结论。一次解析验证与广泛筛查短暂重叠,关键候选另做独立重复;正式固定墙钟 benchmark 在此前已完成。
  • 11 轮模式轮换的小分配负载,普通/compiled-off/dump/trace 的全进程中位数为 11.902 / 12.102 / 13.358 / 12.227 ms,相对普通为 1.000 / 1.017 / 1.122 / 1.027×。包含报告 I/O,且该独立模式实验未固定核心;不能宣称通用 VM instrumentation overhead。
  • 100 次独立生命周期样本中 Context 创建中位约 1.712 ms;未用 QuickJS CPU lifecycle time 除以 Oxide wall time。
  • 本轮没有修改引擎,也没有重跑全量 Test262。新 fixed-work / CPU 采集工具与既有 Python benchmark 测试共 11 项通过。实现候选仍须独立 A/B、相关语义验证与完整回归;热点比例不等于未来收益。

建议的第一批验收

  • Map:去全表快照并建立索引,固定总操作数的规模曲线不再随 Map 容量近似线性增长。
  • Array:数字索引热路径避免 length/string interning,覆盖写入、slice 与 Navier-Stokes。
  • 已有槽写入:同 flags 快路径先于布局复制,宽度实验不再明显依赖总属性数。
  • 再分别推进 TypedArray、平坦字符串、结果对象批量构建;之后评估 VM 和调用帧改动。
  • 每项使用当前快照的普通构建作对照,保留所有失败/timeout/回退证据,并保持语义/所有权/字节码兼容性验证。
完整 50 项固定 microbench(ms,中位数 [min–max])

* 标记参考进程短于 20 ms。通常每引擎三轮;四项转换采用 N=500000 的五轮追加结果。

Case N Oxide QuickJS 慢倍数
empty_loop 6084891 941.337 [936.818–943.084] 28.004 [25.242–30.716] 33.6×
empty_down_loop 4922868 727.989 [719.465–757.899] 22.280 [22.173–22.572] 32.7×
prop_read 846083 962.523 [956.778–976.543] 23.904 [23.167–24.287] 40.3×
prop_write 314880 988.454 [986.772–1014.869] 10.025 [9.998–10.279] 98.6× *
prop_update 244870 984.076 [974.529–1001.705] 9.354 [9.170–10.638] 105.2× *
prop_create 16000 473.764 [464.743–475.411] 13.661 [13.342–14.824] 34.7× *
prop_clone 16000 354.274 [353.050–356.886] 15.422 [15.218–15.574] 23.0× *
prop_delete 13775 933.515 [931.566–947.708] 19.476 [19.289–20.910] 47.9× *
array_read 258984 974.331 [939.281–990.257] 15.199 [14.947–15.566] 64.1× *
array_write 62914 1018.251 [993.866–1018.615] 6.412 [5.475–6.886] 158.8× *
array_update 50862 991.071 [986.400–1016.193] 6.412 [5.367–6.542] 154.6× *
array_prop_create 429 955.775 [948.984–962.737] 5.871 [5.711–6.808] 162.8× *
array_slice 872 934.589 [929.606–942.152] 14.383 [13.640–14.607] 65.0× *
array_length_read 763569 988.487 [971.999–1012.538] 21.206 [20.980–21.212] 46.6×
array_length_decr 276 900.721 [889.720–910.088] 12.216 [12.195–12.344] 73.7× *
array_push 536 954.363 [948.038–964.242] 9.988 [9.929–11.055] 95.6× *
array_pop 357 956.865 [953.330–970.513] 12.634 [12.605–13.189] 75.7× *
typed_array_read 136651 964.113 [960.264–966.960] 13.486 [11.362–13.642] 71.5× *
typed_array_write 130706 942.938 [931.311–948.904] 15.066 [14.250–15.294] 62.6× *
arguments_read 19199 1005.923 [996.929–1012.721] 10.039 [9.968–10.179] 100.2× *
arguments_strict_read 21299 989.891 [984.626–1002.527] 9.093 [8.195–9.700] 108.9× *
global_read 800000 475.010 [474.074–491.454] 14.707 [14.498–14.808] 32.3× *
global_write 800000 513.182 [502.537–524.331] 21.557 [21.346–21.838] 23.8×
local_destruct 16000 194.053 [193.707–196.012] 6.304 [5.985–6.655] 30.8× *
global_func_call 400000 799.553 [793.922–864.780] 31.861 [31.830–33.503] 25.1×
func_call 400000 814.395 [777.952–838.658] 29.778 [29.338–30.055] 27.3×
func_closure_call 387594 971.655 [915.930–991.790] 31.075 [31.040–31.168] 31.3×
int_arith 2936 895.295 [880.068–903.705] 32.691 [30.275–32.980] 27.4×
float_arith 2318 956.662 [945.460–962.188] 29.256 [28.546–29.576] 32.7×
bigint64_arith 1600 629.182 [628.469–654.626] 47.314 [46.079–47.422] 13.3×
map_set_string 10 1022.977 [997.324–1046.565] 4.225 [4.085–4.377] 242.1× *
map_set_int 22 946.043 [943.617–1013.494] 4.220 [3.348–4.227] 224.2× *
map_delete 17 1003.507 [962.275–1019.598] 4.575 [4.382–4.849] 219.4× *
weak_map_set 320 472.549 [464.130–476.554] 20.806 [19.766–20.833] 22.7×
array_for 16000 872.099 [865.024–875.632] 17.652 [16.930–17.819] 49.4× *
array_for_in 13525 923.327 [905.315–929.677] 54.248 [53.667–54.789] 17.0×
array_for_of 8710 934.913 [925.805–944.980] 16.330 [15.777–16.743] 57.3× *
math_min 1105 910.831 [909.563–911.811] 32.099 [31.006–32.112] 28.4×
regexp_ascii 113 975.183 [961.477–1001.322] 16.314 [16.209–16.830] 59.8× *
regexp_utf16 114 978.061 [973.794–1006.473] 16.397 [16.154–16.575] 59.6× *
regexp_replace 115 958.446 [949.552–958.699] 72.638 [71.001–73.048] 13.2×
string_length 486553 927.943 [913.993–944.636] 15.154 [14.997–15.207] 61.2× *
string_build1 1600 413.676 [411.749–436.005] 28.669 [27.232–29.176] 14.4×
string_build3 1600 412.570 [410.640–420.591] 48.273 [48.176–51.337] 8.5×
string_build_large1 80 460.992 [457.152–561.467] 67.594 [67.079–71.105] 6.8×
string_build_large2 80 459.190 [449.246–464.798] 65.614 [65.536–66.535] 7.0×
int_to_string 500000 337.349 [336.992–340.163] 41.085 [39.540–43.002] 8.2×
float_to_string 500000 1637.664 [1625.477–1644.945] 248.151 [246.025–254.227] 6.6×
string_to_int 500000 201.339 [196.078–212.734] 28.860 [28.259–29.571] 7.0×
string_to_float 500000 181.219 [175.300–224.374] 44.904 [43.703–45.081] 4.0×

原始证据状态

上述 benchmark/profile 数字已直接列在 issue 中。原始 perf data/stat、ELF、stdout/stderr、完整样本、构建 receipt 和复现脚本已保留在本地与 eric-83am;原始包尚未上传 GitHub,不把本地路径当作公开下载链接

  • 文件:quickjs-oxide-broad-profile-eric-83am-2026-09-10.tar.gz(65,293,018 bytes,2751 个成员逐项验证)。
  • SHA-256:84b3f5d43b74ccd57aef352a6d05f3e916019895bfda52e720e0e679400b339f
  • 不包含外部 benchmark 源码或生成的第三方 workload。新建的 fixed-work/CPU 工具与完整报告目前仍是本地工作区改动,尚未有公开提交链接。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions