Lambda和Stream本身是高级抽象,运行时通常会变成对象分配、接口调用、MethodHandle调用和多层Sink调用。编译器要做的是把这些抽象逐层“拆掉”。
例如:
long sum = points.stream()
.mapToLong(p -> p.x * p.x)
.filter(v -> v > 10)
.sum();
未经充分优化时,大致是:
Stream对象
→ Map Sink对象
→ Filter Sink对象
→ Reduce Sink对象
→ Function接口调用
→ lambda对象
→ 装箱/拆箱
优化充分后,效果接近:
long sum = 0;
for (Point p : points) {
long v = p.x * p.x;
if (v > 10) {
sum += v;
}
}
主要需要以下能力。
优先级 | 优化能力 | 作用
-- | -- | --
P0 | _allocateInstance intrinsic | 避免lambda、Sink、Collector等对象通过VM通用慢路径分配
P0/P1 | MethodHandle/invokedynamic目标解析 | 把lambda调用解析到真实的lambda$xxx方法
P1 | Type PGO、去虚拟化 | 将Function.apply、Sink.accept等接口调用变成直接调用
P1 | late/incremental inline | 热点信息稳定后,把整条Stream/lambda调用链继续内联
P1 | EA/PEA、标量替换 | 消除不逃逸的lambda、Stream stage、Sink、迭代器和Ref对象
P1 | Stream/Sink链融合 | 合并多层accept/opWrapSink/copyInto调用,暴露真正的数据循环
P2 | 装箱消除 | 消除Integer.valueOf、Long.valueOf及对应拆箱
P2 | 循环优化 | 内联和EA完成后,再做边界检查消除、LICM、展开和向量化
P2 | GC barrier优化 | 对已消除或新创建的临时对象删除、合并不必要的barrier
Lambda优化的关键链路
一段lambda:
items.forEach(x -> consume(x.value));
运行时可能是:
invokedynamic
→ Lambda对象
→ Consumer.accept接口调用
→ lambda$xxx
→ consume
编译器需要依次完成:
解析invokedynamic/MethodHandle目标
→ 根据类型profile去虚拟化Consumer.accept
→ 内联lambda$xxx
→ EA判断lambda对象不逃逸
→ 消除lambda对象分配
缺少其中任意一环,后续优化都会受影响。例如目标没有解析出来,EA就看不到lambda内部;方法没有内联,循环优化也看不到实际计算。
Stream优化的关键链路
Stream更复杂,因为每个stage通常对应一个对象:
ReferencePipeline
→ map Sink
→ filter Sink
→ reduce Sink
→ Spliterator
理想优化顺序是:
类型特化
→ 内联Sink.accept
→ 内联lambda
→ 消除Stream/Sink/Iterator对象
→ 合并调用链
→ 暴露循环
→ 循环和向量优化
对当前用例的对应关系
scrabble
当前第一问题不是泛泛的Stream优化,而是 _allocateInstance。C2关闭该intrinsic后从约51 ms变成79 ms,已经接近C3的80 ms。补齐后再看:
IntPipeline$1.opWrapSinkAbstractPipeline.wrapSinkcopyIntoWithCancelCollectors.summingLong- late inline和Stream/Sink对象消除
rx-scrabble
RxJava虽然不是JDK Stream,但结构类似,是Observer/operator/lambda链。优先考虑:
_allocateInstance- VarHandle/MethodHandle目标解析
- Observer回调去虚拟化
- Rx operator、Disposable和Observer对象的EA
scala-kmeans
findClosest → Vector.foreach → lambda → squareDistance在C3中已经基本内联,因此这里不是“lambda没内联”。优先级是:
_allocateInstanceDoubleRef/ObjectRef及闭包对象的EA/标量替换- 最后才是Vector循环优化
简单说,Lambda/Stream优化不是增加一个单独的“Lambda优化pass”,而是一条能力链:
MethodHandle解析 → Type PGO去虚拟化 → late inline → EA/标量替换 → Stream链融合 → 循环优化。
当前最应该先做的是_allocateInstance;否则大量时间还停留在对象分配runtime路径,后面的Stream和lambda优化收益很难正确评估。
Lambda和Stream本身是高级抽象,运行时通常会变成对象分配、接口调用、MethodHandle调用和多层Sink调用。编译器要做的是把这些抽象逐层“拆掉”。
例如:
未经充分优化时,大致是:
优化充分后,效果接近:
主要需要以下能力。
Lambda优化的关键链路
一段lambda:
运行时可能是:
编译器需要依次完成:
缺少其中任意一环,后续优化都会受影响。例如目标没有解析出来,EA就看不到lambda内部;方法没有内联,循环优化也看不到实际计算。
Stream优化的关键链路
Stream更复杂,因为每个stage通常对应一个对象:
理想优化顺序是:
对当前用例的对应关系
scrabble当前第一问题不是泛泛的Stream优化,而是
_allocateInstance。C2关闭该intrinsic后从约51 ms变成79 ms,已经接近C3的80 ms。补齐后再看:IntPipeline$1.opWrapSinkAbstractPipeline.wrapSinkcopyIntoWithCancelCollectors.summingLongrx-scrabbleRxJava虽然不是JDK Stream,但结构类似,是Observer/operator/lambda链。优先考虑:
_allocateInstancescala-kmeansfindClosest → Vector.foreach → lambda → squareDistance在C3中已经基本内联,因此这里不是“lambda没内联”。优先级是:_allocateInstanceDoubleRef/ObjectRef及闭包对象的EA/标量替换简单说,Lambda/Stream优化不是增加一个单独的“Lambda优化pass”,而是一条能力链:
当前最应该先做的是
_allocateInstance;否则大量时间还停留在对象分配runtime路径,后面的Stream和lambda优化收益很难正确评估。