Когда TypeScript превосходит JavaScript в тестах на скорость +14



Этот пост я пишу в ответ на этот, где сравниваются разные тесты производительности, в том числе одних и тех же алгоритмов, написанных на TypeScript и JavaScript. Как известно многим, первый при релизе переводится во второй. У TypeScript нет своей нативной поддержки в браузерах, нет собственного движка. Более того, многие плюшки этого языка при транспилировании отбрасываются, чтобы получить чистый JS, который можно запускать во всех браузерах (если хотите, даже в Explorer). Хорошо. А теперь смотрите на картинку.



Как вы думаете, что произошло? Код практически одинаковый, единственное отличие — в JS-версии отсутствует информация о типах переменных. Но разрыв в скорости — фундаментальный.

Сначала я тестировал на 10 миллиардах циклов и мне показалось, что браузер просто завис. Но нет, просто под Хромом версия на JS работала 250 секунд, а транспилированная из TS — 15 секунд. Это может взорвать мозг и мне это действительно взорвало, хотя я уже знал об этой особенности TypeScript.

Давайте посмотрим, что произошло. Вот код на TypeScript в текстовом виде:

const fn = (x : number, y : number) => x+y;
console.log(`start `);
let t1 = Date.now();
let sum = 0;
for(let i=0;i<1000000000;i++){
    sum = fn(sum, i);
}
console.log(`end. time ${(Date.now()-t1)/1000} seconds`);

Я взял функцию суммирования, так как она была приведена mayorovp в комментариях к исходному посту о тестах. Действительно, я полностью согласен, что следующие виды записи в TypeScript при переводе в продакшен, то есть в JS-виде, ничем не будут отличаться:

const fn = (x : number, y : number) => x+y;
const fn = (x , y) => x+y;

Я немного сократил тот пример, чтобы показать, что информациях о типах действительно отбрасывается и в финальном билде на JS мы получим одно и то же. И вот то, что мы получаем, и является секретом высокой производительности TypeScript в тестах:

console.log("start ");
for(var n=Date.now(),r=0,o=0;o<1e9;o++)r=r+o; // вот она, наша функция sum
console.log("end. time "+(Date.now()-n)/1e3+" seconds")


Вызов функции TypeScript-транспилер превратил в простую операцию суммирования прямо в теле цикла, что и вызвало превосходство в производительности над кодом, написанным просто в JS. Да, это известный прием — инлайнинг функций в цикле, то есть запись непосредственно их кода вместо вызова. Так можно писать и самому, но код от инлайнинга разбухает и во время коде-ревью
становится так жарко, что месячный запас розог уходит за два часа, а ведь впереди еще дедлайны!

Создатели TypeScript думают над оптимизацией кода при транспайлинге — по крайней мере, на данном этапе развития языка, за что им огромное спасибо. Эти операции часто не очевидны и, к примеру, const не является аналогом inline в С++ (хотя для данных, не для функций, подстановка по месту происходит практически всегда вместо обращения к переменной — это имеет значение, так как скорость обращения к локальным и глобальным переменным отличается).

Для функций подстановка по месту вызова не зависит от const — функция суммирования из примера в цикле будет превращаться просто в оператор сложения при разных формах записи, включая не-стрелочную форму. Более того, если из одной функции вызывать другую — скорее всего, одна из функций не будет развернута и будет вызываться в цикле. Я предлагаю самостоятельно изучить разные короткие алгоритмы в TypeScript и посмотреть, в какой код на js они превращаются в итоге. Некоторые операции очевидны, некоторые — непонятны.

Но эти оптимизации — есть, и именно они могут создавать эффект превосходства вроде бы тех же самых алгоритмов. Вот это и есть ответ на вопрос, заданный zolotyh — как именно может typescript работать быстрее, чем javascript, и при этом кушать в несколько раз меньше памяти?

Хотя скорее всего, это не единственная причина и отнюдь не гарантия ультимативного превосходства вашего проекта на TypeScript по сравнению с проектом коллеги, пишущего на ES6.

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (12):

  1. Zavtramen
    /#19503028 / -1

    Весьма синтетический пример, имхо. Когда у нас есть сложная алгоритмическая задача, ну или перебор там какой, мы сами ручками будем все оптимизировать: разворачивать функции, переносить все переменные во внутренний скоуп, дублировать код чтобы уменьшить кол-во итераций цикла и пр. В остальных рядовых случаях прирост будет ничтожен, когда надо выполнить 10, 100, 1000 циклов. Я не против тайпскрипта если что, просто статья пытается намекнуть, что бывают ситуации когда тайпскритп может быть производительнее чистого JS, но в реальности это происходит только в синтетических тестах.

    • Zoolander
      /#19503034

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

      Речь идет о том, что TS внезапно в принципе местами может генерировать более быстрый код.

    • Fesor
      /#19503040

      Я думаю суть в том что информация о типах штука полезная.

      Что до синтетики — пару лет назад я делал какие-то простенькие алгоритмы обработки изображений в canvas (webgl шейдеры конечно с этим всем справляются лучше но там нужно было именно без webgl) и мне как раз приходилось разворачивать циклы и прочие непотребства. Ну и там как раз были итерации по миллиону штук за раз. Потому в целом кейс найдется. И да, инлайнинг компилятором позволил бы мне писать код чуть менее мерзский нежели тот который был.

      • Zavtramen
        /#19503044 / +1

        Ну я к тому, что обычно такие кейсы не оставляют на откуп компиляторам/интерпретаторам, а сами ручками оптимизируют, теплый ламповый хендмейд. Впрочем, для этого обычно требуется изрядный опыт и знание подкапотных процессов. Впрочем, нет ничего плохого в том, что что-то умеет делать и тайпскрипт при транспайлинге )

      • Zoolander
        /#19503060

        да, самое ценное — это типизация, но оказалось, что и других плюшек куча )
        я бы предпочел явный оператор inline, но и за автоматическую оптимизацию благодарен, тем более, что пока у меня нет реальной необходимости укрощать миллионы итераций (а если появится, я сначала спрошу себя — не сошел ли я с ума и не пишу ли алгоритм сортировки пузырьком?)

        • Fesor
          /#19503068

          я бы предпочел явный оператор inline

          Тогда нужно проверку делать что сайд эффектов нет. Ну то есть можно но зачем?


          не сошел ли я с ума и не пишу ли алгоритм сортировки пузырьком?

          пузырек оно не оптимизирует, увы. Зависимость по данным.

      • Taraflex
        /#19503174

        мне как раз приходилось разворачивать циклы и прочие непотребства.

        uglifyjs (хотя я предпочитаю www.npmjs.com/package/terser ) умеет если включить некоторые небезопасные настройки.

    • Zoolander
      /#19503042

      самое главное, что человек при желании может делать оптимизации по скорости намного круче

      я сейчас немного устал и косноязычен, поэтому эту мысль в статье немного зажевал. Но повторю — там, где человек из нескольких функций может сделать 1 формулу, TS фигачит почему-то только 1 формулу, а в ней вызов остальных функций.

      const fn1 = (x, y) => x + y;
      const fn2 = (x, y) => fn1*2 + x + y;
      
      // fn2 в цикле человек может превратить в формулу
      // 2*(x+y) + x + y
      // а транспайлер просто дает что-то вроде 2*fn1 + x + y
      
      

      • vintage
        /#19503196 / +1

        TS ничем таким не занимается. Прекратите вводить людей в заблуждение. А статьи лучше писать на свежую голову, хорошо проверив факты, а то и попросив знакомых программистов проревьюить.

  2. ReklatsMasters
    /#19503078 / +2

    До ката я был уверен, что дело в какой-то INT32 магии, неявном приведении типов… Во всей статье вы рассказываете о том, как хорошо тайпскрипт ракрывает формулы и оптимизирует. Но вы ничего не говорите о том, что V8 тоже может оптимизировать.
    Такие простые функции V8 инлайнит даже не задумываясь. Да и многие другие вызовы движки также оптимизируют.
    И опыт ваш я не смог повторить. На node 10.14.1 оба варианта цикла отдают примерно одинаковый результат.


    const fn = (x, y) => x + y;
    
    console.time('js');
    let sum = 0;
    for (let i=0; i < 1e9; i++){
      sum = fn(sum, i);
    }
    // for(var r=0,o=0;o<1e9;o++) r=r+o;
    console.timeEnd('js');

  3. Taraflex
    /#19503162 / +1

    console.log("start ");
    for(var n=Date.now(),r=0,o=0;o<1e9;o++)r=r+o; // вот она, наша функция sum
    console.log("end. time "+(Date.now()-n)/1e3+" seconds")

    Это точно не результат после uglifyjs (или terser), который умеет инлайнить функции? Как уже написали выше чистый ts компилятор не проводит данные оптимизации.
    Также ts из коробки не переименовывает переменные по мере возможности, а если и переименовывает то старается сделать это узнаваемым способом (добавляет нижнее подчеркивание и подобное).