Android R8 为什么可以让 Kotlin 协程提速 2 倍?

0 阅读8分钟

这个问题其实我们之前 《AGP 9.2 开始,Android 上协程启动和取消速度提升两倍》就聊过了,因为 PR 终于被合并,所以现在从 Android Gradle Plugin 9.2.0 开始,R8 会将大部分 Atomic*FieldUpdater 调用会被优化为更直接的 Unsafe 操作。

这个变化让 kotlinx.atomicfu 中常见的原子写操作提升大概 2~4 倍,最终让 Jetpack Compose 中 LaunchedEffect 的协程启动和取消性能最高提升大概 2 倍

主要实现就是通过 R8 在应用构建阶段,在构建期提前验证调用对象和写入值的类型,绕过 Atomic*FieldUpdater 每次原子写操作前重复执行的动态类型检查。

实际上在之前,大家一直认为协程的主要成本主要来自线程切换、任务调度或者协程体本身的计算,但是在 Compose 的性能分析里,官方发现,在一些高频 UI 操作中,协程的创建、启动、取消和完成本身这个行为,就可能会占据性能消耗的相当大的比例

在 Compose 里大量并发 API 底层都会调用挂起函数,然后通过协程处理指针事件、动画和交互状态,比如 Modifier.clickable

在之前的实现里,创建和更新 Modifier.clickable 所消耗的时间里,大约 80% 都花在了启动和取消用于处理 InteractionSource 的内部协程上。

从这个角度看,大概就可以理解为什么 Compose 团队早期的一些性能优化会尝试:

  • 将协程移出默认执行路径
  • 延迟初始化交互相关对象
  • 只有真正需要时才创建内部协程

但这种做法本质上其实是在减少协程的使用次数,但是没有解决协程启动和取消本身为什么这么贵的问题

为了定位问题,Google 使用 ART Method Trace 记录了一次空 LaunchedEffect 的完整方法调用:

LaunchedEffect(Unit) {
    // 什么也不做
}

就算协程实现为空,但是这次调用至少经历三个阶段:

  • 初始化协程
  • 启动协程
  • 结束并完成协程

而如果是取消协程,流程和正常完成流程接近相似,但会需要创建一个 CancellationException,所以原文的 Perfetto 图中可以看到,大量细碎调用落在了 java.util.concurrent.AtomicReferenceFieldUpdater 上,虽然单独一次调用看起来并不算慢,但它会在协程初始化、状态切换、父子任务连接、取消和完成过程中反复出现

这里的原因主要在于,kotlinx.coroutines 为了实现结构化并发,需要维护一套无锁的父子任务关系

比如一个 Job 可以拥有多个子 Job,取消父任务需要向下传播,子任务完成后还要更新父节点状态,这类并发数据结构不能简单地用普通字段读写,而需要通过 CAS 等原子操作保证线程安全。

kotlinx.atomicfu 在 JVM 和 Android 上实现这些原子操作时,会使用类似下面的机制:

class Example {
    volatile String data = "";
​
    static final AtomicReferenceFieldUpdater updater =
        AtomicReferenceFieldUpdater.newUpdater(
            Example.class,
            String.class,
            "data"
        );
​
    void update() {
        updater.compareAndSet(this, "", "new");
    }
}

这里的 AtomicReferenceFieldUpdater 不是一个普通的 AtomicReference 对象,它通过“持有对象的类型、字段类型和字段名称”,可以在运行时定位并更新目标对象中的某个 volatile 字段。

这种设计能够减少额外的原子对象包装,但也带来了一个问题:

AtomicReferenceFieldUpdater 在创建时会通过反射查找字段、验证访问权限、字段类型和 volatile 属性,并计算字段偏移量;而在每次原子写操作时,仍然需要检查目标对象和新写入值的运行时类型。

而实际上真正执行 CAS 的操作可能只需要很短的时间,但是外围的反射安全检查也不能忽略,所以这就变成了运行时的性能和安全之间的博弈。

Google 用 AndroidX Benchmark 写了一个简单的测试,对比普通 AtomicReferencekotlinx.atomicfu.atomic

@RunWith(AndroidJUnit4::class)
class AtomicReferenceBenchmark {
​
    @get:Rule
    val benchmarkRule = BenchmarkRule()
​
    private val atomicReference =
        java.util.concurrent.atomic.AtomicReference(false)
​
    private val atomicRef =
        kotlinx.atomicfu.atomic(false)
​
    @Test
    fun atomicReferenceCompareAndSet() {
        benchmarkRule.measureRepeated {
            atomicReference.compareAndSet(true, false)
            atomicReference.compareAndSet(false, true)
        }
    }
​
    @Test
    fun atomicfuCompareAndSet() {
        benchmarkRule.measureRepeated {
            atomicRef.compareAndSet(true, false)
            atomicRef.compareAndSet(false, true)
        }
    }
}

在 Pixel 5、Android API 33 上,就算已经确保 AtomicReferenceFieldUpdater.compareAndSet 在预热阶段完成 JIT 编译,结果还是存在明显差距:

可以看到,问题主要集中在写入类操作上,简单读取几乎没有差异,但 CAS、交换和延迟写入通常会慢 2~4 倍,而且这组测试也排除了一个可能性:

ART 并没有在 JIT 或 AOT 阶段自动把这些反射检查完全消除,因为即使方法已经完成 JIT 编译,atomicfu 的 CAS 仍然慢 2.7 倍,说明这些检查确实构成了真实的运行时成本。

而这时候 R8 就起到作用了,R8 是一个全程序优化编译器,可以接收 Java 或 Kotlin 编译器产生的 JVM 字节码,通过分析整个应用及其依赖关系,最终生成优化后的 DEX。

也就是 R8 可以看到一些运行时看不到的静态信息,比如

static final AtomicReferenceFieldUpdater updater =
    AtomicReferenceFieldUpdater.newUpdater(
        Example.class,
        String.class,
        "data"
    );

对于 R8 来说,这里的信息几乎都是常量:

  • updater 属于 Example
  • 目标字段名称固定为 data
  • 字段类型固定为 String
  • updater 被保存在 static final 字段中
  • 调用位置访问的是确定的对象类型

所以既然这些条件在编译期已经能够证明,运行时就不需要在每次 compareAndSet 时重新检查,所以在这里 R8 所做的,本质上是将:

updater.compareAndSet(
    holder,
    expectedValue,
    newValue
);

改写为类似:

SyntheticUnsafe.UNSAFE.compareAndSwapObject(
    holder,
    Example.updater$offset,
    expectedValue,
    newValue
);

也就是原本的调用路径大概是:

AtomicReferenceFieldUpdater
        ↓
检查目标对象类型
        ↓
检查新写入值类型
        ↓
读取已经保存的字段 offset
        ↓
Unsafe 原子操作

然后现在优化后,路径变成:

编译期完成类型和字段验证
        ↓
直接读取字段 offset
        ↓
Unsafe 原子操作

也就是原子操作本身没有改变,减少的是每次调用之前重复执行的安全检查。

而实际上R8 的优化分为三个阶段,第一阶段是 Instrumentation,R8 首先在原有 updater 字段旁边增加一个字段偏移量:

static final long updater$offset =
    SyntheticUnsafe.UNSAFE.objectFieldOffset(
        Example.class.getDeclaredField("data")
    );

这个时候原来的 updater 不会立即删除,这样做的目的是为了让 R8 可以逐个分析调用点,能安全优化的调用使用 offset,对于无法证明安全的调用继续保留原始实现。

然后第二阶段 Replacement,对于每一个 updater 调用,R8 会检查几个条件:

  • updater 是否能够追踪到已经完成插桩的静态字段
  • holder 是否属于原来声明的持有类型或其子类
  • 新写入的值是否符合字段类型

只有这些条件全部成立,R8 才会把调用替换成 Unsafe ,而如果不能静态排除 updater 或 holder 为 null`,R8 还会插入对应的空值检查,从而保持原始 Java API 的行为。

然后第三阶段是 Clean-up,完成替换后,类中可能同时存在:

  • 原来的 updater 字段
  • 新生成的 offset 字段
  • 已优化和未优化的调用点

最后如果所有调用点都完成了优化,R8 就可以删除原始 updater;如果一个调用点都没有优化,那就删除新增的 offset;如果只有部分调用点可以优化,那两者都会被保留。

这里还有一个编译器层面的细节:普通的死代码消除不能随意删除 newUpdatergetDeclaredField,因为理论上它们可能抛出异常,所以 R8 必须利用前面静态分析得到的结论,明确证明这些初始化不会失败,才能安全地将其移除。

然后最终生成的代码大致会变成:

class Example {
    volatile String data = "";
​
    static final long updater$offset =
        SyntheticUnsafe.UNSAFE.objectFieldOffset(
            Example.class.getDeclaredField("data")
        );
​
    void update() {
        SyntheticUnsafe.UNSAFE.compareAndSwapObject(
            this,
            Example.updater$offset,
            "",
            "new"
        );
    }
}

完成优化以后,kotlinx.atomicfu 和显式使用的 AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater,在大部分基准测试中已经能够达到普通 AtomicReference 的性能。

甚至哦在部分测试中 atomicfu 还会更快一些,因为 atomicfu 还有自己的编译器插件,可以把一个原子属性直接内联为宿主对象中的字段,所以不需要额外创建一个独立的 AtomicReference 包装对象,这样既绕过了 FieldUpdater 的反射检查,也避免了额外对象分配。

例如普通写法可能需要两个对象:

class State {
    val value = AtomicReference<String>("")
}

而 atomicfu 可以在转换后保留为类似:

class State {
    @Volatile
    private var value: String = ""
}

随后再由 R8 把针对这个字段的 updater 调用转换成直接的 Unsafe 操作,所以最终结果不是说就简单地把 atomicfu 换回 AtomicReference,在这个基础上的收益还有:

  • 字段级原子更新
  • 更少的包装对象
  • 没有重复的反射安全检查
  • 直接的底层原子指令

当然,这里的两倍不是说所有 Kotlin 协程代码都会整体快 2 倍,这个 2 被主要是对应 Compose Runtime 的微基准:

升级到包含这项优化的新版 R8 后,LaunchedEffect 中启动和取消协程的耗时降低了一半。

所以如果一段协程的主要时间花在网络请求、数据库访问、图片解码或者复杂计算上,那么原子状态操作只占很小比例,整体任务不可能因此直接快 2 倍的,所以这个虽然说 2 倍,但是不是说一个可以很直观体验到的提升

最后,R8 的方案目前属于构建期优化,而 ART 团队也在尝试从虚拟机层面识别和优化类似的 Atomic*FieldUpdater 模式。

Google 就提到了,当应用面向 API 37 ,同时运行在支持新版 ART 的 Android 设备上时,运行时可能已经能够完成类似优化,相关协程基准在更新后的 JIT 中还观察到了约 15% 的性能改善。

链接

android-developers.googleblog.com/2026/07/how…