Upload
-
View
17
Download
0
Embed Size (px)
Citation preview
Что такое TeaVM
● Исследовательский проект● AOT-компилятор байт-кода JVM● Генерирует JS, WebAssembly● Можно генерить что угодно (например, в рамках
хакатона я сделал LLVM-бэкэнд)● IR: static single assignment● Оптимизации
Минификация
● В мире JS крайне важно добиться настолько маленького размера бинарника, насколько это возможно
● Проблема: JDK большой. Даже сильно порезанный (привет, Jigsaw) занимает несколько мегабайт. Это неприемлемо
● Наблюдение: библиотеки бывают большие, из них часто используется очень небольшая часть функционала
● Вывод: нужен хороший dead code elimination
Наивный DCE
● Строим ориентированный граф вызовов● Обходим, начиная с точки входа (функции main)● Все помеченные методы компилируем, остальные
выбрасываем● Не поддерживает виртуальных вызовов
Наивный DCE: пример
void foo() { printf(“foo”)}
void bar() { foo(); printf(“bar”); foo();}
void baz() { foo(); bar();}
void main() { foo(); bar();}
foo bar
baz
main
printf
Виртуальные вызовы
● В каждый вызов виртуального метода подставляем реализации из всех достижимых классов
● Достижимыми становятся методы, которые раньше, возможно, были недостижимыми
● Продолжаем обход, пока не сойдётся
Виртуальные вызовы: пример
<T> List<T> copy(List<T> list) { List<T> c = new ArrayList<T>(); for (int i = 0; i < list.size(); i++) { c.add(list.get(i)); } return c;}
void main() { MyList<String> list = new MyList<>(); list.add(“foo”); copy(list); }
class MyList<T> extends ArrayList<T> { T get(int index) { new LinkedList<T>(); return super.get(index); }}
main
copy
MyList.size
MyList.get
MyList.add
ArrayList.<init>
ArrayList.get
MyList.<init>
ArrayList.size
ArrayList.add
Виртуальные вызовы: пример
main
copy
MyList.size
MyList.get
MyList.add
ArrayList.<init>
ArrayList.get
MyList.<init>
ArrayList.size
ArrayList.add
LinkedList.size
LinkedList.<init>
LinkedList.add
LinkedList.get
DCE 2.0: Виртуальные вызовы
● Для каждого метода строим data flow graph● Вершины - определения (переменные) SSA● Рёбра - любые иснтрукции, перемещающие значение
из одной переменной в другую (Phi, Assignment, Cast)● Для некоторых рёбер заводим “фильтр”, например, для
Cast● Для каждой вершины заводим множество типов,
изначально пустое
DCE 2.0: invoke
● При статических вызовах соединяем переменные на call site с параметрами вызываемой функции
● Функция foo вызывает функцию v = bar(a1, a2, ...)● bar объявлена как bar(p1, p2, ...) { ... }● Соединяем рёбрами a1 -> p1, a2 -> p2, и т.д.● Для каждого return u в функции bar соединяем u -> v
DCE 2.0: propagate
● Для каждого u = new SomeType добавляем SomeType в множество типов u
● Протаскиваем типы по графу. Для каждой вершины, если в ней есть какой-то тип T, а в каких-либо соседях этого типа нет, добавляем T соседям
● Продолжаем, пока не сойдётся
DCE 2.0: invokevirtual
● Пусть где-то в коде есть конструкция v = r.foo(a1, a2, ...)● В r попадает новый тип T, которого там ещё не было● Резолвим метод T.foo● Если на данном call site ещё не встречался полученный
метод, выполняем операции, аналогичные статическому invoke
● Продолжаем протаскивать типы, пока возможно● После схождения компилируем все достижимые
методы
DCE 2.0: пример
copy
c tmp list
get
r
add
this e this
main
list tmp
T(main.list) = { MyList }
T(main.tmp) = { String }
T(copy.c) = { ArrayList }
DCE 2.0: пример
copy
c tmp list
get
r
add
this e this
main
list tmp
T(main.list) = { MyList }
T(main.tmp) = { String }
T(copy.c) = { ArrayList }
T(copy.list) = { MyList }
T(get.this) = { MyList }
T(add.this) = { MyList, ArrayList }
ArrayList.add
this e
MyList.get
this r
ArrayList.get
this r
Девиртуализация
● Обходим все InvokeVirtual с ресивером r● Берём множество T(r), находим все соответствующие
реализации● Если реализация одна, заменяем InvokeVirtual на
InvokeSpecial● Аналогично можно обрабатывать n-морфные вызовы● В нашем примере T(get.this) = { MyList }, следовательно
можно девиртуализовать● T(add.this) = { ArrayList, MyList }, но MyList сам не
определяет this, следовательно можно девиртуализовать
DCE 2.0: разное
● v = a[i]● Добавляем вершину a1, ребро a1 -> v
● Аналогично a[i] = v
● При присвоении массива a = b для их соответствующих вершин a1 и b1 заводим рёбра в обе стороны, т.е. a1->b1 и b1 -> a1
● Для каждого метода заводим по вершине для обработки исключений e● throw v добавляет ребро v -> e, если нет ближайшего обработчика
исключений. Иначе, если есть обработчик catch(x), добавляем v → x● При обработке инструкций invoke соединяем вершины исключений
методов● Для каждого поля заводим по одной вершине
Interop
● Есть свой API для вызова JS, за счёт ухищрений на основе системы типов нельзя Java-объект передать в JS (но можно завернуть в JS-обёртку)
● Если html4j, придуманный Jaroslav Tulach. Там подобных ухищрений нет
● Вводим специальную вершину I● При вызове JS соединяем все аргументы вызова с I● Так же соединяем I с переменной, принимающей
результат● В худшем случае получим значительный false positive
Результаты
● Измерения проводились на jbox2d benchmark и на Hello, Kotlin.
● teavm-classlib.jar: 690 классов, 5725 методов● jbox2d-library.jar: 67 классов, 1259 методов● kotlin 1.0.3: 501 класс, 4557 методов
Результаты: jbox2d
● jbox2d.jar: 146 классов, 583 метода (46%)● teavm-classlib.jar: 90 классов, 209 методов (4%)● Всего: 236 классов, 792 методов (11%)● Девиртуализовано вызовов: 1846
– Vec2 set(Vec2): 349
– Vec2 subLocal(Vec2): 171
– Vec2 popVec2(): 116
● Среднее время симуляции секунды: 143 мс (до девиртуализации) против 136 мс (после)
Результаты: Hello, Kotlin
● kotlin: 5 классов, 4 метода (< 1%)● teavm-classlib: 90 классов, 160 методов (3%)● Всего: 95 классов, 164 метода (2%)● Девиртуализировано вызовов: 68
– int length(): 8
– StringBuilder append(String): 6
– char charAt(int): 5
Проблемы
● Не параллелится● Не дружественен с кэшем● Не инкрементальный● Плохо работает с reflection и с interop
Решения проблем с reflection
● (как реализовано) не использовать вообще, для задач, где в Java используется reflection, в TeaVM предлагается metaprogramming API
● (реализовано в невыложенной ветке) аннотировать классы и методы, доступные через reflection, в том числе дать API для того, чтобы иметь возможность задавать сложные правила. Например, делать доступными все методы, начинающиеся на test, если класс, где они находятся, является классом TestCase