JIT 在程序运行期间把某种表示编译成可执行代码。输入可以是字节码、IR 或其他表示;编译单位可以是函数、循环、trace 或模块。是否按首次调用触发、是否后台预编译,由运行时策略决定。
关键权衡是编译成本能否被后续执行收益摊薄。它既需要生成快代码,也需要避免把短命代码优化得过于昂贵。
一个简单的收益模型
设优化编译成本为 C,旧代码每次执行耗时 t₀,新代码为 t₁,预计还会执行 K 次。在忽略其他成本时,只有 K(t₀−t₁)>C 才能净节省时间。
例如编译花 4ms,每次节省 2μs,至少还要执行约 2000 次才抵消编译成本。实际还要考虑 profiling、守卫、代码空间、缓存和反优化成本。这解释了为什么“生成的代码更快”不一定让整个短程序更快。
临界点只适用于这次剩余工作量
4 ms 的编译成本与每次 2 μs 的节省,在 2000 次时只是持平;想获得净收益,需要超过这个次数。热点此前已经执行了多少次不直接进入这笔未来收益账,因为已经花掉的解释时间无法通过本次编译追回。
compile_us = 4000
saved_per_call_us = 2
def gain(remaining_calls):
return remaining_calls * saved_per_call_us - compile_us
assert gain(1000) == -2000
assert gain(2000) == 0
assert gain(3000) == 2000
assert gain(3000) - 600 == 1400
实际决策面对的是不确定的剩余执行次数与失效率。优化一个即将结束的循环可能得不偿失;后台编译也会占 CPU 和内存带宽,不能因为编译线程与请求线程不同,就把编译成本从整个服务的资源账中删除。上面只是串行时间预算模型,不是并行编译时墙钟收益的直接公式。
过去已经跑热,能抵消这次编译成本吗?
这里需要比较的是从现在开始的两条策略。过去执行越多,两条总账都会增加同一份沉没成本,所以不会改变未来净收益。下图把过去单列为共同成本,编译投资只与剩余 K 次的节省比较。等号是持平,不是已经盈利;真实系统还需要预测 K,而预测可能错误。
gain=K(t0−t1)−C
正在呈现知识画面
过去已经跑热,能抵消这次编译成本吗?已执行部分是共同沉没成本。只用未来节省 K(t₀−t₁) 偿还本次 C;K=2000 持平,超过才有净收益。更热的过去并不保证还有足够未来。
{"id":"jit-remaining-payback","title":"过去已经跑热,能抵消这次编译成本吗?","summary":"已执行部分是共同沉没成本。只用未来节省 K(t₀−t₁) 偿还本次 C;K=2000 持平,超过才有净收益。更热的过去并不保证还有足够未来。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"past\" data-i18n=\"past\"></label><output for=\"past\" id=\"past-value\"></output><input id=\"past\" type=\"range\" min=\"0\" max=\"10000\" value=\"5000\" step=\"1000\"></div><div class=\"control\"><label for=\"future\" data-i18n=\"future\"></label><output for=\"future\" id=\"future-value\"></output><input id=\"future\" type=\"range\" min=\"0\" max=\"5000\" value=\"2000\" step=\"100\"></div><div id=\"costs\" class=\"table\"></div><div class=\"metrics\"><div><span data-i18n=\"old\"></span><strong id=\"old\"></strong></div><div><span data-i18n=\"new\"></span><strong id=\"new\"></strong></div><div><span data-i18n=\"gain\"></span><strong id=\"gain\"></strong></div></div><p id=\"equation\" class=\"formula\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.metrics{grid-template-columns:repeat(3,minmax(0,1fr))}.metrics strong{font-size:18px}#costs .tr{grid-template-columns:1fr 2fr}.costbar{height:18px;background:var(--accent);display:block;border-radius:3px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function draw(){const past=+$('#past').value,k=+$('#future').value,old=k*.003,ne=4+k*.001,gain=old-ne;set('past-value',past);set('future-value',k);table('costs',[['old',old.toFixed(1)+' ms'],['new',ne.toFixed(1)+' ms'],['past',(past*.003).toFixed(1)+' ms (both)']]);$('#costs').querySelectorAll('.tr').forEach((e,i)=>{const bar=document.createElement('i');bar.className='costbar';bar.style.width=([old,ne,past*.003][i]/30*100)+'%';e.children[1].append(bar)});set('old',old.toFixed(1));set('new',ne.toFixed(1));set('gain',gain.toFixed(1));set('equation',k+' × (3−1) μs − 4000 μs = '+(k*2-4000)+' μs');$('#gain').dataset.profitable=String(gain>0);}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"future":"未来剩余次数 K","gain":"净节省 / ms","heading":"过去已经跑热,能抵消这次编译成本吗?","intro":"编译成本4ms,每次旧代码3μs、新代码1μs。独立改变过去次数与未来剩余次数,观察净收益由谁决定。","new":"编译并用新代码 / ms","old":"继续旧代码 / ms","past":"过去执行次数","reset":"重置","scope":"串行时间模型,固定编译和单次执行成本,不含后台编译竞争、profile、守卫、失效、代码空间或实际 JIT 预测策略。","scopeLabel":"模型范围与约定"}}
分层:先降低启动成本,再投资热点
| 层次 | 目标 | 常见工作 |
| 解释器 | 快速开始执行 | 解码与分派,收集部分反馈 |
| Baseline 编译器 | 用较低编译成本消除解释开销 | 快速生成机器码,有限优化 |
| 优化编译器 | 提高热点代码效率 | 内联、类型特化、冗余消除等 |
并非所有运行时都具备全部层次,也不保证每个函数逐层经过。策略可以参考调用数、循环回边、代码大小、已有反馈和系统资源;失效或环境变化还可能引起重编译。
以 V8 的公开设计为例,Ignition 是解释器,Sparkplug 是从字节码生成机器码的 baseline 编译器;Maglev 的设计展示了中间优化层如何平衡编译速度与代码质量。这些说明用于理解角色,不把某篇历史文章里的层次配置当作所有版本固定不变的流水线。Sparkplug、Maglev
反馈与内联缓存:记住这个访问点发生过什么
假设同一源码位置反复执行 obj.x。通用属性查找可能需要解释对象布局、属性描述符和继承关系。内联缓存记录该访问点观察到的 shape 等条件,以及对应的快速操作。
单态缓存保留一种形态,多态缓存保留少量形态;观察到很多不同形态后,可以改走适合高多态的查找策略。不同系统使用的结构与阈值不同。
缓存通常与访问点及其上下文关联,不是把“上一次任意对象的类型”作为全局答案。对象布局、原型链或方法定义变化时,需要守卫或依赖失效机制,避免缓存仍指向过时实现。IC 也可以存在于解释器中,并非只有优化机器码才能使用。
推测优化:假设必须显式受保护
JIT 观察到变量多为整数,可以生成整数快速路径,但语言可能允许下一次出现浮点数、字符串或大整数。守卫验证相关前提;失配时进入通用慢路径,或恢复到较低优化层执行。
AOT 也能通过静态分析、PGO、代码版本化与运行时检查使用推测。区别在于反馈获得与代码生成的时机,而不是 AOT 永远不知道运行时信息、JIT 才能优化。
反优化:恢复语言层面的状态
优化会把变量放进寄存器、删除冗余值、内联函数,甚至把一个对象拆成几个标量。当推测失效时,运行时必须重建后续执行需要的逻辑状态:当前字节码位置、局部变量、操作数栈、被内联的调用帧和必要对象。
因此反优化元数据记录如何从当前机器状态恢复较低层表示。一些值可以直接读取,一些需要按记录重新物化。仅保存几个寄存器不足以恢复被内联和标量替换的程序。
恢复点还要考虑副作用。假设已经执行一次扣款,然后守卫失败,不能从函数开头重跑导致再次扣款。反优化必须继续到一个语义正确的位置,不能笼统说“回解释器重做全部操作”。
一个带副作用的反优化状态模型
设教学语言的整数可任意精度增长,优化版本只处理 int32 范围。下面用 Python 模拟“运算前检查前提,失败后恢复 i 与 total”;它不生成机器码,也不使用 Python 解释器自己的 JIT 行为作为证据。
MIN32, MAX32 = -(1 << 31), (1 << 31) - 1
def resume_generic(values, i, total):
while i < len(values):
total += values[i]
i += 1
return total
def specialized(values, effects):
effects.append("begin")
total = 0
i = 0
while i < len(values):
item = values[i]
candidate = total + item if not (MIN32 <= item <= MAX32 and MIN32 <= candidate <= MAX32):
snapshot = {"pc": "loop_add", "i": i, "total": total}
return resume_generic(values, i, total), snapshot
total = candidate
i += 1
return total, None
effects = []
result, snapshot = specialized([MAX32, 1, 2], effects)
assert result == MAX32 + 3
assert snapshot == {"pc": "loop_add", "i": 1, "total": MAX32}
assert effects == ["begin"]
result, snapshot = specialized([10, 20], [])
assert result == 30 and snapshot is None
守卫失败时 i=1,表示 values[1] 尚未加入 total。若恢复成 i=2,会漏算 1;若恢复到函数开头,会再次执行 begin。真实生成器可能使用带溢出标志的机器加法后再分支,但反优化元数据必须能还原正确的原始操作数与恢复位置,不能把已经回绕的机器结果冒充语言结果。
如果这里还内联了另一个函数,低层栈上可能只有一个机器栈帧,逻辑上却需要恢复两个调用帧。若对象已经标量替换,还需物化它并保持所要求的身份关系;“生成一个字段相同的新对象”不总能保持引用相等等语义。V8 Maglev 的设计中,帧状态记录就是连接推测优化与恢复正确性的关键部分。Maglev 反优化
守卫失败后,从哪里继续才不会重复副作用?
恢复状态至少要回答:哪个操作还没做、哪些值已经算好、哪些副作用已经发生。下面故意把“从头重跑”和“跳过当前项”也做成可比较的错误策略。前者最终总和可能正确,却重复 begin;后者效果次数看似正常,却少加一项。只有同时检查值、位置和效果,才能说明反优化接续是否正确。
正在呈现知识画面
守卫失败后,从哪里继续才不会重复副作用?第二次加法超出 int32,守卫在执行它之前失败。正确恢复 i=1、total=2147483647,并保留已经发生一次的 begin;从头重跑会重复效果,跳过当前项会漏算。
{"id":"deoptimization-resume-state","title":"守卫失败后,从哪里继续才不会重复副作用?","summary":"第二次加法超出 int32,守卫在执行它之前失败。正确恢复 i=1、total=2147483647,并保留已经发生一次的 begin;从头重跑会重复效果,跳过当前项会漏算。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"mode\"><button data-mode=\"correct\" data-i18n=\"correct\"></button><button data-mode=\"restart\" data-i18n=\"restart\"></button><button data-mode=\"skip\" data-i18n=\"skip\"></button></div><p class=\"source\">begin();\nfor i in 0..3: total += values[i]</p><section class=\"panel\"><h3 data-i18n=\"state\"></h3><div id=\"state\"></div></section><div id=\"items\" class=\"track\"></div><div class=\"metrics\"><div><span data-i18n=\"total\"></span><strong id=\"total\"></strong></div><div><span data-i18n=\"effects\"></span><strong id=\"effects\"></strong></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"ok\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"bad\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n#state{font:13px/1.8 var(--viz-font-ui);min-height:9em;white-space:pre-line}.metrics strong{font-size:18px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}let mode='correct';function draw(){const values=[2147483647,1,2];let total=2147483647,i=1,effects=1;if(mode==='restart'){i=0;total=0;effects++}if(mode==='skip')i++;set('state','guard: i=1, total=2147483647\\nresume: i='+i+', total='+total+'\\nbegin already executed = 1');cells('items',values.map((v,j)=>({text:'['+j+'] '+v,cls:j>=i?'on':mode==='skip'&&j===1?'gold':'empty'})));for(;i<values.length;i++)total+=values[i];set('total',total);set('effects',effects);set('explanation',t(total===2147483650&&effects===1?'ok':'bad'));pressed(mode);}document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{mode=b.dataset.mode;draw()});draw();","audio":false,"strings":{"bad":"恢复位置或副作用次数错误;只看最终数字可能漏掉重复效果。","correct":"恢复当前项","effects":"begin 效果次数","heading":"守卫失败后,从哪里继续才不会重复副作用?","intro":"固定数组 [2147483647,1,2],语言整数可任意增长。选择恢复策略,比较优化路径、恢复状态和最终效果。","mode":"恢复策略","ok":"结果和副作用次数均与通用执行一致。","reset":"重置","restart":"错误:从头重跑","scope":"固定溢出位置的教学执行模型,无真实机器码、内联帧或对象物化;仅解释一个恢复点的局部状态和副作用次数。","scopeLabel":"模型范围与约定","skip":"错误:跳过当前项","state":"守卫失败时的状态","total":"最终 total"}}
OSR:函数尚未返回就切换执行版本
一个只调用一次、却运行很久的循环,等下次函数调用才用优化代码已经太迟。On-Stack Replacement 在运行中的某个合适位置,把当前活跃状态转移到另一版本的代码。
常见用途是从解释或 baseline 的热循环进入优化代码;一些运行时也用广义的栈上替换描述降级转换。它与反优化共享状态转换问题,但不能简单定义为“反优化的特例”:升层 OSR 恰恰是向更优化的版本转移。
OSR 入口必须说明每个活跃值来自哪里
例如循环已经处理 values[0:3],即将处理下标 3。新代码必须接收当前数组引用、i=3 与已累计的 total;不能沿用普通函数入口的 i=0、total=0 初始化。循环外不再使用的临时值可不转移,但后续需要的值必须有确定来源。
| 转移信息 | 为什么需要 | 缺失时的结果 |
| 恢复/进入的程序位置 | 区分循环头、循环体中间和回边 | 重复或跳过一部分操作 |
| 活跃局部变量和操作数 | 继续尚未结束的计算 | 使用错误初始值 |
| 对象与 GC 引用位置 | 让收集器仍能找到活跃对象 | 对象被错误回收或更新遗漏 |
| 异常与内联调用状态 | 保持抛出、展开和调试语义 | 异常落到错误的处理范围 |
OSR 编译也可能在生成期间发现假设已经不适用,进入时仍需验证相应条件。热度计数负责决定值得尝试,入口守卫负责确认此刻可以安全切换,两者不能互相替代。
代码缓存:生成之后还要安全发布与回收
JIT 要分配代码空间、处理重定位、登记 GC 与异常展开信息,并按平台要求发布可执行代码及同步指令缓存。它不必先把每个函数写成 .so 再 dlopen;运行时通常可以直接管理内存中的代码及符号。
回收代码之前要确认没有线程仍在执行它,返回地址、函数入口或缓存引用也不能悬空。采用什么分配器、淘汰策略和写执行权限转换,由实现与平台约束决定,不存在所有 code cache 都用 slab 加 LRU 的统一规则。
评估时分开看几个阶段
| 指标 | 可以暴露的问题 |
| 冷启动与首个请求 | 解析、加载和初始编译成本 |
| 预热过程 | 升层速度、编译线程竞争 |
| 稳态吞吐与尾延迟 | 代码质量、GC、调度与失配 |
| 反优化及重编译次数 | 不稳定假设、输入形态变化 |
| 代码与元数据空间 | 专用版本膨胀、回收不及时 |
不要只测预热后的一小段循环,再把结果推广为服务整体性能。AOT 与 JIT 也可以组合,通过缓存、离线 profile 或预编译基础代码减少运行时工作。
相关章节:垃圾回收、链接与加载。
参考
Keywords: JIT, tiered compilation, inline cache, speculation, guard, deoptimization, OSR, code cache, AOT, PGO