Почему dart2js быстрее JavaScript?

в 10:18, , рубрики: dart, performance, переводы, метки: ,

Теперь компилятор dart2js, транслирующий код из Dart в JavaScript, генерирует более быстрый код. Анализируя программу, компилятор может убрать избыточные проверки, в результате чего, код работает быстрее на современных движках JavaScript.

На графике снизу видно, что производительность кода, сгенерированного dart2js (фиолетовая линия), теперь немного превышает код, написанный от руки на JavaScript (золотая линия). Верхняя линия — код, написанный на Dart и запущенный на Dart VM. Чем выше линия на графике, тем выше производительность.

image

Вывод типов и производительность

Николас Геофрей, разработчик 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);  
    }  
   }

Текущий вариант очевидно меньше и быстрее по следующим причинам:

  1. Удалены bailout (return bailout(1...));
  2. Удалены проверки типов (typeof t1 !== «number»);
  3. Изменен цикл с while на for;
  4. Вызовы функции заменены на доступ к полям (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 (2x2 + 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.

Автор: Zapletnev

Источник

Поделиться

* - обязательные к заполнению поля