Java - 如何克服自动生成代码中的最大方法大小
Java - How to overcome the maximum method size in automatically-generated code
我有一个不寻常的要求:我的应用程序从一个很长的脚本(用动态类型语言编写)自动生成 Java 代码。脚本太长了,我打了the maximum method size of 65k of the JVM.
该脚本仅包含有关原始类型的简单指令(除了数学函数外不调用其他函数)。它可能看起来像:
...
a = b * c + sin(d)
...
if a>10 then
e = a * 2
else
e = a * abs(b)
end
...
... 转换为:
...
double a = b * c + Math.sin(d);
...
double e;
if(a>10){
e = a * 2;
}else{
e = a * Math.abs(b);
}
...
我克服方法大小限制的第一个想法如下:
- 将所有局部变量转为字段
- 在单独的方法中每 100 行拆分一次代码(如果 if/else 块需要,则拆分更长的时间)。
类似于:
class AutoGenerated {
double a,b,c,d,e,....;
void run1(){
...
a = b * c + sin(d);
...
run2();
}
void run2(){
...
if(a>10){
e = a * 2;
}else{
e = a * Math.abs(b);
}
...
run3();
}
...
}
你知道还有其他更有效的方法吗?请注意,我需要代码尽可能快地 运行 因为它将在长循环中执行。我不能求助于编译为 C,因为互操作性也是一个问题...
我也非常感谢指向可能对我有帮助的库的指示。
我很想写一个解释器或者一个内联编译器。您甚至可能会获得一些速度提升,因为大多数由此产生的更小的代码库将更容易缓存。
- Turn all local variables into fields
那不会有丝毫影响。方法大小 == 代码大小。与局部变量无关,只影响调用帧大小。
- Split the code every 100 lines (or longer if needed in case of a if/else block) in separate methods.
除了完全不同的实施策略,这是您唯一的选择。
代码生成器的问题在于它们生成代码。
将局部变量转换为字段实际上可能会对性能产生负面影响只要代码未被 JIT 优化(请参阅 this question and related ones 了解更多信息信息)。但我看到,根据这涉及的变量,可能几乎没有其他可行的选择。
编译和方法大小可能有额外的限制。 Peter Lawrey 在评论中提到“...默认情况下不编译大小超过 8 KB 的方法”——我不知道这个,但他通常知道他在说什么关于,所以你应该在这里更深入地挖掘。此外,您可能想查看 HotSpot VM options 以了解哪些进一步的限制和设置可能与您相关。我主要认为
-XX:MaxInlineSize=35
: Maximum bytecode size of a method to be inlined.
可能需要牢记。
(事实上,调用 so 许多大小为 MaxInlineSize
的方法,内联所有这些调用将 超过 包含方法的 65k 字节大小对于内联过程的稳健性和边缘情况测试来说可能是一个非常讨厌的测试用例...)
您为这些方法草拟了一个 "telescoping" 调用方案:
void run1(){
...
run2();
}
void run2(){
...
run3();
}
这也可能会导致问题:考虑到您有超过 650 个这些方法(在最好的情况下),这至少会导致 非常 深堆栈,并且实际上可能会导致 WhosebugError
- 再次取决于某些 Memory Options。您可能需要相应地设置 -Xss
参数来增加堆栈大小。
实际的问题描述有点含糊,并且没有关于要生成的代码的更多信息(还有关于例如需要多少 局部变量 的问题,即可能必须变成实例变量等),我建议如下:
- 如果可能创建许多小方法(考虑
MaxInlineSize
)
- 尝试重用这些小方法(如果可以通过合理的努力从输入中检测到这种可重用性)
按顺序调用这些方法,如
void run()
{
run0();
run1();
...
run2000();
}
以避免堆栈大小出现问题。
但是,如果您添加更多示例或详细信息,可能会提供更有针对性的建议。这甚至可以是一个 "complete" 示例 - 不一定涉及数千行代码,但显示出现在那里的实际 模式 。
我们在其中一个项目中使用了类似的方法,尽管其他人提到了它的缺点。我们像 @Marco13 建议的那样从单个启动器方法调用多个生成的方法。我们实际上(非常精确地)计算生成的字节码的大小,并仅在达到限制时才启动一个新方法。我们翻译成 Java 代码的数学公式可作为 AstTree 使用,我们有一个特殊的访问者,它计算每个表达式的字节码长度。对于这种简单的程序,它在 Java 版本和不同的编译器中非常稳定。所以我们不会创建不必要的方法。在我们的例子中,直接发出字节码是相当困难的,但你可以尝试使用 ASM 或类似的库为你的语言做这件事(当然,这样,ASM 会为你计算字节码长度)。
我们通常将数据变量存储在单个 double[]
数组中(我们不需要其他类型)并将其作为参数传递。这样您就不需要大量的字段(有时我们有数千个变量)。另一方面,与索引高于 127 的字段访问相比,本地数组访问可能需要更多的字节码字节。
另一个问题是常量池大小。我们通常在自动生成的代码中有很多双精度常量。如果你声明了很多字段 and/or 方法,它们的名字也取常量池条目。所以有可能达到 class 常量池限制。有时我们打它并生成嵌套的classes来克服这个问题。
其他人也建议调整 JVM 选项。请谨慎使用这些建议,因为它们不仅会影响这个自动生成的 class,还会影响其他所有 class(我假设其他代码也在您的案例中的同一 JVM 中执行)。
我有一个不寻常的要求:我的应用程序从一个很长的脚本(用动态类型语言编写)自动生成 Java 代码。脚本太长了,我打了the maximum method size of 65k of the JVM.
该脚本仅包含有关原始类型的简单指令(除了数学函数外不调用其他函数)。它可能看起来像:
...
a = b * c + sin(d)
...
if a>10 then
e = a * 2
else
e = a * abs(b)
end
...
... 转换为:
...
double a = b * c + Math.sin(d);
...
double e;
if(a>10){
e = a * 2;
}else{
e = a * Math.abs(b);
}
...
我克服方法大小限制的第一个想法如下:
- 将所有局部变量转为字段
- 在单独的方法中每 100 行拆分一次代码(如果 if/else 块需要,则拆分更长的时间)。
类似于:
class AutoGenerated {
double a,b,c,d,e,....;
void run1(){
...
a = b * c + sin(d);
...
run2();
}
void run2(){
...
if(a>10){
e = a * 2;
}else{
e = a * Math.abs(b);
}
...
run3();
}
...
}
你知道还有其他更有效的方法吗?请注意,我需要代码尽可能快地 运行 因为它将在长循环中执行。我不能求助于编译为 C,因为互操作性也是一个问题...
我也非常感谢指向可能对我有帮助的库的指示。
我很想写一个解释器或者一个内联编译器。您甚至可能会获得一些速度提升,因为大多数由此产生的更小的代码库将更容易缓存。
- Turn all local variables into fields
那不会有丝毫影响。方法大小 == 代码大小。与局部变量无关,只影响调用帧大小。
- Split the code every 100 lines (or longer if needed in case of a if/else block) in separate methods.
除了完全不同的实施策略,这是您唯一的选择。
代码生成器的问题在于它们生成代码。
将局部变量转换为字段实际上可能会对性能产生负面影响只要代码未被 JIT 优化(请参阅 this question and related ones 了解更多信息信息)。但我看到,根据这涉及的变量,可能几乎没有其他可行的选择。
编译和方法大小可能有额外的限制。 Peter Lawrey 在评论中提到“...默认情况下不编译大小超过 8 KB 的方法”——我不知道这个,但他通常知道他在说什么关于,所以你应该在这里更深入地挖掘。此外,您可能想查看 HotSpot VM options 以了解哪些进一步的限制和设置可能与您相关。我主要认为
-XX:MaxInlineSize=35
: Maximum bytecode size of a method to be inlined.
可能需要牢记。
(事实上,调用 so 许多大小为 MaxInlineSize
的方法,内联所有这些调用将 超过 包含方法的 65k 字节大小对于内联过程的稳健性和边缘情况测试来说可能是一个非常讨厌的测试用例...)
您为这些方法草拟了一个 "telescoping" 调用方案:
void run1(){
...
run2();
}
void run2(){
...
run3();
}
这也可能会导致问题:考虑到您有超过 650 个这些方法(在最好的情况下),这至少会导致 非常 深堆栈,并且实际上可能会导致 WhosebugError
- 再次取决于某些 Memory Options。您可能需要相应地设置 -Xss
参数来增加堆栈大小。
实际的问题描述有点含糊,并且没有关于要生成的代码的更多信息(还有关于例如需要多少 局部变量 的问题,即可能必须变成实例变量等),我建议如下:
- 如果可能创建许多小方法(考虑
MaxInlineSize
) - 尝试重用这些小方法(如果可以通过合理的努力从输入中检测到这种可重用性)
按顺序调用这些方法,如
void run() { run0(); run1(); ... run2000(); }
以避免堆栈大小出现问题。
但是,如果您添加更多示例或详细信息,可能会提供更有针对性的建议。这甚至可以是一个 "complete" 示例 - 不一定涉及数千行代码,但显示出现在那里的实际 模式 。
我们在其中一个项目中使用了类似的方法,尽管其他人提到了它的缺点。我们像 @Marco13 建议的那样从单个启动器方法调用多个生成的方法。我们实际上(非常精确地)计算生成的字节码的大小,并仅在达到限制时才启动一个新方法。我们翻译成 Java 代码的数学公式可作为 AstTree 使用,我们有一个特殊的访问者,它计算每个表达式的字节码长度。对于这种简单的程序,它在 Java 版本和不同的编译器中非常稳定。所以我们不会创建不必要的方法。在我们的例子中,直接发出字节码是相当困难的,但你可以尝试使用 ASM 或类似的库为你的语言做这件事(当然,这样,ASM 会为你计算字节码长度)。
我们通常将数据变量存储在单个 double[]
数组中(我们不需要其他类型)并将其作为参数传递。这样您就不需要大量的字段(有时我们有数千个变量)。另一方面,与索引高于 127 的字段访问相比,本地数组访问可能需要更多的字节码字节。
另一个问题是常量池大小。我们通常在自动生成的代码中有很多双精度常量。如果你声明了很多字段 and/or 方法,它们的名字也取常量池条目。所以有可能达到 class 常量池限制。有时我们打它并生成嵌套的classes来克服这个问题。
其他人也建议调整 JVM 选项。请谨慎使用这些建议,因为它们不仅会影响这个自动生成的 class,还会影响其他所有 class(我假设其他代码也在您的案例中的同一 JVM 中执行)。