TeaVM
Dead Code Elimination & Devirtualization
Что такое 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

TeaVM: dead code elimination and devirtualization

  • 1.
    TeaVM Dead Code Elimination& Devirtualization
  • 2.
    Что такое TeaVM ●Исследовательский проект ● AOT-компилятор байт-кода JVM ● Генерирует JS, WebAssembly ● Можно генерить что угодно (например, в рамках хакатона я сделал LLVM-бэкэнд) ● IR: static single assignment ● Оптимизации
  • 3.
    Минификация ● В миреJS крайне важно добиться настолько маленького размера бинарника, насколько это возможно ● Проблема: JDK большой. Даже сильно порезанный (привет, Jigsaw) занимает несколько мегабайт. Это неприемлемо ● Наблюдение: библиотеки бывают большие, из них часто используется очень небольшая часть функционала ● Вывод: нужен хороший dead code elimination
  • 4.
    Наивный DCE ● Строимориентированный граф вызовов ● Обходим, начиная с точки входа (функции main) ● Все помеченные методы компилируем, остальные выбрасываем ● Не поддерживает виртуальных вызовов
  • 5.
    Наивный DCE: пример voidfoo() { printf(“foo”) } void bar() { foo(); printf(“bar”); foo(); } void baz() { foo(); bar(); } void main() { foo(); bar(); } foo bar baz main printf
  • 6.
    Виртуальные вызовы ● Вкаждый вызов виртуального метода подставляем реализации из всех достижимых классов ● Достижимыми становятся методы, которые раньше, возможно, были недостижимыми ● Продолжаем обход, пока не сойдётся
  • 7.
    Виртуальные вызовы: пример <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
  • 8.
  • 9.
    DCE 2.0: Виртуальныевызовы ● Для каждого метода строим data flow graph ● Вершины - определения (переменные) SSA ● Рёбра - любые иснтрукции, перемещающие значение из одной переменной в другую (Phi, Assignment, Cast) ● Для некоторых рёбер заводим “фильтр”, например, для Cast ● Для каждой вершины заводим множество типов, изначально пустое
  • 10.
    DCE 2.0: invoke ●При статических вызовах соединяем переменные на call site с параметрами вызываемой функции ● Функция foo вызывает функцию v = bar(a1, a2, ...) ● bar объявлена как bar(p1, p2, ...) { ... } ● Соединяем рёбрами a1 -> p1, a2 -> p2, и т.д. ● Для каждого return u в функции bar соединяем u -> v
  • 11.
    DCE 2.0: propagate ●Для каждого u = new SomeType добавляем SomeType в множество типов u ● Протаскиваем типы по графу. Для каждой вершины, если в ней есть какой-то тип T, а в каких-либо соседях этого типа нет, добавляем T соседям ● Продолжаем, пока не сойдётся
  • 12.
    DCE 2.0: invokevirtual ●Пусть где-то в коде есть конструкция v = r.foo(a1, a2, ...) ● В r попадает новый тип T, которого там ещё не было ● Резолвим метод T.foo ● Если на данном call site ещё не встречался полученный метод, выполняем операции, аналогичные статическому invoke ● Продолжаем протаскивать типы, пока возможно ● После схождения компилируем все достижимые методы
  • 13.
    DCE 2.0: пример copy ctmp list get r add this e this main list tmp T(main.list) = { MyList } T(main.tmp) = { String } T(copy.c) = { ArrayList }
  • 14.
    DCE 2.0: пример copy ctmp 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
  • 15.
    Девиртуализация ● Обходим всеInvokeVirtual с ресивером r ● Берём множество T(r), находим все соответствующие реализации ● Если реализация одна, заменяем InvokeVirtual на InvokeSpecial ● Аналогично можно обрабатывать n-морфные вызовы ● В нашем примере T(get.this) = { MyList }, следовательно можно девиртуализовать ● T(add.this) = { ArrayList, MyList }, но MyList сам не определяет this, следовательно можно девиртуализовать
  • 16.
    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 соединяем вершины исключений методов ● Для каждого поля заводим по одной вершине
  • 17.
    Interop ● Есть свойAPI для вызова JS, за счёт ухищрений на основе системы типов нельзя Java-объект передать в JS (но можно завернуть в JS-обёртку) ● Если html4j, придуманный Jaroslav Tulach. Там подобных ухищрений нет ● Вводим специальную вершину I ● При вызове JS соединяем все аргументы вызова с I ● Так же соединяем I с переменной, принимающей результат ● В худшем случае получим значительный false positive
  • 18.
    Результаты ● Измерения проводилисьна jbox2d benchmark и на Hello, Kotlin. ● teavm-classlib.jar: 690 классов, 5725 методов ● jbox2d-library.jar: 67 классов, 1259 методов ● kotlin 1.0.3: 501 класс, 4557 методов
  • 19.
    Результаты: 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 мс (после)
  • 20.
    Результаты: 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
  • 21.
    Проблемы ● Не параллелится ●Не дружественен с кэшем ● Не инкрементальный ● Плохо работает с reflection и с interop
  • 22.
    Решения проблем сreflection ● (как реализовано) не использовать вообще, для задач, где в Java используется reflection, в TeaVM предлагается metaprogramming API ● (реализовано в невыложенной ветке) аннотировать классы и методы, доступные через reflection, в том числе дать API для того, чтобы иметь возможность задавать сложные правила. Например, делать доступными все методы, начинающиеся на test, если класс, где они находятся, является классом TestCase