Skip to content

JEP 165: Compiler Control | 编译器控制

摘要

这篇文章提出了一种改进 JVM 编译器控制的方式。它实现了运行时可管理、方法相关的编译器标志。(在编译过程中不可变。)

目标

  • 精细的、与方法上下文相关的 JVM 编译器(C1 和 C2)控制
  • 能够在运行时更改 JVM 编译器控制选项
  • 没有性能下降

动机

编译过程中与方法上下文相关的控制是编写小型 JVM 编译器测试的有力工具,可以在不重启整个 JVM 的情况下运行。它也非常有用于创建 JVM 编译器中的错误的解决方法。编译器选项的良好封装也是很好的卫生习惯。

描述

指令

控制 JVM 编译器的所有选项将被收集到一组选项中。具有值的选项集称为编译器指令,是一条编译指令。指令与一个方法匹配器一起提供给 VM,该匹配器决定它适用于哪些方法。多个指令可以同时在运行时处于活动状态,但只有一个应用于特定的编译。指令可以在运行时添加和删除。

指令格式

指令文件具有指定的标准化和人类可读的文件格式。可以通过命令行和诊断命令加载指令文件。指令文件有一个或多个定义的指令。指令包含一个方法模式和一些带有值的选项。指令的顺序很重要。CompilerBroker 将应用第一个与编译匹配的指令。

指令文件格式将是 JSON 的子集,其中包含一些附加项。格式在以下几个方面偏离 JSON:

  • 仅支持与命令行选项兼容的数字 - int 和 double。
  • 允许注释 - 以“//”开头的行
  • 数组和对象中允许额外的尾随 ","
  • 可能不允许转义字符(TBD)
  • 选项名称是字符串,但可以选择加引号

该文件可以使用 JVM 规范支持的所有 UTF-8 字符。这些字符保留用于文件格式:

plaintext
{ - 左大括号
} - 右大括号
[ - 左方括号
] - 右方括号
" - 引号
: - 冒号
, - 逗号

指令示例 1

json
[   // 开始指令数组
    {   // 指令块的开始
        // 精确匹配一个或多个模式
        // 数组在只有一个模式时不是必需的
        match: ["java*::*", "oracle*::*"],   
        // 仅适用于单个编译器的指令块
        c1: {   
                // 布尔选项。额外的尾随逗号不应导致解析错误
                PrintAssembly:true,  
        },
        // 另一个编译器块
        c2: {
                // 强制内联模式以 + 开头,以 - 开头为防止内联
                inline: ["+vm*::*","-*::*" ] 
        },
        // 编译器块外的选项适用于所有编译器
        BreakAtExecute: true   // 在已编译的代码中启用断点 
        BreakAtCompile: true   // 在编译器中启用断点
    },
    {  // 开始另一个指令块
        // 匹配类名以“Concurrent”结尾的方法
        match: ["*Concurrent::*"],    
        c2: {
                //禁用编译
                Exclude:true,    
        }    
        // 如果未指定 c1 指令,则选项将保持默认值。
    }
]

指令示例 2

json
[   
    {   
            // 要匹配的类 + 方法 + 签名的模式
            // 允许前导和尾随通配符(*)
            match: "apa.Dingo::*",
    
            // 覆盖指定编译器的默认值
            // 最内层的选项具有最高优先级
            c1: {
            //覆盖 c1 预设
            PrintInlining: false  // 例如 - 此选项可能不存在
            }

            c2: {
            // 控制方法的内联
            // +强制内联,-不内联
            inline : [ "+java.util::*", "-com.sun::*"],
            }

            // 不在特定预设内的指令适用于所有编译器          
            // +强制内联,-不内联
            inline : [ "+java.util::*", "-com.sun::*"],         
            PrintAssembly: true
    },
    {
            // 匹配多个模式需要一个数组
            match: ["steve::*","alex::*"]

            c2: {
                Enable: false,    // 忽略此指令对 c2 的影响。
                BreakAtExecute: true // 这将不会被应用,因为上面的 Enable 为 false                
            }

            // 适用于所有编译器                   
            // +强制内联,-不内联
            inline : [ "+java.util::*", "-com.sun::*"],         
            PrintInlining: true
    },
]

指令选项列表

第一个实现包含以下选项。所有选项都已在 CompileCommand 选项命令中使用过。将添加更多选项。

通用标志:

Enable, bool Exclude, bool BreakAtExecute, bool BreakAtCompile, bool Log, bool PrintAssembly, bool PrintInlining, bool PrintNMethods, bool ReplayInline, bool DumpReplay, bool DumpInline, bool CompilerDirectivesIgnoreCompileCommands, bool Inline, ccstr[]

C2 only:

BlockLayoutByFrequency, bool PrintOptoAssembly, bool PrintIntrinsics, bool raceOptoPipelining, bool TraceOptoOutput, bool TraceSpilling, bool Vectorize, bool VectorizeDebug, bool CloneMapDebug, bool IGVPrintLevel, intx MaxNodeLimit, intx DisableIntrinsics, ccstr

inline:<一个模式或一个字符串模式数组>
模式是一个字符串,与指令匹配的方法名相同。 
以“+”开头的模式表示匹配的方法应该强制内联。 
以“-”开头表示应该防止内联。 
使用第一个匹配的命令。 
例如:inline:["+java.lang.*::*", -"sun*::*"]
例如:inline:"+java.lang.*::*"

指令模式

用于 “match” 和 “inline” 选项的方法模式具有以下模式:Class.method(signature)

类包括由/分隔的包名。类和方法可以使用前导和尾随的 * 通配符,或替换为 * 。如果省略签名,则默认为 *

这些是有效的模式:

"java.lang.String::indexOf"
"java/lang/String.indexOf"
".lang.String::indexOf(I)"
"java/lang/String.(I)"
"java/lang/String.()"
"."
"::"
"java.lang.::"

指令解析器

指令解析器负责解析指令文件并将信息添加到 VM 内部格式中。

如果在命令行上指定了格式不正确的指令文件,则 VM 将打印错误并退出。如果通过诊断命令添加了格式不正确的指令文件,则将被忽略并打印适当的警告。

解析器将验证所有选项是否有效。在不支持这些选项的平台上,平台相关选项将打印警告。原因是相同的指令文件应该在部署在任何平台上时都可用。

未指定的选项将使用默认值。如果指定了命令行选项,它将成为默认值。方法模式的默认值为 "."(匹配所有方法)。

CompilerBroker

CompilerBroker 具有指令堆栈,其中包含所有应用的指令。底部的指令是默认集,永远不能删除。当加载带有附加指令的文件时,它们将以相反的顺序添加,文件中的第一个指令将位于堆栈的顶部。这是一个易用性功能。

当提交方法进行编译时,CompilerBroker 将选择第一个匹配的指令并将其传递给编译器。CompilerBroker 和编译器将忽略会产生错误代码的选项(例如,在不支持的平台上强制使用硬件指令),并发出适当的警告。指令选项具有与普通命令行标志相同的限制 - 例如,只有在 IR 不会增长过大的情况下,才会尊重强制内联。

命令行界面

可以在命令行中添加指令文件。如果标志错误(正常的命令行解析)、文件丢失或文件内容格式不正确,VM 将以错误消息退出。

bash
-XX:CompilerDirectivesFile=<file>

诊断命令界面

以下是与编译器控制一起使用的诊断命令:

plaintext
jcmd <pid> Compiler.add_directives <file>   
从文件中添加附加指令。新指令将添加到旧指令之上,文件中的第一个指令将位于指令堆栈的顶部。

jcmd <pid> Compiler.list_directives        
从顶部到底部列出指令堆栈中的所有指令。

jcmd <pid> Compiler.clear_directives     
清除指令堆栈

jcmd <pid> Compiler.remove_directives     
从指令堆栈中删除顶部元素

CompileCommand 和向后兼容性

CompilerControl 将在所有用例中替代 CompileCommand。为了向后兼容,将保留 CompileCommand,并且目标是尽可能保持行为一致。

可以应用四个级别的控制。编译器控制具有最高优先级,将覆盖任何其他标志或命令。其次是 CompileCommand,再次是任何命令行标志,最后是默认标志值。如果同时使用编译器控制和 CompileCommand,编译器控制将考虑 CompileCommand 正在覆盖默认值。

如果同时使用 CompileCommand 和编译器指令,JVM 应打印警告。

方法模式

编译器控制将使用与 CompileCommand 相同的方法模式格式。模式由三个部分组成:包和类名、方法名和签名。这三个部分中的任何一个都可以使用前导或尾随的 * 作为通配符。任何部分的默认值都是 *

Example:

txt
java/example/Test.split

Is composed by three parts

txt
java/example/Test + split + (Ljava/lang/String;)Ljava/lang/String;

风险和假设

编译器选项的数量会限制我们一开始只关注其中的一个子集。我们将重点关注一个子集,并从那里扩展。

依赖关系

  • 诊断命令 - 已经存在
  • 使用完整的 JDK - 已经存在

影响

  • 文档:标志和 API
  • CCC: 对于指令格式、JVM 编译器标志更改和 API,将需要进行 CCC 请求。
  • Performance: 标准回归测试。