【问题标题】:Java - How to overcome the maximum method size in automatically-generated codeJava - 如何克服自动生成代码中的最大方法大小
【发布时间】:2015-11-18 23:38:05
【问题描述】:

我有一个不寻常的要求:我的应用程序从一个很长的脚本(用动态类型语言编写)自动生成 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,因为互操作性也是一个问题......

我也很感激指向可能对我有帮助的库的指针。

【问题讨论】:

  • 如果担心效率问题,请注意,超过 8 KB 的方法默认不会编译。
  • 我会考虑内联可以为您做什么。是否有任何重复的代码序列在内联后会产生相同的代码?
  • @PeterLawrey,超过 8KB 会发生什么?代码是否被解释,效率很高?是什么让编译器决定它应该编译什么?关于inlining,它究竟是如何工作的?我应该在代码中寻找“模式”并创建专门的方法来处理它们吗?
  • 如果方法没有被编译,它将被解释。通过内联,我建议为您在生成的代码中使用的常见功能编写一些小方法。
  • 如果我是你,我会认真考虑@OldCurmudgeon 的答案。你说它需要尽可能快地运行,但我总是会质疑这样的假设,因为它很大程度上取决于那个长循环中发生的任何其他事情。如果这段代码即使在被解释的时候也只占总数的 20%,那么编译它就不能节省更多,而且很可能你在其他地方有更大的“炸鱼”。

标签: java jvm code-generation auto-generate


【解决方案1】:

尽管其他人提到了它的缺点,但我们在其中一个项目中使用了类似的方法。我们从 @Marco13 建议的单个启动器方法中调用多个生成的方法。我们实际上(非常精确地)计算了生成的字节码的大小,并且只有在达到限制时才开始一个新方法。我们翻译成 Java 代码的数学公式可以作为 AstTree 使用,并且我们有一个特殊的访问者来计算每个表达式的字节码长度。对于这样简单的程序,它在 Java 版本和不同编译器之间相当稳定。所以我们不会创建不必要的方法。在我们的例子中,直接发出字节码非常困难,但您可以尝试使用 ASM 或类似库为您的语言执行此操作(当然,ASM 会为您计算字节码长度)。

我们通常将数据变量存储在单个double[] 数组中(我们不需要其他类型)并将其作为参数传递。这样您就不需要大量的字段(有时我们有数千个变量)。另一方面,与索引高于 127 的字段访问相比,本地数组访问可能需要更多的字节码字节。

另一个问题是恒定的池大小。我们通常在自动生成的代码中有很多双常量。如果您声明许多字段和/或方法,它们的名称也会采用常量池条目。所以有可能达到类常量池的限制。有时我们会碰到它并生成嵌套类来解决这个问题。

其他人也建议调整 JVM 选项。请谨慎使用这些建议,因为它们不仅会影响这个自动生成的类,还会影响其他所有类(我假设在您的情况下其他代码也在同一个 JVM 中执行)。

【讨论】:

    【解决方案2】:

    将局部变量转换为字段实际上可能会对性能产生负面影响只要代码未经过 JIT 优化(请参阅this question and related ones 了解更多信息)。但我看到,根据这涉及的变量,可能几乎没有其他可行的选择。


    编译和方法大小可能有额外的限制。 Peter Lawrey 在 cmets 中提到“...默认情况下不编译大小超过 8 KB 的方法” - 我不知道这个,但他通常知道他在说什么,所以你应该在这里更深入地挖掘。此外,您可能想查看HotSpot VM options 以了解哪些进一步的限制和设置可能与您相关。我主要是这样认为的

    -XX:MaxInlineSize=35 : 要内联的方法的最大字节码大小。

    可能需要牢记。

    (事实上,调用 so 很多大小为 MaxInlineSize 的方法,内联所有这些调用将超过包含的大小为 65k 字节方法对于内联过程的鲁棒性和边缘情况测试可能是一个非常讨厌的测试用例......)


    您为这些方法绘制了一个“伸缩式”调用方案:

    void run1(){
       ...
       run2();
    }
    
    void run2(){
       ...
       run3();
    }
    

    这也可能导致问题:考虑到您有超过 650 个这些方法(在最好的情况下),这至少会导致 非常 深堆栈,实际上可能会导致StackOverflowError - 再次取决于Memory Options 中的一些。您可能需要通过相应地设置-Xss 参数来增加堆栈大小。


    实际的问题描述有点模糊,并且没有关于要生成的代码的更多信息(还有关于例如你需要多少局部变量的问题,可能必须转到实例变量等),我会提出以下建议:

    • 尽可能创建许多小方法(考虑MaxInlineSize
    • 尝试重用这些小方法(如果可以通过合理的努力从输入中检测到这种可重用性)
    • 按顺序调用这些方法,如

      void run()
      {
          run0();
          run1();
          ...
          run2000();
      }
      

      避免堆栈大小问题。


    但是,如果您添加更多示例或详细信息,您可能会提供更有针对性的建议。这甚至可以是一个“完整”的示例——不一定涉及数千行代码,而是显示那里出现的实际模式

    【讨论】:

    • 感谢您的回答,绝对有帮助!很难提供代码的“代表性样本”,因为它可能非常多样化。在指令级别有一些重复模式(各种行看起来很相似),但在程序级别没有那么多(行块看起来很相似)。
    【解决方案3】:
    • 将所有局部变量变成字段

    这不会有丝毫影响。方法大小 == 代码大小。与局部变量无关,只影响调用帧大小。

    • 在单独的方法中每 100 行(如果需要 if/else 块,则可以更长)拆分代码。

    这是您唯一的选择,而不是完全不同的实施策略。

    代码生成器的问题在于它们会生成代码。

    【讨论】:

    • 将局部变量转换为字段是拆分方法的一个促成因素(我不必弄清楚方法1应该传递给方法2,因为所有变量都可以作为字段使用)
    【解决方案4】:

    我很想写一个解释器或者可能是一个内联编译器。您甚至可以获得一些速度提升,因为生成的大部分小得多的代码库将更容易缓存。

    【讨论】:

    • 您的意思是编写一个解释器(用Java)来解释原始语言?内联编译器如何工作以及如何编写?
    • @EricLeibenguth - 内联编译器将读取该语言并构建一个可以直接执行的数据结构 - 可能类似于状态机。这可能比 interpteter 更快。
    • 好的,我明白你的意思,但是我自己构建起来似乎有点复杂。也许您知道可能有帮助的库?
    • @EricLeibenguth - 您可能会发现Expression Parser 是一个好的开始。 JEP 看起来很有趣。
    猜你喜欢
    • 1970-01-01
    • 2022-12-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-08
    • 1970-01-01
    • 2019-04-18
    相关资源
    最近更新 更多