@AndroidDev: R8使Android上的Kotlin协程速度提升2倍 借助AGP 9.2.0,R8将优化大多数Atomic*FieldUpdater调用为Un…

X AI KOLs Timeline 工具

摘要

AGP 9.2.0中的R8将Atomic*FieldUpdater调用优化为Unsafe变体,使Android上的Kotlin协程速度提升高达2倍。这显著提升了Jetpack Compose的性能。

R8使Android上的Kotlin协程速度提升2倍 😎 借助AGP 9.2.0,R8将优化大多数Atomic*FieldUpdater调用为Unsafe变体,这些变体在常见操作上的性能提升2倍到4倍。 阅读技术深度解析 → https://t.co/zKNEyMbhtH https://t.co/f9Gys56Ly7
查看原文
查看缓存全文

缓存时间: 2026/07/27 19:56

R8 让 Android 上的 Kotlin 协程速度提升 2 倍 😎 借助 AGP 9.2.0,R8 会将大多数 Atomic*FieldUpdater 调用优化为 Unsafe 变体,常见操作的性能提升 2 到 4 倍。阅读深度技术解析 → https://t.co/zKNEyMbhtH https://t.co/f9Gys56Ly7


R8 如何让 Android 上的 Kotlin 协程速度提升 2 倍

来源:https://android-developers.googleblog.com/2026/07/how-r8-made-kotlin-coroutines-2x-faster.html

作者:Andrei Shikov,Android 工具包高级软件工程师;Jonathan Starup,R8 团队软件工程师

从 AGP 9.2.0 开始,R8 会将大多数 Atomic*FieldUpdater 调用优化为 Unsafe 变体,常见操作的性能提升 2 到 4 倍 (https://github.com/Kotlin/kotlinx.coroutines/issues/3950)。这对 kotlinx.atomicfu 库(它为 kotlinx.coroutines 实现原子操作)影响尤其显著,使得启动和取消协程的速度提升高达 2 倍。要获得这些好处,请将 AGP 更新到 9.2.0 或更高版本。

随着大多数 Android 应用选择 Kotlin 作为主要语言,kotlinx.coroutines 已成为异步编程的事实标准。该库提供了一种设计精良且结构化的方式来管理协程并发流,这种方式对 Kotlin 来说非常原生。Jetpack Compose 也不例外,它采用协程来管理指针事件、动画和其他交互。在撰写本文时,Compose 中的大多数并发 API 在底层调用 suspend 函数,并通过启动和/或取消协程来处理更新。

当 Compose 团队开始研究性能时,发现协程是许多在组合之外发生的操作的瓶颈。例如,创建和更新 Modifier.clickable 所花费的时间中,有 80% 被用于启动和取消处理 InteractionSource 更新的内部协程。基于这些观察,许多早期的性能工作都集中在从默认路径中移除协程,并将初始化延迟到必要之时。

协程的成本

在 Android 上分析函数内部行为的最简单方法是捕获 Android 运行时 (ART) 方法跟踪。ART 方法跟踪是一种记录应用执行流程的工具,可以精确显示哪些方法被调用、它们的顺序以及每个方法花费的时间,从而使开发人员能够识别性能瓶颈。对于空的 LaunchedEffect { } 调用,它的方法跟踪大致如下:

在 Perfetto UI 中可视化的 LaunchedEffect 方法跟踪

上面的方法跟踪可以分为三个部分:

  • 初始化一个新协程
  • 启动协程
  • 完成协程(因为它立即退出)

取消 LaunchedEffect 类似于正常完成,只是还会创建一个 CancellationException。从上面的性能分析中,一个立即引起怀疑的事情是频繁调用 java.util.concurrent.AtomicReferenceFieldUpdater(带有 j… 标签的紫色或绿色方块)。虽然每次调用都相对较快,但频率令人担忧;任何在多次调用中分散的不可忽略的开销都可能累积成明显的性能回归。放大某个调用会发现,大部分时间都花在了……反射检查上?

LaunchedEffect 初始化期间 AtomicReferenceFieldUpdater.get 方法跟踪的近距离观察

协程实现了一个无锁的父子关系树结构,这使得结构化并发成为可能。事实证明,kotlinx.atomicfu 库使用一个众所周知的 JVM 原语 AtomicReferenceFieldUpdater 来实现无锁原子操作。该更新器在运行时使用类引用和字段名来执行原子操作,并且必须运行多个反射安全检查来确保字段存在且可访问。协程中的每个操作(启动、挂起、取消、完成)至少调用一次原子操作,因此如果它很慢,协程的性能就不会好。

探究 AtomicReferenceFieldUpdater

但不要过早下结论。实际上,AtomicReferenceFieldUpdater 在 JVM 上已经优化得很好超过 10 年了 (https://shipilev.net/blog/2015/faster-atomic-fu/),而方法跟踪可能会捕获到那些被虚拟机级优化完全消除的开销:即时编译 (JIT) 或预先编译 (AOT)。为了验证性能,我们编写几个基准测试来衡量 kotlinx.atomicfu 中的原子引用与 java.util.concurrent.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 atomicReference_compareAndSet() {
        benchmarkRule.measureRepeated {
            atomicReference.compareAndSet(true, false)
            atomicReference.compareAndSet(false, true)
        }
    }

    @Test
    fun atomicRef_compareAndSet() {
        benchmarkRule.measureRepeated {
            atomicRef.compareAndSet(true, false)
            atomicRef.compareAndSet(false, true)
        }
    }

    /* 测量上述方法跟踪中的其他方法 */
}

在 Pixel 5(确保在预热期间 AtomicReferenceFieldUpdater#compareAndSet 被 JIT 编译)上运行此基准测试,在 Pixel 5(API 33)上得到以下结果:

50.7 ns atomicReference_compareAndSet
135 ns atomicRef_compareAndSet

测量结果确认了差距,kotlinx.atomicfu 版本明显慢了约 2.7 倍。这证实 ART 没有执行任何隐藏优化,反射访问检查在运行时增加了实际开销。回顾最初的方

法跟踪,AtomicReferenceFieldUpdater 执行的唯一有意义的工作是内部调用 Unsafe.getObjectVolatile,该调用实际执行底层的原子操作。在大多数情况下,更新器初始化器是静态的,并且可以根据周围类的结构证明总是正确的。因此,可以在编译期间静态分析大多数 AtomicReferenceFieldUpdater 的用法,并将其替换为内部的 Unsafe 变体。恰好 Android 构建工具链拥有自己的优化编译器,可以精确地做到这一点。

使用 R8 进行优化

Atomic*FieldUpdater 类支持细微的、动态的以及基于反射的用法,但通常以静态明显的方式使用。这既解释了较差的基线性能,也解释了优化的需求。R8 是一个全程序优化编译器,非常适合识别这些简单模式,从而消除反射安全检查的开销。R8 接收 Java 或 Kotlin 编译器生成的 JVM 字节码,但为了便于阅读,这些示例以 Java 语法呈现。这就是为什么 AtomicReferenceFieldUpdater 没有类型参数。

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

    void example() {
        // ...
        updater.compareAndSet(this, "", "new");
        // ...
    }
}

基本示例创建了一个静态 final 更新器,该更新器访问一个 volatile 字段,并使用简单的常量参数表示持有者、类型和字段名。使用的反射是完全透明的。很明显,这个更新器引用了一个有效字段,并且更新器创建的位置对该字段具有有效访问权限。本质上,Atomic*FieldUpdater 是字段偏移量和 Unsafe 调用的包装器。优化最佳情况是将更新器字段替换为偏移量字段,并将更新器调用替换为对 Unsafe 的调用。

优化 Atomic*FieldUpdater

该优化分为三个部分实现:插桩、替换和清理。

插桩

第一步是在更新器字段旁边引入偏移量字段,以便通过 Unsafe 调用进行直接访问。

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

该字段通过反射访问,并使用 Unsafe 提取类上的字段偏移量。如果你忽略反射验证,这段代码代表了 Atomic*FieldUpdater 的内部实现。相反,编译器会静态跟踪更新器的持有者类型和 volatile 字段的字段类型。请注意,原始字段及其初始化保持不变。优化过程乐观地插桩、优化使用,然后进行清理。这是一种简单的实现方法,但也允许对更新器字段进行部分优化,即某些使用保持不变,而其他使用得到优化。

替换

在编译器中,经过适当的并发连接点之后,我们得到了一个插桩后的更新器字段列表。这意味着我们可以根据几个条件单独优化每个调用点。考虑一个示例调用:

updater.compareAndSet(holder, expectedValue, newValue);

Atomic*FieldUpdater 需要的条件如下:

  • updater 是否来自一个已插桩的字段?也就是说,静态分析能否将对象的值追溯到对已插桩更新器字段的读取?
  • holder 是否与最初定义的持有者类型相同,或是其子类?
  • newValue 是否与最初定义的字段类型相同,或是其子类?

如果满足所有条件,则该调用将被替换为对 Unsafe 的调用,无需进行任何反射检查。

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

这个新调用更快、更简单,但在处理 updaterholder 的空值方面与原始调用有所不同。除非静态排除了空值可能性,否则两者都会插入空检查。

清理

此时,持有类既包含原始更新器字段,也包含新的偏移量字段,以及可能使用两者之一的调用点。如果没有调用点被优化,则应移除偏移量字段;如果所有调用点都被优化,则应移除更新器字段。在这两种情况下,初始化调用也应被删除。删除未使用字段和移除死代码已经在编译器中完成,但在这里删除初始化代码需要更多技巧。对 newUpdatergetDeclaredField 的调用可能有副作用,因为它们可能抛出异常(并且它们的实现也是未知的,因为它取决于 API 版本)。这意味着通过通用优化,它们不能安全地被删除。因此,这种清理需要显式考虑插桩后的字段,因为静态已知这些字段不会引发异常。最后,上面展示的简单更新器示例在优化后如下所示:

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

    void example() {
        // ...
        SyntheticUnsafe.UNSAFE.compareAndSwapObject(this, Example.updater$offset, "", "new")
        // ...
    }
}

结果

经过这些优化后,kotlinx.atomicfu 以及大多数显式使用 AtomicInt/Long/ReferenceFieldUpdater 的地方,在使用 R8 应用后现在与 AtomicReference 的性能相匹配。事实上,在某些基准测试中甚至更快;kotlinx.atomicfu 有一个编译器插件,可以将 atomic 实例内联到字段中,从而减少创建原子更新字段所需的分配。

Jetpack Compose 是这项工作的主要受益者。Compose 运行时有一些微基准测试,它们非常密切地跟踪协程性能,以便及早发现性能回归。当基准测试更新到新版本的 R8 时,我们注意到在 LaunchedEffect 中启动和取消协程时性能提升了 2 倍!

基准测试图显示了在 LaunchedEffect 中启动和取消协程所花费的时间(越低越好)。图中的变化对应于一次 R8 更新,展示了 2 倍的性能提升。

除此之外,ART 团队正在虚拟机级别原生实现这些优化。如果你的应用目标为 API 36,并且在较新版本的 Android 上运行,那么你的设备可能已经在以类似方式优化协程。上述协程基准测试在最近版本的 ART 中,经过 JIT 更新后观察到性能提升了约 15%。当升级到 AGP 9.2.0 或直接使用 R8 9.2.0 时,你的应用将默认获得此优化。更多信息,请参阅 D8 dexer 和 R8 shrinker (https://r8.googlesource.com/r8/+/refs/heads/main/README.md#replacing-r8-in-agp)。

相似文章