美文网首页
深入浅出 Compose Compiler(4) 智能重组与 $

深入浅出 Compose Compiler(4) 智能重组与 $

作者: 我爱田Hebe | 来源:发表于2022-12-05 09:57 被阅读0次

    前言

    重组是 Compose 的一个重要特征,重组过程中 Composable 函数会对参数进行比较,如果参数没有发生变化则会跳过重组,即所谓的“智能”的重组。但是这个参数的比较不全是 runtime 的事情,Compiler 也会参与其中。

    @Composable
    fun Foo(bar: String) {
        Text(bar)
    }
    

    我们拿上面这个非常简单的 Composable 作例子,它经过编译后变成下面这样(反编译后的伪代码,看着更清晰):

    @Composable 
    fun Foo(bar: String, $composer: Composer<*>, $changed: Int) {
        $composer.startRestartGroup(405544596)
        var $dirty = $changed
        if ($changed and 14 === 0) {
            $dirty = $dirty or if ($composer.changed(x)) 2 else 4
        }
        if ($dirty and 11 !== 2 || !$composer.skipping) {
            Text(bar)
        } else {
           $composer.skipToGroupEnd()
        }
        $composer.endRestartGroup().updateScope {
            Foo(bar, $changed)
        }
    }
    

    大多数的 Composable 函数编译后都会被包装在 startRestartGroup/endRestartGroup 中,让当前函数有了重组的能力,可以看到函数结尾处 updateScope 注册的 lambda 就是用于重组时的递归调用。

    为实现“智能”的重组,函数执行允许 skip ,此时会直接执行下面的 else 分支,skipToGroupEnd() 将对 SlotTable 的遍历推进到最后:

     if ($dirty and 11 !== 2 || !$composer.skipping) {
            Text(bar)
        } else {
           $composer.skipToGroupEnd()
        }
    

    这里 if 条件中依赖对 $dirty 的判断,而 $dirty 来自 $changed。 那这些变量代表什么呢?另外,代码中出现了好多魔数,诸如 14 ,11, 2, 4 之类的,这些又代表什么呢?本文就来讨论一下这些内容。

    $changed 与 ParamState

    Composable 经过编译后函数签名会发生变化。除了新增 $composer 以外,还会添加 <math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>c</mi><mi>h</mi><mi>a</mi><mi>n</mi><mi>g</mi><mi>e</mi><mi>d</mi><mtext>参数。前缀</mtext><mi mathvariant="normal">‘</mi></mrow><annotation encoding="application/x-tex">changed 参数。前缀 </annotation></semantics></math>changed参数。前缀‘告诉我们这些都是 Compiler 的产物。$changed 可以为参数提供额外的辅助信息,这些信息辅助运行时的参数比较,减少运行时的判断,提升性能。

    $changed 是一个 Int 类型 (32bits),最低位是保留位,用来表示是否强制重组。然后从低到高,每3bits 代表一个参数的信息,这样 32 bit 至多可以承载 10 个参数的信息 ( 10 * 3 + 1 = 31 )。如果 Composable 参数超过 10 个,那么相应地会在签名中插入 $changed, $changed1 ... 以此类推。

    这 3bits 信息被称为 ParamState,它是一个 Enum,有 5 个取值所以需要占用 3 个 bit

    enum class ParamState(val bits: Int) {
    
        Uncertain(0b000),
    
        Same(0b001),
    
        Different(0b010),
    
        Static(0b011),
    
        Unknown(0b100)
    }
    
    • Uncertain(0b000) :参数最新值与最后一次重组的值比较,是否有变化不确定
    • Same(0b001) :参数最新值与最后一次重组的值比较,没有发生变化
    • Different(0b010) :参数最新值与最后一次重组的值比较,发生了变化
    • Static(0b011): 参数是一个静态常量
    • Unknown(0b100): 3bits 的最高位表示参数类型是否稳定,1 表示不稳定

    Composable 函数在调用处会根据编译期静态分析的结果,设置最适当的 $changed 值。 例如下面代码中, App 中传入 Foo 的是一个静态值,所以 $changedStatic(0b011)

    @Composable
    fun App() {
        Foo("Hello world!")
    }
    
    //编译后:
    @Composable
    fun MyApp($composer: Composer<*>) {
        Foo("Hello world!", $composer, 0b0110)  //static(0b011) shl 1 + 0b1
    }
    

    在例如下面代码中,App 中传入 Foo 是一个变量,所以编译期无法确定类型,传入 Uncertain(0b000)

    var str = ""
    @Composable
    fun App() {
        Foo(str)
    }
    
    //编译后:
    @Composable
    fun MyApp($composer: Composer<*>) {
        Foo("Hello world!", $composer, 0b0000)  //Uncertain(0b000) shl 1 + 0b1
    }
    

    $dirty 与 ParamState.Uncertain

    $changedUncertain 时,没法确定此次参数有没有变化,此时就需要运行时进行参数比较,经比较后的到一个确定结果 - 要么是 Same(0b001) 要么 Different(0b010),并更新到 $dirty 对应的字段。

    var $dirty = $changed
    if ($changed and 14 === 0) {
        $dirty = $dirty or if ($composer.changed(x)) 4 else 2
    }
    

    14 二进制是 0b1110,所以上面 if 语句中 $changed and 14 === 0 的条件,只有 $changedUncertain (0b000 左移一位) 时才成立。

    当命中 Uncertain 时,调用 $composer.changed(x) 拿当前参数与 SlotTable 中的记录进行比较,如果有变化则则返回 false, 并将最新的参数存入 SlotTable。因此 4 就是 0b010 左移 1 位的结果,对应 Different; 同理,2 对应的就是 Same

    参数比较的结果会更新到 $dirty 用于后续判断是否参与重组。这里可能有人会问为什么要用 or 进行更新,直接赋值不就好了? 别忘了 $dirty$changed 至多可以承载十个参数状态,我们这个例子只有一个参数看不出来 or 的意义,当有多个参数,就需要 or 去合并多个参数状态了。

    ParamState.Same 与 ParamState.Different

    前面讲了,一个 Uncertain 状态经过比较可以转换为 SameDifferent。如果 $changed 初始就是 SameDifferent,则意味着要么跳过重组,要么参与重组,总之行为是 Certaiin 的,因此需再进行参数比较了,参数值也不必存入 SlotTable 了,这样可以节省一些比较的开销以及 SlotTablle 的内存。

    更新后的 $dirty 用来判断是否参与重组:

        if ($dirty and 11 !== 2 || !$composer.skipping) {
            Text(bar)
        } else {
           $composer.skipToGroupEnd()
        }
    

    11 二进制表示是 0b1011, 2 是 0b10, 所以只有 DifferentUnKnown 符合条件。

    • Different : 0b0100 and 0b1011 != 2
    • Same:0b0010 and 0b1011 = 2
    • Static: 0b0110 and 0b1011 = 2
    • UnKnown:0b1000 and 0b101 != 2

    Different 会对函数体进行重组,SameStatic 则跳过重组。 Unknown 虽然也符合条件,但是 Compiler 针对类型稳定性有其他优化,后文会看到。

    ComposableFunctionBodyTransformer

    上面的这些代码生成逻辑都是在 ComposableFunctionBodyTransformer 中实现的,这是 Compose Compiler 中最复杂的一个文件,今后我们再慢慢介绍,这里只看一下 $changed 的代码的生成部分,主要在 visitRestartableComposableFunction 函数内:

    private fun visitRestartableComposableFunction(
        declaration: IrFunction,
        scope: Scope.FunctionScope,
        changedParam: IrChangedBitMaskValue,
        defaultParam: IrDefaultBitMaskValue?
    ): IrStatement {
    
        //...
    
        //是否可以跳过重组
        canSkipExecution = buildPreambleStatementsAndReturnIfSkippingPossible(
            ...
        )
    
        // if it has non-optional unstable params, the function can never skip, so we always
        // execute the body. Otherwise, we wrap the body in an if and only skip when certain
        // conditions are met.
        val dirtyForSkipping = if (dirty.used && dirty is IrChangedBitMaskVariable) {
            skipPreamble.statements.addAll(0, dirty.asStatements())
            dirty
        } else changedParam
    
        val transformedBody = if (canSkipExecution) {
    
            //是否应该执行重组
            var shouldExecute = irOrOr(
                dirtyForSkipping.irHasDifferences(scope.usedParams),
                irNot(irIsSkipping())
            )
    
            //...
    
            //生成执行重组或跳过重组的 if...else 代码块
            irIfThenElse(
                condition = shouldExecute,
                thenPart = irBlock(
                    statements = bodyPreamble.statements + transformed.statements
                ),
                // Use end offsets so that stepping out of the composable function
                // does not step back to the start line for the function.
                elsePart = irSkipToGroupEnd(body.endOffset, body.endOffset),
                startOffset = body.startOffset,
                endOffset = body.endOffset
            )
        } else irComposite(
            statements = bodyPreamble.statements + transformed.statements
        )
    
        //...
    }
    

    canSkipExecution 表示是否可以跳过重组,党可以跳过重组时,生成下面这样的 if ... else 代码:

        if ($dirty and 11 !== 2 || !$composer.skipping) {
            Text(bar)
        } else {
           $composer.skipToGroupEnd()
        }
    

    irIfThenElse 用来生成 if...else 代码, shouldExecute 是 if 里的 $dirty and 11 !== 2 || !$composer.skipping 判断。 thenPartelsePart 分别生成对应花括号里的代码。

    这段代码告诉我们,如果 canSkipExecution 为 false,压根就不会生成上面的 if...else 的判断逻辑,一定会执行 Text(bar). 那么 canSkipExecution 是如何被赋值的呢?我们从 buildPreambleStatementsAndReturnIfSkippingPossible 里找一下实现

    parameters.forEachIndexed { slotIndex, param ->
        val stability = stabilityOf(param.varargElementType ?: param.type)
    
        stabilities[slotIndex] = stability
    
        val isRequired = param.defaultValue == null
        val isUnstable = stability.knownUnstable()
        val isUsed = scope.usedParams[slotIndex]
    
        //...
    
        if (isUsed && isUnstable && isRequired) {
            // if it is a used + unstable parameter with no default expression, the fn
            // will _never_ skip
            mightSkip = false
        }
    }
    

    buildPreambleStatementsAndReturnIfSkippingPossible 最终返回的是上面的 mightSkip。也就是说当 Composable 的函数参数中,有任何一个参数是 isUsed && isUnstable && isRequired,即 参数是不稳定类型、且没有默认值而且函数体中被使用,则当前 Composable 的重组就不应该跳过,无需生成 skipToGroupEnd 相关的 if...else 逻辑,减少运行开销和产物体积。

    我们做个一个实验验证一下:

    data class Bar(var str: String)
    
    @Composable
    fun Foo(bar: Bar) {
        Text(bar.str)
    }
    

    上面的 Bar 就是一个不稳定类型。因此编译后的代码如下:

    @Composable 
    fun Foo(bar: Bar, $composer: Composer<*>, $changed: Int) {
        $composer.startRestartGroup(405544596)
        Text(bar.str)
        $composer.endRestartGroup().updateScope {
            Foo(bar, $changed)
        }
    }
    

    果然,$dirty 以及 skipToGroupEnd 相关的逻辑都没有了,100% 会执行重组。但是如果 Foo 中没有对依 bar 的读取,则即使 Bar 是不稳定类型,也会生成 skipToGroupEnd 的代码。

    Bar 的类型稳定性来自 stabilityOf,那么编译器怎么决定类型是是否稳定呢,这个留着我们下一篇文章再做介绍。

    最后

    最后做一个总结:Compose Compiler 编译期提为参数提供了 ParamState 信息,可以减少无谓的参数比较,提升重组的性能。我们用下面的图做一个收尾:

    作者:fundroid
    链接:https://juejin.cn/post/7171253940087390221

    相关文章

      网友评论

          本文标题:深入浅出 Compose Compiler(4) 智能重组与 $

          本文链接:https://www.haomeiwen.com/subject/cfgbfdtx.html