R8 优化:Method Outlining

原文出自 jakewharton 关于 D8 和 R8 系列文章第十篇。

  • 原文链接 : R8 Optimization: Method Outlining
  • 原文作者 : jakewharton
  • 译者 : 小伟

近来我在 he economics of generated code 文章中讨论了优化自动生成的代码,在人工编写过程中是非常不值得的。虽然文章中的代码是我过去工作中写的,但是也做了一些新的改动。

[Moshi](https://github.com/square/moshi/) 中提议的一项改变是通过 StringBuilder 来进行字符串的拼接。在 JSON 模型中的每个非空属性都会产生一个异常来确保读取的数据非空。

 name = stringAdapter.fromJson(reader) ?:
     throw JsonDataException(
-        "Non-null value 'name' was null at ${reader.path}")
+        StringBuilder("Non-null value '").append("name")
+            .append("' was null at ").append(reader.path).toString())

当非空属性缺少默认值且 JSON 中没有值时,会生成第二个异常。

 return Person(
   name = name ?: throw JsonDataException(
-      "Required property 'name' missing at ${reader.path}"),
+      StringBuilder("Required property '").append("name")
+          .append("' missing at ").append(reader.path).toString()),

上面的两个情况是进行这个提议的原因。

每个属性都会产生这些异常,这就意味着假如你有 10 个这样的属性会产生 20 个这样的异常,最终会导致创建很多 StringBuilder 对象。

所以给 [Moshi](https://github.com/square/moshi/) 的一个提议是可以通过提取一个包含四个参数的 (prefix, name, suffix, path) 的私有方法来减少字节码的创建量。然而我们不是生成一个方法,因为它最终会由于 R8 优化降低 APK 的大小。让我们找出原因。

1. 典型例子

与直接使用 MoshikaptKotlin 生成不同,使用具有代表性的示例更容易。首先,我们需要一些 JSON 模型对象。为了要求以上两种 StringBuilder 用法,每个属性都有一个非空类型,并且没有默认值。

data class User(
  val id: String,
  val username: String,
  val displayName: String,
  val email: String,
  val created: OffsetDateTime,
  val isPublic: Boolean
)

data class Tweet(
  val id: String,
  val userId: String,
  val content: String,
  val created: OffsetDateTime
)

当使用 [Moshi](https://github.com/square/moshi/) 时,这个类型会被声明为 @JsonClass 注解,利用注解处理器生成代码。然后使用 [Moshi](https://github.com/square/moshi/) 中的 JsonReader 处理每个属性。我们可以使用 Android 内置的 JsonReader 手工编写生成的代码。

object TweetParser {
  fun fromJson(reader: JsonReader): Tweet {
    var id: String? = null
    // other properties…

    reader.beginObject()
    while (reader.peek() != JsonToken.END_OBJECT) {
      when (reader.nextName()) {
        "id" -> id = reader.nextString() ?:
            throw IllegalStateException(
                StringBuilder("Non-null value '").append("id")
                    .append("' was null at").append(reader).toString())
        // other properties…
        else -> reader.skipValue()
      }
    }
    reader.endObject()

    return Tweet(
      id = id ?: throw IllegalStateException(
          StringBuilder("Required property '").append("id")
             .append("' missing at ").append(reader).toString()),
      // other properties…
    )
  }
}

在上面的例子中,我们只演示了 Tweet 中的一个属性,你可以使用同样的方式添加别的属性。如果我们使用 kotlincD8 进行编译打包,通过 dexdump 查看打包后的文件,可以看到 StringBuilder 被多次重复利用。

0181: new-instance v1, Ljava/lang/IllegalStateException;
0183: new-instance v4, Ljava/lang/StringBuilder;
0185: invoke-direct {v4, v3}, Ljava/lang/StringBuilder;.<init>:(Ljava/lang/String;)V
0188: invoke-virtual {v4, v12}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
018b: invoke-virtual {v4, v2}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
018e: invoke-virtual {v4, v0}, Ljava/lang/StringBuilder;.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
0191: invoke-virtual {v4}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String;
0194: move-result-object v0
0195: invoke-direct {v1, v0}, Ljava/lang/IllegalStateException;.<init>:(Ljava/lang/String;)V
0198: check-cast v1, Ljava/lang/Throwable;
019a: throw v1

通过这种方式比生成一个单独的字符串常量轻量级很多,但这仍然是一种浪费。在每种解析器类型中使用此代码生成一个方法将减少其影响。那我们为什么不选择呢?

2. Outlining

在本系列的多数文章中,我们提到使用 inlining 来进行优化。这种优化是当一个方法足够小并且不被频繁调用的时候,可以将该方法的实现直接放到调用处的方法中并且在字节码中删除该方法。Outlining 是一种相反的优化,在这种优化中,公共字节码序列被识别并提取到共享方法中。

在使用 R8 编译前,我们先添加一个 main 方法。

fun main() {
  println(TweetParser.fromJson(JsonReader(StringReader(""))))
  println(UserParser.fromJson(JsonReader(StringReader(""))))
}

我们不运行上面的代码,所以我们用 "" 来模拟数据,只需要关心 R8 优化的结果。

$ cat rules.txt
-keepclasseswithmembers class * {
  public static void main(...);
}
-dontobfuscate

$ java -jar r8.jar \
      --lib $ANDROID_HOME/platforms/android-28/android.jar \
      --release \
      --output . \
      --pg-conf rules.txt \
      *.class

D8 生成的代码相比,R8 的输出显示了异常代码非常不同的画面。

0181: new-instance v1, Ljava/lang/IllegalStateException;
-0183: new-instance v4, Ljava/lang/StringBuilder;
-0185: invoke-direct {v4, v3}, Ljava/lang/StringBuilder;.<init>:(Ljava/lang/String;)V
-0188: invoke-virtual {v4, v12}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
-018b: invoke-virtual {v4, v2}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
-018e: invoke-virtual {v4, v0}, Ljava/lang/StringBuilder;.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
-0191: invoke-virtual {v4}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String;
+0183: invoke-static {v3, v12, v2, v0}, Lcom/android/tools/r8/GeneratedOutlineSupport;.outline0:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;
 0194: move-result-object v0
 0195: invoke-direct {v1, v0}, Ljava/lang/IllegalStateException;.<init>:(Ljava/lang/String;)V
-0198: check-cast v1, Ljava/lang/Throwable;
 019a: throw v1

outlining 的优化中是被到 StringBuilder 被重复多次利用,字节码片段被放到 com.android.tools.r8.GeneratedOutlineSupport 类中的 outline0 方法中,所以出现这个字节码的片段都被新提取的方法替代。我们看看新方法生成的字节码。

[000eb4] com.android.tools.r8.GeneratedOutlineSupport.outline0:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;
0000: new-instance v0, Ljava/lang/StringBuilder
0002: invoke-direct {v0, v1}, Ljava/lang/StringBuilder;.<init>:(Ljava/lang/String;)V
0005: invoke-virtual {v0, v2}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder
0008: invoke-virtual {v0, v3}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder
000b: invoke-virtual {v0, v4}, Ljava/lang/StringBuilder;.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder
000e: invoke-virtual {v0}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String
0011: move-result-object v1
0012: return-object v1

R8 已经创建了我们正在考虑添加自己的助手方法!我在示例中特别选择使用两种类型,它们一起具有 10 个属性,从而产生 20StringBuilder 。这是 R8Outlining 操作优化的下限要求。重复的字节码也必须在 3 - 99 个字节之间。

如果moshi生成一个私有的StringBuidler帮助器方法,那么我们的示例仍然有两个副本。在R8介入并消除helper方法的重复之前,您需要20个JSON模型对象。通过选择复制StringBuilder代码,在R8大纲开始之前,任何数量的JSON模型对象中只需要20个属性。一旦发生这种情况,不管使用多少JSON模型对象和属性,我们只需支付一次代码。

3. 总结

Outlining 对于重复生成的代码非常有效。在上面的例子中,您可以避免将一个助手函数放在运行时库中,而当它重复足够多时,您可以依靠 R8 来消除重复的字节码。而且,由于 R8 正在进行整个程序分析,不相关的代码(恰好具有相同的字节码模式)参与了重复数据消除。

考虑到它如何与 Kotlin 的内联函数修饰符交互也很有趣。使用内联函数越多(尤其是在其他内联函数内部调用内联函数),R8 就越有可能将一些函数体重新概括为常规方法。确保您正在使用 inline 来处理类似于已具体化的泛型的事情,或者避免按预期分配 lambda 对象。

在上一篇关于 R8 的文章中,我取笑了下一篇文章(又称本篇文章)将涉及创建 const 类字节码的优化。在写了本系列以外的关于生成代码的两篇文章并讨论了moshi的变化之后,然而,对于包含大纲的内容来说,这是一个自然的进展。在概述的方式之外,下一个R8帖子将回到生产const类字节码的轨道上。

你可能感兴趣的:(Android进阶)