35

我正在用 Scala 宏替换 Java 程序中的一些代码生成组件,并且遇到了 Java 虚拟机对单个方法(64 KB)生成的字节码大小的限制。

例如,假设我们有一个很大的 XML 文件,它表示从整数到我们想要在程序中使用的整数的映射。我们想避免在运行时解析这个文件,所以我们将编写一个宏来在编译时进行解析并使用文件的内容来创建我们方法的主体:

import scala.language.experimental.macros
import scala.reflect.macros.Context

object BigMethod {
  // For this simplified example we'll just make some data up.
  val mapping = List.tabulate(7000)(i => (i, i + 1))

  def lookup(i: Int): Int = macro lookup_impl
  def lookup_impl(c: Context)(i: c.Expr[Int]): c.Expr[Int] = {
    import c.universe._

    val switch = reify(new scala.annotation.switch).tree
    val cases = mapping map {
      case (k, v) => CaseDef(c.literal(k).tree, EmptyTree, c.literal(v).tree)
    }

    c.Expr(Match(Annotated(switch, i.tree), cases))
  }
}

在这种情况下,编译的方法将刚好超过大小限制,但不是一个很好的错误说明,而是给我们一个巨大的堆栈跟踪,其中包含很多调用,TreePrinter.printSeq并被告知我们已经杀死了编译器。

我有一个解决方案,包括将案例分成固定大小的组,为每个组创建一个单独的方法,并添加一个顶级匹配,将输入值分派到适当的组的方法。它可以工作,但令人不快,而且我不希望每次编写宏时都必须使用这种方法,其中生成的代码的大小取决于某些外部资源。

有没有更清洁的方法来解决这个问题?更重要的是,有没有办法更优雅地处理这种编译器错误?我不喜欢图书馆用户收到一个难以理解的“那个条目似乎已经杀死了编译器”错误消息的想法,只是因为某个宏正在处理的 XML 文件已经超过了一些(相当低的)大小阈值。

4

2 回答 2

10

Imo 将数据放入 .class 并不是一个好主意。它们也被解析,它们只是二进制的。但是将它们存储在 JVM 中可能会对垃圾收集器和 JIT 编译器的性能产生负面影响。

在您的情况下,我会将 XML 预编译成正确格式的二进制文件并对其进行解析。现有工具的合格格式可以是例如FastRPC或良好的旧DBF。或者,如果您需要快速的高级查找和搜索,也可以预先填充ElasticSearch存储库。后者的一些实现还可以提供基本索引,甚至可以将解析排除在外——应用程序只会从相应的偏移量中读取。

于 2013-06-16T19:18:23.633 回答
4

由于有人必须说些什么,我按照说明在Importers返回之前尝试编译树。

如果你给编译器足够的堆栈,它会正确地报告错误。

(它似乎不知道如何处理 switch 注释,留作将来的练习。)

apm@mara:~/tmp/bigmethod$ skalac bigmethod.scala ; skalac -J-Xss2m biguser.scala ; skala bigmethod.Test
Error is java.lang.RuntimeException: Method code too large!
Error is java.lang.RuntimeException: Method code too large!
biguser.scala:5: error: You ask too much of me.
  Console println s"5 => ${BigMethod.lookup(5)}"
                                           ^
one error found

apm@mara:~/tmp/bigmethod$ skalac -J-Xss1m biguser.scala 
Error is java.lang.StackOverflowError
Error is java.lang.StackOverflowError
biguser.scala:5: error: You ask too much of me.
  Console println s"5 => ${BigMethod.lookup(5)}"
                                           ^

客户端代码就是这样:

package bigmethod

object Test extends App {
  Console println s"5 => ${BigMethod.lookup(5)}"
}

我第一次使用这个 API,但不是最后一次。谢谢你让我开始。

package bigmethod

import scala.language.experimental.macros
import scala.reflect.macros.Context

object BigMethod {
  // For this simplified example we'll just make some data up.
  //final val size = 700
  final val size = 7000
  val mapping = List.tabulate(size)(i => (i, i + 1))

  def lookup(i: Int): Int = macro lookup_impl
  def lookup_impl(c: Context)(i: c.Expr[Int]): c.Expr[Int] = {

    def compilable[T](x: c.Expr[T]): Boolean = {
      import scala.reflect.runtime.{ universe => ru }
      import scala.tools.reflect._
      //val mirror = ru.runtimeMirror(c.libraryClassLoader)
      val mirror = ru.runtimeMirror(getClass.getClassLoader)
      val toolbox = mirror.mkToolBox()
      val importer0 = ru.mkImporter(c.universe)
      type ruImporter = ru.Importer { val from: c.universe.type }
      val importer = importer0.asInstanceOf[ruImporter]
      val imported = importer.importTree(x.tree)
      val tree = toolbox.resetAllAttrs(imported.duplicate)
      try {
        toolbox.compile(tree)
        true
      } catch {
        case t: Throwable =>
          Console println s"Error is $t"
          false
      }
    }
    import c.universe._

    val switch = reify(new scala.annotation.switch).tree
    val cases = mapping map {
      case (k, v) => CaseDef(c.literal(k).tree, EmptyTree, c.literal(v).tree)
    }

    //val res = c.Expr(Match(Annotated(switch, i.tree), cases))
    val res = c.Expr(Match(i.tree, cases))

    // before returning a potentially huge tree, try compiling it
    //import scala.tools.reflect._
    //val x = c.Expr[Int](c.resetAllAttrs(res.tree.duplicate))
    //val y = c.eval(x)
    if (!compilable(res)) c.abort(c.enclosingPosition, "You ask too much of me.")

    res
  }
}
于 2013-06-21T11:21:37.647 回答