CoffeeScript array comprehensions — модно, стильно, медленно
В детстве мне часто говорили, что сахар — белая смерть. Позже я понял, что калории есть калории, а разглагольствующие о вреде сахара зачастую просто не владеют матчастью.
И вдруг выяснилось, что всё, чем меня пугали взрослые — чистая правда. Сахар жуткая вещь, которая убивает мозг и медленно ведёт нас к альцгеймеру. Его нельзя есть никому и никогда. Эта тема подробно раскрыта в книге Гэрри Тауба Good Calories Bad Calories, а также в книге Дэвида Перлмуттера Grain Brain.
Но речь в статье не об этом. Недавно я обнаружил, что группа моих товарищей плотно сидит на CoffeeScript. Они ничуть не стесняются этого факта и даже умудряются испытывать в процессе какое-то противоестественное удовольствие. Более того, они не чураются использовать содержащийся в кофе синтаксический сахар и даже хотели пристрастить к этому меня.
К счастью, я наделён редким даром. Я всегда узнаю зло, и неважно в какие одежды оно вырядилось на этот раз.
Первую дозу нам заботливо (и абсолютно бесплатно) предлагают на официальном сайте. Там, прямо на стартовой странице, выложен код на кофескрипте и джаваскриптовый код, в который его превратит компайлер.
Надо отдать людям должное — они не пытаются вас обмануть. Приведённого на стартовой странице кода более чем достаточно, чтобы навсегда отказаться от синтаксического сахара и вообще десять раз подумать перед тем, как начать употреблять заморский напиток. Я не буду приводить код целиком — тем более, что каждый может поглядеть на него самостоятельно. Приведу лишь значимый кусок.
CoffeeScript |
JavaScript |
|
|
list это массив, а math.cube, как нетрудно догадаться, функция, возводящая входной параметр в куб.
Джаваскриптовый код кошмарен
Никто конечно в здравом уме и светлой памяти такого кода сам бы не написал. За этим многобуквием скрывается простая и известная конструкция.
cubes = list.map(math.cube);
Что, кстати понятнее и короче, чем кофемановый вариант.
А в чём собственно проблема
Да, с эстетической точки зрения джаваскриптовый код просто ужасен, но когда я намекнул на это товарищам, мне обоснованно возразили, что это выхлоп компилятора, который нормальные люди не читают. Напротив, надо посмотреть на этот код и порадоваться, что тебе никогда не придётся писать его самому.
Точка зрения не новая, многократно подтверждённая на практике такими монстрами как, например, Бьёрн Страуструп. Если компилятор не порет косяков, то действительно лучше довериться ему, расслабиться и получать удовольствие.
Однако одного взгляда на приведённый выше код достаточно для того, чтобы осознать — при виде сахара компилятор теряет разум. В первую очередь в глаза бросается постинкреметный оператор, но он, конечно, погоды не делает.
Настоящая проблема в том, что код, сгенерированный компилятором, примерно в 3 раза медленнее, чем cubes = list.map(math.cube);. Да, совершенно верно, основательно закинувшись сахаром, можно совершенно бесплатно троекратно замедлить часть кода не получив взамен ничего.
Как так вышло
Причина проста и прозаична до безобразия. То, что в javascript называется массивами, в других языках программирования называется хэшами, а время доступа по ключу к элементу хэша это не тоже самое, что время доступа к элементу массива по индексу. С другой стороны, элементы хэша связаны в список, поэтому последовательный перебор всех элементов можно сделать быстро и просто. Не так быстро и не так просто, как элементы настоящего массива, но в разы быстрее, чем это делает код, сгенерированный компилятором CoffeeScript.
Может мы на самом деле измеряем разные вещи
Как некоторые наверное уже заметили — cubes = list.map(math.cube); делает то же самое, что код на кофескрипте, но на самом деле не является его прямым отображением. Точно отобразить этот код можно так:
cubes = list.reduce(function(prev, curr) {
return prev.concat(math.cube(curr));
}, []);
Может быть дело в том, что в у нас каждый раз возвращается новый массив? Может быть всё дело в .push и в том, что массив приходится выращивать? Ну то есть понятно, что это невозможно, но вдруг? Может надо так?
list.reduce(function(result, curr) {
result.push(math.cube(curr));
return result;
}, []);
Это вообще отображение один в один, всё за исключением перебора взято из оригинала. Но даже такой код всё равно в 3 раза быстрее, чем код с заглавной страницы CoffeeScript.
Но можно быть ещё правдивей
А может быть дело не только в доступе к элементам? Давайте попробуем просто суммировать значения элементов массива, а не перекладывать их в новый массив.
Сравним этот код:
var sum = (function(list) {
var i, len, sum = 0;
for (i = 0, len = list.length; i < len; i++) {
var num = list[i];
sum += num;
}
return sum;
}(list));
Вот с этим:
var sum = list.reduce(function(prev, curr) {
return curr + prev;
}, 0);
Результаты уже не так однозначны. Но разница всё равно в полтора раза в пользу джаваскрипта.
Такие дела
С моей точки зрения, если технология прямо с порога пробивает с ноги делает такие заявки, это повод серьёзно подумать перед тем, как начинать её использовать. Конечно, если вы балуетесь CoffeeScript уже давно, и он вас радует — продолжайте баловаться — кофе сам по себе ещё никого не убил. Особенно учитывая, что лямбды безо всяких сложностей можно использовать в CoffeeScript и компилятор транслирует их именно в лямбды, у которых, как мы сейчас выяснили, проблем со скоростью нет.
Но вот с сахаром, по крайней мере с некоторыми его видами, лучше завязать — sugar is bad for ya.
P.S.
Посмотреть код тестов можно вот тут.