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

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

-jd4h2dd9vfoox10orpz51tytl0.png

Как вы думаете, что произошло? Код практически одинаковый, единственное отличие — в 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.

© Habrahabr.ru