Scalamacros和JVM的方法大小限制
我正在使用ScalamacrosreplaceJava程序中的一些代码生成组件,并正在运行到Java虚拟机对于单个方法(64千字节)生成的字节代码大小的限制。
例如,假设我们有一个大型的XML文件,它代表了我们想要在程序中使用的从整数到整数的映射。 我们希望避免在运行时parsing这个文件,所以我们将编写一个macros来编译时进行parsing,并使用文件的内容来创build我们方法的主体:
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
并被告知我们已经杀死了编译器。
我有一个解决scheme ,包括将案例分成固定大小的组,为每个组创build一个单独的方法,并添加一个顶级匹配,将input值分派给相应的组方法。 它的工作,但它是不愉快的,我不希望每次我写一个macros的地方,生成的代码的大小取决于一些外部资源,不必使用这种方法。
有没有更好的方法来解决这个问题? 更重要的是,有没有办法更好地处理这种编译器错误? 我不喜欢图书馆用户的想法,得到一个无法理解的“那个条目似乎已经杀死了编译器”的错误消息,只是因为一些正在被macros处理的XML文件已经跨越了一些(相当低的)大小的阈值。
由于有人不得不说,我按照Importers
的指示试图在返回之前编译树。
如果你给编译器足够的堆栈,它会正确地报告错误。
(它似乎不知道如何处理切换注释,留作未来练习。)
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 } }
把数据放入.class并不是一个好主意。 它们也被parsing,它们只是二元的。 但是将它们存储在JVM中可能会对Garbagge收集器和JIT编译器的性能产生负面影响。
在你的情况下,我会预先将XML编译成适当格式的二进制文件并parsing。 具有现有工具的合适格式可以是例如FastRPC或好的旧的DBF 。 后者的一些实现也可以提供基本的索引,甚至可以parsing出来 – 应用程序只是从相应的偏移量中读取。