Макросы Scala и ограничение размера метода JVM

Я заменяю некоторые компоненты генерации кода в программе Java с помощью макросов Scala, и я запускаю лимит виртуальной машины Java на размер сгенерированного байтового кода для отдельных методов (64 килобайта).

Например, предположим, что у нас есть большой 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 файл, обрабатываемый макросом, пересек некоторое (довольно низкое) размерное значение.

2 ответа

Поскольку кто-то должен что-то сказать, я выполнил инструкции в 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
 }
}


Imo поместить данные в .class на самом деле не очень хорошая идея. Они также анализируются, они просто двоичные. Но их хранение в JVM может отрицательно сказаться на производительности сборщика garbagge и JIT-компилятора.

В вашей ситуации я бы предварительно скомпилировал XML в двоичный файл соответствующего формата и проанализировал его. Разрешенные форматы с существующим инструментом могут быть, например, FastRPC или старый добрый DBF. Некоторые реализации последних могут также обеспечивать базовое индексирование, которое может даже оставить разбор - приложение будет просто читать из соответствующего смещения.

licensed under cc by-sa 3.0 with attribution.