На графике снизу видно, что производительность кода, сгенерированного dart2js (фиолетовая линия), теперь немного превышает код, написанный от руки на JavaScript (золотая линия). Верхняя линия — код, написанный на Dart и запущенный на Dart VM. Чем выше линия на графике, тем выше производительность.
Вывод типов и производительность
Николас Геофрей, разработчик dart2js, работает над системой типизации в Dart. Николас в своем интервью сравнивает примеры старого генерированного кода и нового оптимизированного.
Меньше и быстрее
Фрагмент кода Dart из оригинального теста на производительность:
void addConstraintsConsumingTo(Variable v, List<Constraint> coll) { Constraint determining = v.determinedBy; for (int i = 0; i < v.constraints.length; i++) { Constraint c = v.constraints[i]; if (c != determining && c.isSatisfied()) { coll.add(c); } } }
Старый dart2js, генерировал следующий код (почему так, рассмотрим далее):
addConstraintsConsumingTo$2: function(v, coll) { var determining, i, t1, c; determining = v.get$determinedBy(); i = 0; while (true) { t1 = $.get$length$as(v.get$constraints()); if (typeof t1 !== "number") return bailout(1, v, coll, i, determining, t1); if (!(i < t1)) break; t1 = v.get$constraints(); if (t1.constructor !== Array) return bailout(2, v, coll, i, determining, t1); if (i >= t1.length) throw $.ioore(i); c = t1[i]; if (!$.$eq(c, determining) && c.isSatisfied$0() === true) coll.push(c); ++i;
С ревизии #20130 данный код стал выглядеть так:
addConstraintsConsumingTo$2: function(v, coll) { var determining, t1, i, c; determining = v.determinedBy; for (t1 = v.constraints, i = 0; i < t1.length; ++i) { c = t1[i]; if ((c == null ? determining != null : c !== determining) && c.isSatisfied$0()) { coll.push(c); } }
Текущий вариант очевидно меньше и быстрее по следующим причинам:
- Удалены bailout (return bailout(1…));
- Удалены проверки типов (typeof t1 !== «number»);
- Изменен цикл с while на for;
- Вызовы функции заменены на доступ к полям (v.get$determinedBy() теперь v.determinedBy);
Null семантика
Помните, что Dart — это не просто синтаксический сахар для JS, это отдельный язык со своей семантикой и библиотеками. Эти семантики также должны быть представлены в скомпилированном JavaScript. При этом глобальный вывод типов может помочь снизить уровень детализации JavaScript, сохраняя семантики операций.
Null семантика — хороший пример для сравнения между Dart и JS. Рассмотрим следующий фрагмент кода.
var x = null; var y = 1; var results = x + y; // Dart бросает NoSuchMethodError потому что в объекте null // нет определения + оператора. // Однако, JavaScript установит результат в 1.
Многие разработчики ожидают аналогичное поведение, как в Dart, так и в JS, dart2js должен поддерживать данную семантику. Dart может определить на уровне компиляции, что x никогда не будет null, поэтому dart2js исключит данные проверки.
var x = null; var y = 1; var results = x + y; print(results);
Заметьте, x принимает значение null, поэтому Dart сгенерирует следующий код:
$.main = function() { $.Primitives_printString($.toString$0($.JSNull_methods.$add(null, 1))); };
Если же x не будет принимать null значение, то dart2js исключит проверки:
$.main = function() { $.Primitives_printString($.JSInt_methods.toString$0(3)); };
Рассмотрим более сложный пример. Обратите внимание на полное отсутствие аннотаций типов. Это означает, что dart2js вычисляет типы на основании использования объектов, а не с помощью аннотаций (помните, Dart является опционально типизированным языком, поэтому аннотации игнорируются во время исполнения).
add(x , y) { var z = x * 2; return z + y; } main() { var x = null; var y = 1; var result = add(x, y); print(result); }
Так как x может принимать null значение, dart2js сгенерирует следующий код:
$.main = function() { $.Primitives_printString($.toString$0($.$add$ns($.JSNull_methods.$mul(null, 2), 1))); }; $.$add$ns = function(receiver, a0) { if (typeof receiver == "number" && typeof a0 == "number") return receiver + a0; return $.getInterceptor$ns(receiver).$add(receiver, a0); };
Теперь предположим, что x не может принимать значение null:
add(x , y) { var z = x * 2; return z + y; } main() { var x = 2; var y = 1; var result = add(x, y); print(result); }
Тогда, сгенерированный код примет следующий вид:
$.main = function() { $.Primitives_printString($.JSInt_methods.toString$0(5)); };
Я бы сказал, впечатляет! Обратите внимание, что Dart произвел все необходимые математические действия и спустился до числа 5 (2×2 + 1).
Еще один пример, как dart2js относится к null. Рассмотрим код, который явно производит проверки на null:
add(x , y) { if (x == null || y == null) throw new ArgumentError('must not be null'); var z = x * 2; return z + y; } main() { var x = null; var y = 1; var result = add(x, y); print(result); }
В некотором смысле, в данном примере вы говорите «Предположим, что данные переменные могут пройти проверку на null». Тогда сгенерируется следующий код:
$.add = function(x, y) { if (x == null || false) throw $.$$throw($.ArgumentError$("must not be null")); return $.$add$ns($.JSNull_methods.$mul(x, 2), y); }; $.main = function() { $.Primitives_printString($.toString$0($.add(null, 1))); };
Не так хорошо, как когда x и y не null, то все же лучше, чем код, созданный без проверок на null.
Основным моментом, является то, что dart2js поддерживает семантику Dart в сгенерированном JS коде. С выводом типов сгенерированный код умнее и быстрее без ущерба семантике.
Семантика индексов
Различного поведение между между Dart и JS наблюдается в случае запроса элемента, индекс которого выходит за границы массива или списка. Рассмотрим код:
var fruits = ['apples', 'oranges']; var fruit = fruits[99]; // Dart бросает RangeError, потому что fruits содержит только 2 элемента. // JavaScript же установит fruit в неопределенное значение.
Опять же многие разработчики, ожидают, что сработает исключение, когда кто-то пытается выйти за допустимые границы. Для сохранения этого поведения dart2js должен вставить дополнительные проверки:
$.main = function() { var fruits = ["apples", "oranges"]; if (99 >= fruits.length) throw $.ioore(99); $.Primitives_printString($.toString$0(fruits[99])); };
Однако dart2js может определить выходит ли индекс за границы или нет на уровне компиляции и, следовательно. оптимизировать код. Есть два пути указать список, размеры которого будут известны на этапе компиляции – использовать константные списки или списки фиксированного размера.
Пример, использующий константные листы:
main() { var fruits = const ['apples', 'oranges']; var fruit = fruits[99]; print(fruit); }
В этом случае dart2js сгенерирует следующий код:
.main = function() { throw $.ioore(99); $.Primitives_printString($.toString$0($.List_apples_oranges[99])); }; // ... $.List_apples_oranges = Isolate.makeConstantList(["apples", "oranges"]);
Обратите внимание, что исключение будет сразу же брошено, потому что компилятор знает, что 99 выходит за границы.
Подводя итоги
Компилятор dart2js, конвертирующий код из Dart в JavaScript, должен поддерживать семантику Dart’а и в сгенерированном коде. В связи с новыми обновлениями dart2js проводит анализ кода и устраняет ненужные проверки на типы, диапазоны и другой не нужный код, сохраняя при этом семантику Dart’а.
Это означает, что сгенерированного кода теперь меньше, и зачастую он быстрее.
Не вся работа еще закончена, впереди много изменений, поэтому пока команда Dart остерегает от использований данных особенностей компилятора dart2js.
Если же у вас есть код, который может быть сгенерирован более оптимально, пожалуйста, сообщайте.
Из-за маленькой кармы, не могу писать в хаб «Переводы», оригинал статьи на Dart News.
ссылка на оригинал статьи http://habrahabr.ru/post/175615/
Добавить комментарий