22
TeaVM Dead Code Elimination & Devirtualization

TeaVM: dead code elimination and devirtualization

  • Upload
    -

  • View
    17

  • Download
    0

Embed Size (px)

Citation preview

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