iprof 文件格式

注意:本文档假定读者熟悉 GraalVM 配置文件引导优化

为了使用配置文件引导优化(PGO)构建优化的本地镜像,需要向 native-image 工具提供分析数据,这些数据通过在已插桩镜像上执行工作负载收集。这些分析信息以 JSON 对象的形式存储在扩展名为 .iprof 的文件中。本文档概述了 iprof 文件格式的结构和语义。

结构 #

iprof 文件所使用的 JSON 格式的完整模式可以在 iprof-v1.0.0.schema.json 文档中找到。此 JSON 模式完整定义了 iprof 文件格式,可用于验证任意 iprof 文件的结构。

一个最小的有效 iprof 文件由一个包含 3 个字段的 JSON 对象组成:typesmethodsversion。以下是当前版本(1.0.0)的最小有效 iprof 文件。

{
  "version": "1.0.0",
  "types": [],
  "methods": []
}

除了这些字段,iprof 文件还可以选择包含其他提供各种运行时配置文件信息的字段。以下是一个完全填充的 iprof 文件(版本 1.0.0)的示例,其中每个字段的实际内容已替换为 ...

{
  "version": "1.0.0",
  "types": [...],
  "methods": [...],
  "monitorProfiles": [...],
  "virtualInvokeProfiles": [...],
  "callCountProfiles": [...],
  "conditionalProfiles": [...],
  "samplingProfiles": [...]
}

本文档的后续章节将提供一个启发性示例,并更详细地描述 iprof 文件的每个字段。

启发性示例 #

考虑以下计算并打印前 10 个斐波那契数的 Java 程序。

import java.io.*;

public class Fib {

    private int n;

    Fib(int n) {
        this.n = n;
    }

    synchronized void fibonacci() {
        int num1 = 0, num2 = 1;

        for (int i = 0; i < n; i++) {
            try {
                Thread.sleep(10);
            } catch (Exception e) {
                // ignored
            }
            // Print the number
            System.out.print(num1 + " ");

            // Swap
            int num3 = num2 + num1;
            num1 = num2;
            num2 = num3;
        }
    }

    public static void main(String args[])
    {
        new Fib(10).fibonacci();
    }
}

此应用程序将用作解释 iprof 文件结构和语义的示例。要从此应用程序生成 iprof 文件,请将其保存为 Fib.java 并逐一执行以下命令:

javac Fib.java
native-image --pgo-instrument -cp . Fib
./fib

fib 终止后,工作目录中应该有一个 default.iprof 文件。

注意:本文档中显示的确切值在您的运行中可能会有所不同,因此,如果您尝试验证本文档中的说法,则需要理解这些值的语义。

版本 #

本节描述了 iprof 文件格式的版本。iprof 格式采用语义版本控制方案(即 major.minor.patch),以确保 iprof 文件的任何使用者都能知道应预期哪些信息以及以何种格式。主要版本在发生重大更改(例如,信息编码的新方式)时更新,次要版本在发生非重大更改(例如,在顶级 JSON 对象中添加新的可选字段)时更新,而补丁版本在发生不应影响任何客户端的次要修复时更新。当前 iprof 文件格式的版本是 1.0.0,这可以在示例应用程序生成的 iprof 文件中看到。

...
    "version": "1.0.0",
...

类型 #

iprof 文件中的此条目包含理解配置文件所需的所有类型信息。这包括但不限于原始类型、声明已分析方法(profiled methods)的类型,以及这些方法签名中提到的任何类型。

iprof 文件中的 types 字段是一个 JSON 对象数组,其中数组的每个元素代表一个类型。

每个类型都通过其完全限定名(存储在类型对象的 name 字段中)唯一标识。iprof 格式依赖用户不要在不相关的上下文中使用 iprof 文件,例如,在一个应用程序上收集配置文件并将其应用于另一个具有完全不同但共享完全限定名的类型的应用程序。此外,本节中的每个类型都通过一个唯一的 ID(一个整数值)进行标识。此 ID 特定于一个 iprof 文件,这意味着,例如,一个 iprof 文件中 ID 为 3 的类型可能与另一个 iprof 文件中 ID 为 3 的类型完全不同。

这些 ID 在整个 iprof 文件中用于引用类型(例如,方法的返回类型,参见方法部分)。这样做是为了减小 iprof 文件的占用空间,因为每次都引用类型的完全限定名会显著增加其大小。

请参见下面斐波那契示例 iprof 文件的类型数组中的部分值。

...
    "types": [
        {
            "id": 0,
            "name": "boolean"
        },
        {
            "id": 1,
            "name": "byte"
        },
        {
            "id": 2,
            "name": "short"
        },
...
        {
            "id": 8,
            "name": "void"
        },
        {
            "id": 9,
            "name": "java.lang.Object"
        },
        {
            "id": 10,
            "name": "Fib"
        },
...
        {
            "id": 629,
            "name": "java.lang.System"
        },
...
        {
            "id": 4823,
            "name": "[Ljava.lang.String;"
        },
...
    ]
...

每个条目都包含之前解释过的两个组件:idname。原始类型(例如,booleanbyteshort)、启发性示例中声明的 Fib 类,以及示例中使用的任何其他类型(例如,java.lang.System 用于调用 print 方法)都在列表中。

注意:此处仅显示了部分类型,因为尽管我们的启发性示例非常小,但 iprof 文件总共包含了 5927 种类型,其中大部分来自 JDK。

方法 #

iprof 文件中的此条目包含理解配置文件所需的所有方法信息。这包括但不限于应用程序插桩构建期间所有被插桩(instrumented)的方法。它也可以包括未被插桩的方法,例如,如果配置文件通常通过采样而非插桩来收集。

与类型一样,方法(在一个 iprof 文件内)通过一个整数 ID 唯一标识,此 ID 在整个 iprof 文件中用于引用该方法。与类型不同,它们不能仅通过其名称进行全局标识,尽管其名称也存储在 iprof 文件中。因此,iprof 文件还存储了方法的签名信息。此信息存储在方法对象的 signature 字段中,并被建模为一个整数数组。这些整数值中的每一个都是一个类型的 ID,该类型必须存在于 iprof 文件的 types 条目中。此数组中值的顺序很重要:第一个值是声明该方法的类型,第二个值是该方法的返回类型,其余值是该方法的按序参数类型。请注意,接收者类型不属于签名的一部分。

考虑示例应用程序 iprof 文件中的以下方法选择

    "methods": [
...
        {
            "id": 19547,
            "name": "main",
            "signature": [
                10,
                8,
                4823
            ]
        },
...
        {
            "id": 19551,
            "name": "fibonacci",
            "signature": [
                10,
                8
            ]
        },
    ]
...

每个方法对象由三个组件组成:idnamesignature。名为 main 的方法的 ID 为 19547signature 字段中的值是 1084823。这得出结论,main 方法在一个 ID 为 10 的类型中声明,检查“类型”部分给出的示例,你会发现它确实是 Fib 类。第二个值标识方法的返回值,即 void(ID 为 8)。最终值(4823)是 main 方法单个参数的类型 ID——一个 java.lang.String 数组。

调用计数配置文件 #

本节介绍 iprof 文件中最简单的配置文件之一——调用计数配置文件。这些配置文件包含方法在所有内联上下文(inlining contexts)中执行的次数信息。这意味着 iprof 文件不仅包含每个已插桩方法的单独计数,还包含该方法被内联到另一个方法中的每种情况的计数。此内联信息称为“部分调用上下文”或简称“上下文”,理解此概念对于理解 iprof 文件中数据存储量至关重要。

部分调用上下文 #

部分调用上下文描述了代码中特定位置的多个调用方方法层级,并且可以为每个部分调用上下文分配不同的配置文件。部分调用上下文的长度可以任意选择,也可以始终指定一个没有调用方的单个代码位置(即始终使用与上下文无关的代码位置)。

这些上下文标识代码中的特定位置,以便将相关配置文件应用于正确的位置。从概念上讲,上下文只是一个有序的方法和字节码索引(BCI)列表,表明配置文件与方法 a 在 BCI x 上的调用相关,该调用被内联到方法 b 中,并且在 BCI y 处进行了调用,依此类推。

考虑以下 Java 示例程序,特别是程序的调用图。

public class EvenOrOddLength {

    public static void main(String[] args) {
        printEvenOrOdd(args[0]);
    }

    private static void printEvenOrOdd(String s) {
        if (s.length() % 2 == 0) {
            printEven();
        } else {
            printOdd();
        }
    }

    private static void printEven() {
        print("even");
    }

    private static void printOdd() {
        print("odd");
    }

    private static void print(String s) {
        System.out.println(s);
    }
}

此程序具有以下不完整的调用图,其中方框是方法(包含其名称和 iprof 文件中的 ID),它们通过标记箭头连接,表示“在 BCI 上调用”的关系。


             BCI 2    +----------+     BCI 9
          +-----------| printEven|<-----------+
          |           |   ID 3   |            |
          V           +----------+            |
     +-------+                        +----------------+   BCI 3   +------+
     | print |                        | printEvenOrOdd |<----------| main |
     | ID 5  |                        |    ID 2        |           | ID 1 |
     +-------+                        +----------------+           +------+
          ^                                   |
          |           +---------+             |
          +---------- | printOdd|<------------+
             BCI 2    |   ID 4  |    BCI 15
                      +---------+

最简单的部分上下文示例是未内联的方法的开头。请注意,这并不意味着该方法从未被内联——只是在此上下文中它作为编译根(compilation root)存在。此信息存储为一对由 : 分隔的整数。这两个整数中的第一个是方法 ID(如前所述),第二个是 BCI。由于示例是关于方法的开头,因此 BCI 将为 0。在此示例应用程序中,此类单一方法开头的部分上下文示例将是 BCI 0 处的 main,或者表示为 1:0(ID:BCI)。

如果需要标识单一方法部分上下文中的其他位置,您可以拥有像 1:3 这样的部分上下文,它表示 main 方法中 BCI 3 处的位置。调用图显示此上下文对应于 printEvenOrOdd 的调用。

现在考虑一个方法被内联到另一个方法中的上下文。假设在此示例应用程序的编译过程中,编译从 main 开始。再假设内联器决定将对 printEvenOrOdd 的调用内联到 main(在 BCI 3 处)。叠加在调用图上的编译单元(compilation unit)如下所示。


             BCI 2    +----------+     BCI 9
          +-----------| printEven|<-----------+
          |           |   ID 3   |            |
          V           +----------+ +----------|-----------------------------+
     +-------+                     |  +----------------+   BCI 3   +------+ |
     | print |                     |  | printEvenOrOdd |<----------| main | |
     | ID 5  |                     |  |    ID 2        |           | ID 1 | |
     +-------+                     |  +----------------+           +------+ |
          ^                        +----------|-----------------------------+
          |           +---------+             |
          +---------- | printOdd|<------------+
             BCI 2    |   ID 4  |    BCI 15
                      +---------+

现在需要识别可以描述为“printEvenOrOdd 在 BCI 3 处内联到 main 中的起始位置”的位置。上下文将与前面的示例一样开始——方法 ID(printEvenOrOdd 为 2),后跟 : 和 BCI(方法开头为 0)。但是,还需要编码额外的上下文信息——即 printEvenOrOdd 在 BCI 3 处被内联到 main 中。为此,上下文会附加 < 字符,然后附加额外的上下文。由此产生的上下文表示为 2:0<1:3——ID 为 2 的方法在 BCI 0 处,内联到 ID 为 1 的方法在 BCI 3 处。类似地,从此编译单元对 printEven 的调用(在 printEvenOrOdd 的 BCI 9 处)可以表示为 2:9<1:3

让我们扩展此编译单元以包含更多方法:print 方法在 BCI 3 处内联到 printEven 中,然后 printEven 在 BCI 9 处内联到 printEvenOrOdd 中,再由 printEvenOrOdd 在 BCI 3 处内联到 main 中。扩展后的编译单元在下图中显示。

   +------------------------------------------------------------------------+
   |         BCI 2    +----------+     BCI 9                                |
   |      +-----------| printEven|<-----------                              |
   |      |           |   ID 3   |            |                             |
   |      V           +----------+            |                             |
   | +-------+                        +----------------+   BCI 3   +------+ |
   | | print |                        | printEvenOrOdd |<----------| main | |
   | | ID 5  |                        |    ID 2        |           | ID 1 | |
   | +-------+                        +----------------+           +------+ |
   +------^-----------------------------------|-----------------------------+
          |           +---------+             |
          +---------- | printOdd|<------------+
             BCI 2    |   ID 4  |    BCI 15
                      +---------+

现在可以相当简洁地写下几个部分上下文,而用自然语言写下它们则非常繁琐。考虑 5:0<3:2<2:2<1:3 部分上下文。这被解读为“print 的开始,内联到 printEven 的 BCI2 处,该方法又内联到 printEvenOrOdd 的 BCI 9 处,最后该方法内联到 main 的 BCI 3 处”。这些部分上下文可以任意长,具体取决于编译器在构建已插桩镜像时所做的内联决策。

请注意,此编译单元不包含 printOdd。现在假设 printOdd 是一个编译根,并且在 BCI 2 处将 print 内联到其中。两个编译单元叠加在调用图上后如下所示。

   +------------------------------------------------------------------------+
   |         BCI 2    +----------+     BCI 9                                |
   |      +-----------| printEven|<-----------                              |
   |      |           |   ID 3   |            |                             |
+--|------V------+    +----------+            |                             |
|  | +-------+   |                    +----------------+   BCI 3   +------+ |
|  | | print |   +-----------------+  | printEvenOrOdd |<----------| main | |
|  | | ID 5  |                     |  |    ID 2        |           | ID 1 | |
|  | +-------+                     |  +----------------+           +------+ |
|  +------^-----------------------------------|-----------------------------+
|         |           +---------+  |          |
|         +---------- | printOdd|<------------+
|            BCI 2    |   ID 4  |  | BCI 15
|                     +---------+  |
+----------------------------------+

这将导致“print 的开始”有两个不同的部分配置文件:一个带有前面所示的上下文(5:0<3:2<2:2<1:3),另一个则以 printOdd 作为部分上下文中的最右侧条目(5:0<4:2)。请注意,如果 print 也被编译为编译根(例如,如果它在代码中的其他位置被调用且未在该处内联),那么 print 的开头将有另一个部分上下文,即简单的 5:0

存储调用计数配置文件 #

iprof 文件中的此条目是一个对象数组,其中每个对象包含一个上下文(存储在对象的 ctx 字段中)以及配置文件的实际数值(存储在对象的 records 字段中)。在调用计数配置文件的情况下,唯一存储的数值是该方法(在上下文开始处,BCI 为 0)在该上下文中执行的次数。这被建模为一个包含单个整数值的数组。

考虑第一个应用程序示例中的以下调用计数配置文件。

"callCountProfiles": [
...
        {
            "ctx": "19551:0",
            "records": [
                1
            ]
        },
...
        {
            "ctx": "4669:0<19551:34",
            "records": [
                10
            ]
        },
...
]

第一个显示的对象表示 ID 为 19551 的方法在该上下文中仅执行了一次。在 iprof 文件的 methods 字段中查找该 ID 的方法显示,它是 Fib 类的 fibonacci 方法。该方法在运行期间确实只执行了一次,并且碰巧没有被内联到其唯一的调用者(main)中。

第二个对象显示,ID 为 4669 的方法被内联到 fibonacci 中,并且该调用发生在 BCI 34 处。该方法在该上下文中执行了 10 次。进一步查看 iprof 文件可以看到,这实际上是通过 System.out 调用的 java.io.PrintStream#print 方法,该方法在该上下文中确实执行了 10 次。验证此点留给读者作为练习。

条件配置文件 #

条件配置文件包含代码中条件(即分支)行为的信息。这包括 ifswitch 语句以及所有循环,因为它们最终都受条件语句的约束。配置文件信息本质上是条件语句的每个分支被执行了多少次。

条件配置文件的存储方式与调用计数配置文件非常相似——一个对象数组,其中包含 ctxrecords 字段,它们的值分别是字符串和整数数组。建议理解调用计数配置文件部分中的信息,特别是“部分调用上下文”子部分。

考虑斐波那契示例中的以下条件配置文件选择。

"conditionalProfiles": [
...
        {
            "ctx": "19551:11",
            "records": [
                20,
                0,
                10,
                53,
                1,
                1
            ]
        },
...
]

此对象的 ctx 字段中的值显示,相关方法的 ID 为 19551,即 Fib#fibonacci。相关 BCI 为 11。检查方法的字节码会显示 BCI 11 对应于 fibonacci 方法中 for 循环的条件检查。这意味着此配置文件与 fibonacci 方法中的 for 循环有关。此对象的 records 条目是一个包含 6 个值的数组。这是因为条件有两个分支(一个到循环的开始,另一个退出循环),并且每个分支存储 3 个整数值:分支跳转到的 BCI、分支的索引以及该分支被执行的次数。这意味着条件配置文件中 records 数组的长度必须始终可被 3 整除。一个有 100 个分支的 switch 语句将产生一个包含 300 个值的数组。分支的索引只是编译器强加的分支顺序。这是必要的,因为多个分支可能目标相同的 BCI,但索引是唯一的。

回到示例值(200105311)来看,它们表示跳转到 BCI 20(索引 0)发生了 10 次(前 3 个值),而跳转到 BCI 53(索引 1)发生了一次。回到 fibonacci 的源代码,循环执行 n 次,示例中 n 为 10。这与收集到的配置文件一致——10 次跳转到循环开始处以重复循环 10 次,以及 1 次跳转到循环外部以终止循环。

虚拟调用配置文件 #

虚拟调用配置文件包含虚拟调用接收者的运行时类型信息。具体来说,它记录了每种记录的类型作为虚拟调用接收者的次数。PGO 的当前实现限制了每个位置记录的类型数量为 8,但 iprof 格式中没有此限制。

虚拟调用配置文件的存储方式与调用计数配置文件非常相似——一个对象数组,其中包含 ctxrecords 字段,它们的值分别是字符串和整数数组。建议理解调用计数配置文件部分中的信息,特别是“部分调用上下文”子部分。

考虑斐波那契示例中的以下虚拟调用配置文件选择。

...
    "virtualInvokeProfiles": [
...
        {
            "ctx": "3236:11<4669:2<19551:34",
            "records": [
                2280,
                10
            ]
        },
...
        {
            "ctx": "6886:9<6882:23",
            "records": [
                1322,
                2,
                2280,
                60,
                3660,
                56
            ]
        },
...
    ]
...

上下文末尾的方法 ID 为 19551(Fib#fibonacci)。在该方法的 BCI 34 处,ID 为 4669 的方法被内联到 fibonacci 中。查看 iprof 文件中的方法,可以看到它是 java.io.PrintStream#print,这与源代码预期一致。此外,在 BCI 2 处,ID 为 3239 的方法被内联到 print 中,并且配置文件指的是该方法的 BCI 11。再次查看 iprof 文件中的方法,可以看到 java.lang.String#valueOf(java.lang.Object) 方法的 ID 为 3236。此 valueOf 方法在 BCI 11 处有一个虚拟调用。该方法的源代码如下所示,相关虚拟调用是对 ObjecttoString 方法的调用。

    public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    }

records 数组只有 2 个值。第一个数字是记录的类型 ID(在本例中 2280java.lang.String)。第二个数字是此类型作为此虚拟调用接收者的次数计数。由于示例应用程序只向 print 方法传递 java.lang.String(请注意 num1 后添加了一个空格,这隐式地将参数转换为 java.lang.String),并且 print 方法被调用了 10 次——所以 java.lang.String 的计数是 10。

虚拟调用配置文件的 records 数组的长度总是 2 的倍数,因为这些值代表一个类型 ID 和计数对。在示例的第二个对象中,records 数组有 6 个条目,意味着在运行时记录了 3 种不同的类型作为接收者类型。

监视器配置文件 #

本节描述了监视器配置文件。在 Java 中,每个对象都有自己的监视器(monitor),可用于确保对代码段的独占访问(使用 synchronized 关键字)。监视器配置文件记录了哪些类型用于同步代码(无论是通过向类型的方法添加 synchronized 隐式同步,还是通过 synchronized(obj) {...} 显式同步),以及每种类型发生这种情况的次数。

监视器配置文件以与调用计数配置文件非常相似的格式存储——一个对象数组,其中包含 ctxrecords 字段,它们的值分别是字符串和整数数组。建议理解调用计数配置文件部分中的信息,特别是部分调用上下文子部分。

值得注意的是,由于监视器配置文件是全局的,即与特定上下文无关,因此数组中只有一个对象,并且该对象在 ctx 字段中有一个虚拟的 0:0 上下文。这样做是为了兼容性原因,以保持所有配置文件格式的一致性。

请参见下面斐波那契示例的全部监视器配置文件。

    "monitorProfiles": [
        {
            "ctx": "0:0",
            "records": [
                9,
                4,
                10,
                1,
                579,
                9,
                619,
                10,
                1213,
                1,
                1972,
                1,
                2284,
                2,
                2337,
                1,
                2612,
                2,
                3474,
                3,
                3654,
                61,
                3807,
                3,
                3820,
                7,
                4060,
                2,
                4127,
                3,
                4725,
                6
            ]
        }
    ],
...
]

如前所述,数组中单个对象的 ctx 字段的值是一个虚拟上下文 0:0。另一方面,records 的格式类似于虚拟调用配置文件所使用的格式——一个由类型 ID 和计数对组成的数组。这意味着,与虚拟调用配置文件一样,records 数组的长度必须是 2 的倍数。

数组的前两个值表示 ID 为 9 的类型(java.lang.Object)已被用于同步 4 次。由于示例仅对 Fib 实例进行了一次同步(fibonacci 方法是 synchronized 的),接下来的两个值表示 ID 为 10 的类型(Fib)已被用于同步一次(回想一下 fibonacci 方法只执行了一次)。

采样配置文件 #

本节描述了采样配置文件。与迄今为止所有通过插桩收集且仅具有部分上下文的配置文件不同,采样配置文件通过定期采样调用堆栈来收集,无需插桩。这也意味着采样配置文件中包含的上下文不是部分上下文,而实际上是采样时的整个调用堆栈。这意味着与其它配置文件相比,采样配置文件中出现更长的上下文是正常且预期的。

采样配置文件以与调用计数配置文件非常相似的方式存储——一个对象数组,其中包含 ctxrecords 字段,它们的值分别是字符串和整数数组。建议理解调用计数配置文件部分中的信息,特别是“部分调用上下文”子部分。

斐波那契示例运行速度相当快,采样器难以收集到各种有用的样本,因此下面显示的是全部采样配置文件。

...
    "samplingProfiles": [
        {
            "ctx": "11823:38<12811:1<12810:33<12855:25<19551:17<19547:9<19529:10<6305:105<5998:67<5941:0<5903:50<2684:23<2685:1",
            "records": [
                10
            ]
        },
        {
            "ctx": "22500:23<22353:65<22210:15<22187:246<22032:20<22030:1<22027:22<11795:68<11793:12<43854:2",
            "records": [
                1
            ]
        }
    ],
...
]
...

采样配置文件中 ctx 值的长度要长得多。采样配置文件中的第一个对象在上下文顶部具有 ID 为 11823 的方法。查看 iprof 文件中的方法条目,这是 com.oracle.svm.core.thread.PlatformThreads#sleep 方法,它从 ID 为 12811 的方法(java.lang.Thread#sleepNanos0)调用,后者从 ID 为 12810 的方法(java.lang.Thread#sleepNanos)调用,后者从 ID 为 12855 的方法(java.lang.Thread#sleep)调用,后者从 ID 为 19551 的方法(Fib#fibonacci)调用,以此类推直到应用程序的入口点。再次注意,这是一个完整上下文,与其它配置文件使用的部分上下文不同。

records 数组包含一个单个值,它告诉我们此唯一调用堆栈在运行时采样期间被看到了多少次。在本例中,这意味着上一段中描述的上下文被记录了 10 次。

采样配置文件数组中的另一个对象包含不同的上下文,并且此样本仅被看到了 Y 次。理解此样本的性质留给读者作为练习。

联系我们