JEP 165: Compiler Control | 编译器控制
摘要
这篇文章提出了一种改进 JVM 编译器控制的方式。它实现了运行时可管理、方法相关的编译器标志。(在编译过程中不可变。)
目标
- 精细的、与方法上下文相关的 JVM 编译器(C1 和 C2)控制
- 能够在运行时更改 JVM 编译器控制选项
- 没有性能下降
动机
编译过程中与方法上下文相关的控制是编写小型 JVM 编译器测试的有力工具,可以在不重启整个 JVM 的情况下运行。它也非常有用于创建 JVM 编译器中的错误的解决方法。编译器选项的良好封装也是很好的卫生习惯。
描述
指令
控制 JVM 编译器的所有选项将被收集到一组选项中。具有值的选项集称为编译器指令,是一条编译指令。指令与一个方法匹配器一起提供给 VM,该匹配器决定它适用于哪些方法。多个指令可以同时在运行时处于活动状态,但只有一个应用于特定的编译。指令可以在运行时添加和删除。
指令格式
指令文件具有指定的标准化和人类可读的文件格式。可以通过命令行和诊断命令加载指令文件。指令文件有一个或多个定义的指令。指令包含一个方法模式和一些带有值的选项。指令的顺序很重要。CompilerBroker 将应用第一个与编译匹配的指令。
指令文件格式将是 JSON 的子集,其中包含一些附加项。格式在以下几个方面偏离 JSON:
- 仅支持与命令行选项兼容的数字 - int 和 double。
- 允许注释 - 以“//”开头的行
- 数组和对象中允许额外的尾随 ","
- 可能不允许转义字符(TBD)
- 选项名称是字符串,但可以选择加引号
该文件可以使用 JVM 规范支持的所有 UTF-8 字符。这些字符保留用于文件格式:
{ - 左大括号
} - 右大括号
[ - 左方括号
] - 右方括号
" - 引号
: - 冒号
, - 逗号
指令示例 1
[ // 开始指令数组
{ // 指令块的开始
// 精确匹配一个或多个模式
// 数组在只有一个模式时不是必需的
match: ["java*::*", "oracle*::*"],
// 仅适用于单个编译器的指令块
c1: {
// 布尔选项。额外的尾随逗号不应导致解析错误
PrintAssembly:true,
},
// 另一个编译器块
c2: {
// 强制内联模式以 + 开头,以 - 开头为防止内联
inline: ["+vm*::*","-*::*" ]
},
// 编译器块外的选项适用于所有编译器
BreakAtExecute: true // 在已编译的代码中启用断点
BreakAtCompile: true // 在编译器中启用断点
},
{ // 开始另一个指令块
// 匹配类名以“Concurrent”结尾的方法
match: ["*Concurrent::*"],
c2: {
//禁用编译
Exclude:true,
}
// 如果未指定 c1 指令,则选项将保持默认值。
}
]
指令示例 2
[
{
// 要匹配的类 + 方法 + 签名的模式
// 允许前导和尾随通配符(*)
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 将以错误消息退出。
-XX:CompilerDirectivesFile=<file>
诊断命令界面
以下是与编译器控制一起使用的诊断命令:
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:
java/example/Test.split
Is composed by three parts
java/example/Test + split + (Ljava/lang/String;)Ljava/lang/String;
风险和假设
编译器选项的数量会限制我们一开始只关注其中的一个子集。我们将重点关注一个子集,并从那里扩展。
依赖关系
- 诊断命令 - 已经存在
- 使用完整的 JDK - 已经存在
影响
- 文档:标志和 API
- CCC: 对于指令格式、JVM 编译器标志更改和 API,将需要进行 CCC 请求。
- Performance: 标准回归测试。