JIT 即时编译(Just-In-Time Compilation)完整详解
一、基础概念
JIT = Just-In-Time,即时编译 核心思想:程序运行时(Runtime),把中间代码动态编译成本地机器码执行。 对比两种经典执行模型:
- 解释执行(Interpreter) 边读中间代码 → 逐条翻译成机器指令立刻执行,不保存结果;重复执行同一段代码要反复翻译。 优点:启动快;缺点:循环、高频代码性能极差。例子:早期 Python、老式 Lua、浏览器初代 JS 解释器
- AOT 提前编译(Ahead-of-Time) 程序运行之前,一次性把源码 / 中间代码全部编译为平台机器码,直接运行二进制文件。 优点:运行速度稳定;缺点:启动编译耗时、跨平台差、无法利用运行时信息优化。例子:C/C++、Rust、Go
- JIT 即时编译(折中方案) 先靠解释器快速启动;运行过程中监控代码热度,把频繁执行的热点代码动态编译成机器码缓存起来,后续直接运行机器码。代表:Java HotSpot 虚拟机、V8 (Chrome/Node.js)、PyPy、.NET CLR
二、JIT 完整工作流程(以 HotSpot JVM 为例)
阶段 1:源码 → 中间表示(编译期,非运行时)
Java 源码 .java → javac 静态编译 → 字节码 .class(Bytecode,平台无关中间代码)
注意:javac 只是生成中间码,不是机器码!
阶段 2:程序启动,解释器先行(启动阶段)
JVM 加载 class 字节码,先用解释器逐条解释执行 ✅ 优势:程序可以快速启动,不需要等待全部代码编译。
阶段 3:运行采样,识别热点代码(Profiling 剖析)
JVM 后台持续统计:循环、频繁调用的函数执行次数,达到阈值标记为HotSpot 热点代码。 典型热点:高频循环、工具函数、频繁调用方法。
阶段 4:JIT 编译器启动,热点编译(核心步骤)
把热点字节码一次性编译为当前 CPU 架构原生机器码,同时做大量优化:
- 常量传播、死代码消除
- 循环展开、循环不变量外提
- 方法内联(Inline,消除函数调用开销)
- 逃逸分析、栈上分配
- 分支预测优化
- 类型特化(动态语言 JIT 尤为重要,如 V8)
阶段 5:缓存机器码,后续直接执行
编译好的机器码存入代码缓存(Code Cache)。 下次执行这段代码时:不再解释、不再重新编译,直接执行本地机器码,速度大幅提升。
阶段 6:逆优化(Deoptimization,去优化)【动态语言关键】
运行环境发生变化,之前编译时的假设失效: 例:Java 多态、JS 变量类型突变。 此时废弃优化后的机器码,退回解释模式重新执行,等待重新收集信息再次 JIT 编译。
三、两类主流 JIT 实现
1. 追踪式 JIT(Tracing JIT / Trace-based JIT)
代表:PyPy、LuaJIT 逻辑:追踪程序执行路径,捕捉高频执行的一条执行轨迹(trace),只编译这条路径。 适合:大量循环、数值计算。 缺点:分支多、执行路径多变时收益下降。
2. 方法式 JIT(Method-based JIT / Function JIT)
代表:HotSpot JVM(C1/C2 编译器)、V8 TurboFan 逻辑:以整个函数 / 方法作为编译单元,统计方法调用次数,达标后编译整个方法。
HotSpot 双层 JIT(经典设计)
- C1(客户端编译器):快速编译,少量优化,适合启动速度优先
- C2(服务端编译器):慢速编译,极高强度优化,长时间运行服务首选
四、JIT 固有缺陷
-
编译开销(CPU / 内存占用) 运行中占用 CPU 编译代码;机器码占用专门代码缓存。短时运行小程序,JIT 编译耗时 > 性能收益,越跑越慢。典型场景:命令行短脚本不适合 JIT。
-
启动延迟(暖启动开销) 程序刚启动一段时间只能解释执行,需要预热才能到达最高性能。冷启动性能差。
-
内存不可控 代码缓存存在上限;缓存持续膨胀会触发清理、反复重编译。
-
优化失效、逆优化开销 动态类型语言频繁类型变更、多态重载频繁触发 deoptimize,性能抖动。
-
调试、安全、可观测性更复杂 栈跟踪、崩溃转储需要同时处理解释帧 + JIT 机器码帧;部分安全沙箱对动态生成代码有限制(W^X 防护)。
五、典型应用案例
-
Java / JVM(HotSpot) 最知名 JIT 实现,后端服务依靠 JIT 预热获得高性能。补充:GraalVM 同时支持 JIT 与 AOT 两种模式。
-
V8 引擎(Chrome、Node.js) JS 是动态弱类型语言。 早期:Ignition 解释器 + TurboFan JIT;依靠类型反馈,为稳定类型生成优化机器码。
-
PyPy CPython 无 JIT(纯解释);PyPy 内置 Tracing JIT,数值循环代码比 CPython 快几倍~几十倍。
-
.NET Core CLR CLR JIT:程序加载时即时编译 IL 中间代码;同时提供 ReadyToRun(AOT 预编译选项)。
-
LuaJIT 高性能 Lua 实现,基于追踪式 JIT,游戏、网关大量使用。
深度学习框架中的 JIT 完整解析 + 如何绕过 JIT
先区分:通用编程语言 JIT(Java/LuaJIT) ≠ 深度学习框架里的 JIT 深度学习 JIT 本质:把动态 Python 计算图 → 运行时编译为静态 IR / 优化内核,消除 Python 调度开销。
一、深度学习领域主流 JIT 体系
1. PyTorch 两条 JIT 路线
(1)TorchScript JIT(PyTorch 1.x 经典)
torch.jit.trace()/torch.jit.script()- 工作方式: Eager 模式(动态图,Python 逐行执行)→ 捕捉计算流程生成 TorchScript IR → 运行时编译优化;生成
*.pt可在 LibTorch C++ 运行。 - 触发时机:调用
trace/script生成 ScriptModule,前向执行时执行 JIT 编译器做算子融合、死代码消除。
(2)torch.compile()(PyTorch2.0+,Inductor 后端)
新一代 JIT 编译器,默认后端 Inductor。 逻辑:捕获 forward 完整计算图 → 编译成优化后的 C++/CUDA 内核。
重大痛点:每遇到新的 tensor shape / 数据类型,会触发重新编译;在线推理遇到动态 batch / 动态分辨率会出现突发延迟尖刺。
2. TensorFlow / JAX JIT(XLA)
- TF2:
tf.function()= XLA JIT - JAX:默认所有函数走 XLA JIT(强制 JIT ) 原理:将 TF/JAX 算子编译为 XLA IR,融合算子、消除内存读写;同样存在shape 变化触发重编译问题。
3. 其他推理引擎隐性 JIT
- ONNX Runtime:内部有 MLAS/CPU JIT、CUDA 算子即时编译
- TensorRT:载入 ONNX 后在线编译 engine(广义 JIT,运行期生成优化引擎)
- TVM:运行时编译算子,属于典型深度学习 JIT
二、深度学习 JIT 和普通 JIT 核心区别
通用 JIT(HotSpot、V8)
边解释执行,统计热点,运行一段时间后自动编译热点代码。
深度学习框架 JIT
主动、显式触发编译 ;大多在第一次前向传播完成编译 ;不是自动探测热点,用户主动开启。 代价:首次推理延迟极高(编译耗时);shape 变化触发重编译。
优势
- 算子融合(Conv+BN+ReLU 合并为单个 CUDA kernel,减少显存读写)
- 消除 Python GIL、Python 层循环开销
- 常量折叠、静态形状优化、死代码删除
代价(也是你想要绕过它的根本原因)
- 冷启动巨大延迟:第一次 forward 卡住做编译
- 动态输入 shape 持续重编译(线上 API 噩梦,延迟抖动)
- 大量自定义算子、控制流、if-else、循环难以兼容,编译报错
- 调试困难,栈信息丢失,难以定位报错
- 占用额外 CPU / 显存存储编译缓存
典型需要绕过 JIT 场景: 1)动态分辨率 / 动态 batch 在线推理;2)快速调试模型;3)短生命周期一次性任务;4)模型存在大量条件分支;5)不允许延迟抖动。
三、分框架:如何【绕过 / 禁用 JIT】
🔹 PyTorch
1)TorchScript JIT(torch.jit.trace/script)
绕过 = 不要使用 trace/script ,直接原生model.eval() eager 执行
# 使用JIT(不要这么写,如果想绕过)
# model = torch.jit.trace(model, dummy_input)
# 绕过:原生Eager模式
model.eval()
with torch.no_grad():
out = model(x)
全局环境变量强制禁用 TorchScript 编译器(调试用)
# shell
export PYTORCH_JIT=0
python run.py
2)PyTorch2.0 torch.compile ()(高频踩坑点)
绕过方法:不调用 torch.compile
# 开启JIT
# model = torch.compile(model)
# 绕过,直接原生eager
model.eval()
如果你代码里被第三方库隐式 compile,可以全局关闭:
import torch
import os
# 方式1:环境变量
os.environ["TORCH_COMPILE_DISABLE"] = "1"
# 方式2:运行时开关(新版本支持)
torch._dynamo.config.disable = True
补充:很多人混淆:Eager ≠ JIT PyTorch 默认 Eager 模式没有 JIT 编译 ;只有手动调用
jit.trace/script / torch.compile才开启。
🔹 TensorFlow XLA JIT(tf.function)
绕过策略:
-
不要装饰 @tf.function,直接原生 Eager 执行(tf2 默认 eager)
开启XLA JIT
@tf.function(jit_compile=True)
def infer(x):
return model(x)绕过JIT,原生eager
model(x)
全局关闭自动 XLA 聚类
export TF_XLA_FLAGS="--tf_xla_auto_jit=-1"
局部作用域禁用 XLA
with tf.xla.experimental.jit_scope(compile_ops=False):
y = model(x)
🔹 JAX(重点!JAX 默认强制 JIT)
JAX 所有函数默认 jit,想要绕过必须显式关闭:
import jax
# 默认开启jit
# jax.jit(fn)
# 绕过:不要jit包装,或者使用 jax.disable_jit()
with jax.disable_jit():
res = fn(x)
四、进阶思路:两种 "绕过 JIT" 路线(工程部署最常用)
路线 A:运行期彻底不触发任何即时编译(调试 / 动态输入首选)
全程使用框架原生 Eager 动态执行:
- PyTorch eager
- TensorFlow2 eager
- JAX disable_jit 缺点:性能偏低,无法算子融合优化
路线 B:AOT 提前编译,把编译阶段离线做完,线上运行不再编译(广义绕过运行时 JIT)
很多场景需求不是 "不要编译",而是不要在线推理的时候编译:
离线一次性编译 → 保存引擎文件 → 线上直接加载预编译好的引擎,运行期无编译开销。 代表方案:
- PyTorch → ONNX → 提前导出 TensorRT Engine(离线 build),推理直接 load engine,无运行编译
- Torch → torch.export AOT 导出,不用 torch.compile 运行时编译
- TVM:离线 compile 生成 model library,线上直接加载
⚠️ 区分概念:
- 运行时 JIT(要规避):线上第一次推理 / 新 shape 触发编译
- AOT 预编译(推荐替代方案):离线一次性编译,线上直接执行,不存在 JIT 延迟
五、极易混淆误区澄清
-
ONNX Runtime ≠ 一定带 JIT ORT 默认 CPU 后端部分算子存在 JIT;CUDA ExecutionProvider 大多直接调用 cuBLAS/cuDNN 预编译算子,无 JIT。 如果你想关闭 ORT 内部 JIT,可以设置 session option:
import onnxruntime as ort
opts = ort.SessionOptions()
opts.use_cpu_jit = False
sess = ort.InferenceSession("model.onnx", sess_options=opts) -
TensorRT 载入 ONNX = JIT 行为
builder.build_serialized_network()属于运行期编译;想要绕过运行时 JIT ,需要提前保存 engine 文件 ,线上直接加载.engine,避免推理服务启动时编译。 -
不要混淆:CUDA Graph ≠ JIT CUDA Graph 是捕获 kernel 执行序列,不属于编译式 JIT;可以和 JIT 搭配使用,二者独立。
六、决策参考:什么时候应该绕过 JIT?
建议绕过 JIT
- 输入 shape 动态变化(动态分辨率、不定长序列、可变 batch)
- 在线推理严格控制尾延迟,不允许突发编译卡顿
- 调试模型、逐层打印张量、pdb 断点调试
- 模型含有大量分支 if/else、循环、动态控制流
- 短任务,执行次数很少,编译开销 > 优化收益
建议开启 JIT
- 固定 shape 离线推理、长时间持续跑同一个模型(训练任务、静态批量推理)
- 数值密集计算,循环多,Python 开销巨大