В детстве мне часто говорили, что сахар — белая смерть. Позже я понял, что калории есть калории, а разглагольствующие о вреде сахара зачастую просто не владеют матчастью.

И вдруг выяснилось, что всё, чем меня пугали взрослые — чистая правда. Сахар жуткая вещь, которая убивает мозг и медленно ведёт нас к альцгеймеру. Его нельзя есть никому и никогда. Эта тема подробно раскрыта в книге Гэрри Тауба Good Calories Bad Calories, а также в книге Дэвида Перлмуттера Grain Brain.

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

К счастью, я наделён редким даром. Я всегда узнаю зло, и неважно в какие одежды оно вырядилось на этот раз.

Первую дозу нам заботливо (и абсолютно бесплатно) предлагают на официальном сайте. Там, прямо на стартовой странице, выложен код на кофескрипте и джаваскриптовый код, в который его превратит компайлер.

Надо отдать людям должное — они не пытаются вас обмануть. Приведённого на стартовой странице кода более чем достаточно, чтобы навсегда отказаться от синтаксического сахара и вообще десять раз подумать перед тем, как начать употреблять заморский напиток. Я не буду приводить код целиком — тем более, что каждый может поглядеть на него самостоятельно. Приведу лишь значимый кусок.

CoffeeScript


JavaScript


cubes = (math.cube num for num in list)

cubes = (function() {
  var i, len, results;
  results = [];
  for (i = 0, len = list.length; i < len; i++) {
    num = list[i];
    results.push(math.cube(num));
  }
  return results;
})();


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.


Посмотреть код тестов можно вот тут.

Update


Сведения изложенные в статье справедливы для node версии v0.10.40. В последней версии node и в последних версиях браузеров картина другая. В Хроме версии 45 код, сгенерированный CoffeeScript медленней, в файфоксе вроде быстрее. Хотя надо собрать побольше статистики.

Комментарии (67)


  1. lega
    13.10.2015 16:14
    +13

    Зачем вы пишите

     cubes = (math.cube num for num in list) 
    когда вы можете написать
     cubes = list.map math.cube 
    И получите тот же самый код что и в js. CoffeуScript просто дает вам варианты.

    Результаты уже не так однозначны. Но разница всё равно в полтора раза в пользу джаваскрипта.
    По идее вызов функции на каждую итерацию должен быть медленнее простого суммирования, возможно V8 научился хитро оптимизировать, но нужно будет перепроверить.

    PS: Вам надо было код вложить на jsperf, что-б любой желающий мог запустить, так же увидели бы результаты в разных браузерах.


    1. lega
      13.10.2015 16:20
      +8

      Вот добавил ваш последний тест на jsperf, в Chrome CS-цикл быстрее чем JS reduce более чем в 2 раза, а в Firefox CS-цикл быстрее в 20 раз.


      1. poxu
        13.10.2015 16:27
        -3

        Прикольно. Звезда в шоке. Я гонял тесты node.js.


      1. poxu
        13.10.2015 18:26
        -2

        Так. Вот тесты для суммирования массивов. У меня в файрфоксе и хроме зеркальная картина. Хром 45-ой версии.
        Далее. Нод, на котором я гонял тесты как выяснилось старый. Версии v0.10.40. В новом ноде пока не проверял сам, но друзья кофеманы говорят, что лямбды не рулят. Сейчас немного подумаю и добавлю апдейт с опровержением, или нову статью сделаю с разбором.


        1. RubaXa
          14.10.2015 09:05
          +1

          Все массивные методы написаны на JS и сильно избыточны, они априори не могут быть быстрей, почитайте/посмотрите Далтона, он в докладах про lodash хорошо разжевывает суть, ну и тестов у него ого-го.


          1. poxu
            14.10.2015 10:53

            По измерениям в немного устаревшей версии node и в текущей версии Chrome map быстрее. Ну и я верю, что в джаваскрипте рано или поздно начнут инлайнить функции.


            1. RubaXa
              14.10.2015 13:08

              Проверьте ваш тест, map/forEach/reduce и остальные избыточны, они всегда работают через `call`, но самое главное в них используется мега тормознутый `in`, выкинув его, мы уже получим 5х ускорение (но, да, таким образом мы получим иное поведение чем в native реализации, но вот нужно ли такое поведение? Скорее да, чем нет, ну и тесты говорят за себя).

              — Браузерные тесты: jsperf.com/arraymap/14 (как и ожидалось самый медленный нативный)
              — Вот ещё я накидал простенький тест, чтобы показать, где и как можно выиграть.

              Код теста
              'use strict';
              
              const MAX = 1000;
              const source = new Array(MAX);
              
              for (let i = 0; i < MAX; i++) {
              	source[i] = i * 2;
              }
              
              // Максимально приближеный к нативному
              function trueMyMap(array, callback, thisArg) {
              	const result = [];
              	const length = array.length;
              
              	for (let i = 0; i < length; i++) {
              		if (i in array) {
              			result.push(callback.call(thisArg, array[i], i, array));
              		}
              	}
              
              	return result;
              }
              
              // Простой и быстрый (без учета «дырок»)
              function simpleMap(array, callback, thisArg) {
              	const result = [];
              	const length = array.length;
              
              	for (let i = 0; i < length; i++) {
              		result.push(callback.call(thisArg, array[i], i, array));
              	}
              
              	return result;
              }
              
              // Оптимизированный (без учета «дырок»)
              function optimizedMap(array, callback, thisArg) {
              	const length = array.length;
              	const result = new Array(length);
              	const argsLength = callback.length;
              
              	for (let i = 0; i < length; i++) {
              		if (thisArg !== void 0) {
              			result.push(callback.call(thisArg, array[i], i, array));
              		} else if (argsLength <= 1) {
              			result.push(callback(array[i]));
              		} else if (argsLength === 2) {
              			result.push(callback(array[i], i));
              		} else {
              			result.push(callback(array[i], i, array));
              		}
              	}
              
              	return result;
              }
              
              // Хелпер для запуска тестов
              function perf(name, tests) {
              	console.log(name);
              
              	Object.keys(tests).forEach(function (name) {
              		const callback = tests[name];
              		console.time('  ' + name);
              
              		for (let i = 0; i < 1e4; i++) {
              			callback();
              		}
              
              		console.timeEnd('  ' + name);
              	});
              }
              
              //
              // Тесты
              //
              perf('Array#map(val)', {
              	'native': function () {
              		source.map(function (val) {
              			return val;
              		});
              	},
              
              	'trueMyMap': function () {
              		trueMyMap(source, function (val) {
              			return val;
              		});
              	},
              
              	'simple': function () {
              		simpleMap(source, function (val) {
              			return val;
              		});
              	},
              
              	'optimized': function () {
              		optimizedMap(source, function (val) {
              			return val;
              		});
              	}
              });
              
              perf('Array#map(val, idx)', {
              	'native': function () {
              		source.map(function (val, idx) {
              			return val * idx;
              		});
              	},
              	
              	'simple': function () {
              		simpleMap(source, function (val, idx) {
              			return val * idx;
              		});
              	},
              
              	'optimized': function () {
              		optimizedMap(source, function (val, idx) {
              			return val * idx;
              		});
              	}
              });
              
              perf('Array#map(val, idx, array)', {
              	'native': function () {
              		source.map(function (val, idx, array) {
              			return val * idx + array.length;
              		});
              	},
              
              	'simple': function () {
              		simpleMap(source, function (val, idx, array) {
              			return val * idx + array.length;
              		});
              	},
              
              	'optimized': function () {
              		optimizedMap(source, function (val, idx, array) {
              			return val * idx + array.length;
              		});
              	}
              });
              
              perf('Array#map(val, idx, array) + thisArg', {
              	'native': function () {
              		source.map(function (val, idx, array) {
              			return val * idx + array.length * this.foo;
              		}, {foo: 123});
              	},
              
              	'simple': function () {
              		simpleMap(source, function (val, idx, array) {
              			return val * idx + array.length * this.foo;
              		}, {foo: 123});
              	},
              
              	'optimized': function () {
              		optimizedMap(source, function (val, idx, array) {
              			return val * idx + array.length * this.foo;
              		}, {foo: 123});
              	}
              });
              


    1. poxu
      13.10.2015 16:21

      Могу и даже рекомендовал писать именно его в заключении. Но приведённый мной код — цитата с главной страницы сайта CoffeeScript. Поэтому он и был подвергнут разбору.
      За совет с jsperf спасибо.


  1. SerafimArts
    13.10.2015 16:20
    +3

    Открою секрет, что выхлоп кофе один в один такой, как написал бы человек. Если человек написал бы циклом for, то и выхлоп соответствующий, как если бы он написал циклом for, если бы воспользовался методом forEach или map, то соответственно.


    1. SerafimArts
      13.10.2015 16:27

      С другой стороны не всё так идеально, как хотелось бы. Вот пример на кофе:

      some = (args...) -> console.log args
      

      А вот результат:
      var some,
        slice = [].slice;
      
      some = function() {
        var args;
        args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
        return console.log(args);
      };
      


      Как недавно выяснил — в этом варианте есть небольшие проблемы: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments


    1. poxu
      13.10.2015 16:29
      -2

      Если бы array comprehensions были частью javascript, замечание было бы справедливым. Но там их нет и поэтому for из coffeescript имеет с for циклом столько же общего, сколько с ним имеет for in.


      1. SerafimArts
        13.10.2015 16:34
        +1

        for i in (arr[Symbol.iterator] || arr) работает точно так же (Ну почти, там ещё next надо брать) как for of в JS. Ну и наоборот: http://coffeescript.org/#try:for%20i%20of%20%7Ba%3A%201%2C%20b%3A%202%7D%0A%20%20console.log(i)

        Что с этими циклами не так?


        1. poxu
          13.10.2015 16:41

          for(i=0;i<array.length;++i);
          

          и

          for (item in array);
          

          Это две разные конструкции, вот что я хочу сказать.


          1. SerafimArts
            13.10.2015 16:47
            +1

            console.log i for i in array
            

            и

            console.log i for i of object
            


            Тоже две совершенно разные конструкции в кофе. И точно так же как в вашем примере, одна по массиву, другая по объекту. Я так и не понял какую мысль вы хотите донести.


            1. poxu
              13.10.2015 16:58
              -2

              Я хочу сказать, что проход по массиву в джаваскрипте делается с помощью forEach map и им подобными. Делается не только из соображений удобства, но и из соображений производительности. А coffeescript при трансляции выбирает медленный вариант.


              1. SerafimArts
                13.10.2015 17:16
                +3

                А что мешает написать на кофе:

                array.forEach (i) -> console.log i
                

                ?

                А кофе вообще ничего не выбирает. Это написавший написал цикл, а не вызвал метод.

                Да и цикл побыстрее будет метода. Я не понимаю почему вы ознакомившись с тестами http://habrahabr.ru/post/268753/#comment_8611909 до сих пор пытаетесь всех убедить, что это медленный вариант.

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


  1. nazarpc
    13.10.2015 16:32
    +2

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

    Вы ничего не перепутали? В JS массивы как массивы, там только цифровые индексы, и:

    var nums = [1, 2, 3];
    delete nums[1];
    console.log(nums.length);
    

    Ожидаемо выдаст 3, просто массивы здесь динамические по размеру. Но это не PHP, где массивы — это всё что угодно, только не обычные массивы.

    О cubes = list.map math.cube вам уже написали (хотя я предпочитаю ставить в таких случаях скобки, то есть единственное отличие будет в отсутствии запятой).
    А теперь попробуйте сделать такое на JS:

    cubes = (math.cube num for num in list by 2)
    

    И тогда мы поговорим о сравнении с чистым JS.

    Не стоит винить синтаксический сахар в том, что его используют неправильно.


    1. poxu
      13.10.2015 16:38
      -3

      Вы ничего не перепутали? В JS массивы как массивы, там только цифровые индексы, и:

      Увы, это не так. Массивы в джаваскрипте примерно такие же как в php. Собственно если в языке можно писать и array[1] и array['ke'] то они другими и быть не могут.

      Не стоит винить синтаксический сахар в том, что его используют неправильно

      Не стоит использовать его там, где в нём нет нужды. И тем более не стоит выкладывать примеры бесполезного использования на главную страницу.


      1. jj_killer
        13.10.2015 17:02
        +1

        Массивы в JS это обычные массивы, а то о чем вы пишите, хорошо объясняется тут: www.2ality.com/2011/08/array-prototype-performance.html


        1. poxu
          13.10.2015 17:14
          -1

          Когда вы говорите, что массивы в js это обычные массивы, вы имеете в виду, что они располагаются в одном непрерывном куске памяти?


          1. jj_killer
            13.10.2015 17:17

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


            1. poxu
              13.10.2015 17:26

              При том, что доступ к элементам массива, находящегося в непрерывном куске памяти происходит практически мгновенно. В отличие от хэша.
              И да, в джаваскрипте действительно не оговорено как располагать в памяти массив. Но оговорено, то индексы массивов могут быть строками и могут содержать дырки. Что естественным образом приводит к необходимости держать его в хэше. Вследствии чего доступ по индексу мендленный.


              1. lega
                13.10.2015 17:30
                +2

                При правильном использовании массивов, оптимизатор использует массивы, а не хеш таблицы.


              1. nazarpc
                13.10.2015 17:34

                Индексы не могут быть строками, не путайте. Массивы в JS — это объекты, так что array['ke'] это доступ к свойству этого объекта, не к элементу массива, так же как и более привычное array.ke.
                Дырки могут содержать, это да, размер-то массива может быть больше чем количество элементов в нём, ничего странного в этом нет. А в том же PHP размер массива при удалении сразу же меняется (мы не говорим сейчас об SPL семействе).


      1. SerafimArts
        13.10.2015 17:04
        +2

        > Увы, это не так. Массивы в джаваскрипте примерно такие же как в php. Собственно если в языке можно писать и array[1] и array['ke'] то они другими и быть не могут.

        Всегда считал, что в JS массивы — это объект Array (методы forEach. map, etc) с цифровыми ключами и Symbol.iterator методом, а в пыхе — это примитив в чистейшем виде, они совершенно разные как по поведению, так и по реализации (одно объект, другое примитив). Более того, если в JS квадратные скобки являются элементом доступа к полю объекта, то в пыхе за это отвечает ArrayAccess интерфейс и соответствующая реализация оного. Так же и за итерацию, в JS это Symbol.iterator, а в пыхе IteratorAggregate интерфейс (и прочие). Т.е. вы как минимум сравниваете тёплое с мягким. Просьба поправить, если я заблуждаюсь.


        1. poxu
          13.10.2015 17:21
          -2

          В php есть встроенные в язык массивы. Они — хэши. Ещё есть способ разрешить любому объекту весте себя как массив — ArrayAccess. Это как перегрузка оператора []. И есть настоящие массивы фиксированной длины с однотипными элементами.


          1. nazarpc
            13.10.2015 17:38

            Они не чистые хэши. Там весьма сложная внутренняя структура, которая включаэт и хэши, и списки, вот хорошая презентация: slides.catchy.io/PHP-Data-Structures-and-the-impact-of-PHP-7-on-them.html
            И (что немного парадоксально), они хотя и потребляют больше памяти, намного быстрее в типичных задачах за те же SPL реализации. В презентации много интересного.


            1. poxu
              14.10.2015 14:07

              Не чистые хэши, но одного непрерывного куска памяти за массивом в php нет. В документации так и написано — ordered map. Это даёт возможность бысть перебирать элементы по порядку, но не даёт быстрого доступа по индексу.

              Насчёт презентации — не подскажете ссылку на видео?


          1. SerafimArts
            13.10.2015 18:23
            +2

            Именно это я и написал с просьбой поправить следующее: Я высказался, что в js массивы вообще ничем не отличаются от обычного Object, включая способ доступа к элементам этого «массива», а в пыхе реализовать их невозможно по определению — передача по значению объекта невозможна на уровне реализации объекта.


    1. vintage
      13.10.2015 23:03

      Array.prototype['0'] = 'x'
      console.assert( Array(2).length === 2 )
      console.assert( Array(2)[0] === 'x' )


  1. SerafimArts
    13.10.2015 16:42

    del


  1. jj_killer
    13.10.2015 16:47
    +5

    Мне кажется вы немного теряете контекст, этот пример висит там уже без малого 5 лет. В то время, реально поддерживаемые браузерами возможности javascript немного отличаются от теперешних. Насколько я понимаю CoffeeScript до сих пор работает хорошо с IE8, и все это оставлено в угоду совместимости.


    1. poxu
      13.10.2015 17:02
      -4

      К счастью, IE8 в нашем мире осталось не так уж и много. Совместимость это хорошо, но когда совместимость достигается в угоду производительности это уже не так хорошо. Было бы неплохо ключом компиляции это настраивать хотя бы.


      1. SerafimArts
        13.10.2015 17:10
        +2

        Ну так map, reduce и прочие, как оказалось выше (http://habrahabr.ru/post/268753/#comment_8611909) в разы медленнее обычного цикла. Ключ для понижения производительности? А учитывая то, что выражение на кофе один в один будет как выражение на JS, то и в угоду читаемости выхлопа (а учитывая sourcemap, его вообще читать не нужно)?


      1. jj_killer
        13.10.2015 17:11

        Сделать отдельные ключи это не так просто, особенно если учесть что их нужно подвязывать на какую-то конкретную реализацию рантайма, например, node (у которого и своих ключей совместимости полно). И вообще не ясно, как все это будет влиять на реальную производительность.


      1. Envek
        14.10.2015 06:30
        +1

        Вот зря вы так. Мы тут на новом проекте еле-еле отбились от IE8. Он, к сожалению, много ещё где живее всех живых, особенно в мирах, где правит кровавый ынтырпрайз.


  1. o_nix
    13.10.2015 17:00

    Ровно в полночь раз в год появляется очередная статья о том, насколько же кофе ужасен и насколько плохо его читать тем, кто его не осилил. Не кажется ли, что это уже не модно?


    1. poxu
      13.10.2015 17:03
      -4

      Статья о том, что CoffeScript генерирует медленный код, а не о том, что его плохо читать.


      1. nazarpc
        13.10.2015 17:43
        +3

        Уважаемый, трюк в том, что при написании кода на CoffeeScript думать нужно на порядок больше, чем на JS.
        Читать код проще, понимать тоже в целом легко, если знаете синтаксис, но при написании кода нужно четко понимать не только то, что вы хотите получить, но и то, как оно будет выглядеть в JS.
        Вы же приводите два совершенно разных куска кода и говорите что один быстрее за другой. Он не может быть быстрее или медленнее просто по определению, вы используете совершенно разные подходы для достижения одной и той же цели!


        1. poxu
          13.10.2015 18:35
          +1

          Если при написании кода на CoffeeScript действительно надо думать на порядок больше, то на нём лучше не писать. Весь смысл использования языка, отличного от джаваскрипта в том, чтобы облегчить написание кода, а не усложнить его.

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


          1. SerafimArts
            13.10.2015 18:57
            -1

            Одно не противоречит другому. Паскаль, к примеру, очень простой язык, но не предпочитать же его, например шарпу. Так же и тут — кофе удобнее и мощнее ES5.


            1. poxu
              13.10.2015 19:31
              -1

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

              CoffeScript не может сделать ничего, что не может джаваскрипт, соответственно причина выбирать его — простота написания кода и ошибкобезопасность. Я уверен, если сообщить создателям CoffeeScript, что на их языке писать сложнее, чем на джаваскрипте, они ответят, что стремились создать язык, на котором писать проще.


              1. SerafimArts
                13.10.2015 20:02
                -1

                На паскале тоже это можно реализовать. Так же как на ES5 написать классы. А то что это долго делать и результат не очень красивый будет — это не так уж и важно, правда? Почему вы защищаете шарп, это ведь почти такой же сахар по сравнению с паскалем? И, я должен это сказать, боже, он медленнее!

                Я хочу донести до вас простую мысль — вы противоречите сами себе. Кофе нужен для того, что бы облегчить жизнь, так же как и любой более современный язык, по сравнению с более древним. Он сложнее для изучения, как и любой современный язык, но профита от него в разы больше при умелом использовании, код становится чище и читаемее. Но между паскалем и шарпом есть одна большая разница — это разные языки, корректнее сравнивать паскаль и дельфи.

                В вашей статье приводятся примеры совершенно разных языковых конструкций и немое удивление почему они работают по разному с разной скоростью. Это как написать «float a» на одном языке и «double a» на другом, удивляясь почему второй жрёт оперативы в два раза больше, даже не осознавая то, что можно и там и там написать и «float a». И читая подобную некомпетентность (точнее откровенное незнание кофе, как языка), у меня, признаюсь, очень сильно подгорает. =)


                1. poxu
                  13.10.2015 20:48

                  Чтобы реализовать всё, что есть в C# на паскале — придётся написать на нём vm и компилятор. И код в основном будет работать с той же скоростью, что С#. Игра не стоит свеч.

                  Гораздо лучше пример с С и С++. 2 разных языка, один является подмножетсвом другого, но все оптимизации, которые можно сделать для того, чтобы производительность С++ была такой же, как С — обязательно делаются.

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

                  Сравнение double c float тут некорректно, потому что такую ситуацию нельзя выловить компилятором. А ситуацию из статьи — можно. И приходится помнить, что конструкцию не надо применять, если шаг равен еденице. Это очень неприятно.

                  То, что вы классифицируете как незнание языка — на самом деле претензия к компилятору, который не оптимизирует ситуацию, которую оптимизировать можно.


                  1. SerafimArts
                    13.10.2015 21:16
                    +4

                    А вы понимаете что надо сравнивать код на кофе:

                    list.reduce ((prev, curr) curr + prev), 0
                    


                    С этим на JS:
                    list.reduce(function(prev, curr) {
                        return curr + prev;
                    }, 0);
                    


                    И соответственно код на Coffee:
                    cubes = (math.cube num for num in list)
                    


                    С этим кодом на JS:
                    var cubes = (function() {
                      var i, len, results;
                      results = [];
                      for (i = 0, len = list.length; i < len; i++) {
                        num = list[i];
                        results.push(math.cube(num));
                      }
                      return results;
                    })();
                    


                    Что бы уже были какие-то реальные претензии к транслятору и его оптимизациях.

                    Вы пишете цикл, а не вызываете метод и удивляетесь, что на выходе цикл, а не метод. Сколько раз это надо повторить всему сообществу хабра, что бы до вас наконец, простите, дошло, что вы сравниваете совершенно разные вещи?


                    1. poxu
                      13.10.2015 22:28
                      +1

                      Если бы CoffeeScript выполнялся браузером, а не транслировался в javascript, то смысл делать сравнения, о которых вы говорите, был бы.

                      А тут такое дело. Мы точно знаем во что компилятор транлирует cubes = (math.cube num for num in list). И знаем во что он мог бы его транслировать для улучшения производительности. И знаем, что причин не делать этой оптимизации нет. Если мне приведут причины не делать этой оптимизации, до меня дойдёт, что я сравнивал совершенно разные вещи.

                      Причина, которую приводите вы — нужно транслировать for в синтаксически сходную конструкцию. Я считаю, что сохранить синтаксис не важно, так как это не даст ничего — важно сохранить семантику. Она при такой оптимизации сохранится.


                      1. SerafimArts
                        13.10.2015 22:40
                        +1

                        В ваших словах есть смысл, если считать кофе отдельным языком. Но проблема в том, что кофе — это транслятор, один-в-один. Все оптимизации делает программист так же, как он делал бы это на JS. У него такая идеалогия. Во втором абзаце на главной странице так и написано: «The golden rule of CoffeeScript is: 'It is just JavaScript'.». Он не извращает язык, не оптимизирует, он просто сокращает синтаксис приводит его в такой вид, какой бы любой человек написал бы на JS. Именно в этом и причины отсутствия каких-либо сторонних манипуляций с кодом, всё транслируется слово-в-слово и можно дословно перевести js в кофе и обратно, не потеряв практически не единой строчки кода.

                        Если же нужны оптимизации — никто не мешает поверх навесить какой-нибудь гулповский uglifer или ещё что.


          1. nazarpc
            13.10.2015 18:58

            Если при написании кода на CoffeeScript действительно надо думать на порядок больше, то на нём лучше не писать. Весь смысл использования языка, отличного от джаваскрипта в том, чтобы облегчить написание кода, а не усложнить его.

            Знаете, а я предпочитаю больше думать и меньше писать. А вообще, код чаще читают чем пишут (несравнимо чаще), и вот читать CoffeeScript при разумном использовании существенно легче, полагаю, для этого он и создан в первую очередь (радикально меньше визуального мусора).
            Ну и если есть 2 совершенно разных куска кода, которые используются для достижения одной и той же цели и один кусок быстрее другого, то лучше использовать инструмент, который генерит быстрый код.

            При чем тут генерация вообще? Если бы у вас был шаг не 1, а 2 (как в моем примере выше) — у вас бы код был аналогичной конструкции.
            На CoffeeScript можно и нужно писать cubes = list.map(math.cube) — это короче, понятнее и (судя по тому, что вы доказывали в статье) даже быстрее.
            А то, что вы на JS взяли простой вариант, а на CoffeeScript взяли неудачный пример из документации (которая просто показывает что так можно делать, но не обязательно нужно) — это ваша личная проблема и ни чья больше. Повторюсь ещё раз, cubes = list.map(math.cube) — вилидный CoffeeScript, который не быстрее и не медленнее такой же строчки на JS. А то, что вы написали называется высасывание проблемы с пальца.


            1. poxu
              13.10.2015 19:44

              Знаете, а я предпочитаю больше думать и меньше писать

              История развития языков программирования это история того, как сделать так, чтобы при написании кода можно было меньше думать. Если думать надо больше, то и неверных решений будет принято больше и ошибок значит будет больше.
              Если бы у вас был шаг не 1, а 2 (как в моем примере выше) — у вас бы код был аналогичной конструкции.

              Если есть способ оптимизировать случай с шагом в 1 — надо это сделать.


              1. nazarpc
                13.10.2015 23:16
                +1

                Если есть способ оптимизировать случай с шагом в 1 — надо это сделать.

                Не совсем, код делает ровно то, что вы написали. Хотите пройтись циклом и вернуть массив значений — код делает ровно это и ничего больше. На JS код был бы аналогичный выхлопу компилятора CoffeeScript. Если хотите применить .map — код опять абсолютно аналогичный.
                Ваши примеры НЕ эквивалентны, они совершенно разные, как вы не поймете это! Что написали — то и получили.


  1. poxu
    13.10.2015 19:44

    комментарий удалён


  1. dom1n1k
    13.10.2015 19:51

    Что-то я не совсем понял, почему map внезапно превратился в reduce?


    1. poxu
      13.10.2015 19:58
      -1

      Потому, что код, сгенерированный CoffeeScript внутри устроен как reduce. Там есть накопитель, и цикл с его наполнением. map делает массив по размеру равный исходному и потом просто присваивает элементам массива нужные значения. Поэтому логично сравнивать код, сгенерированный CoffeeScript с reduce, а не с map.


      1. dom1n1k
        13.10.2015 20:06

        Ну, во-первых, это сугубо его проблемы, как он устроен. Главное это смысловая суть кода.
        А во-вторых, там нет вызова функции на каждой итерации. Так что я совершенно не уверен, что это справедливая замена.


  1. Zibx
    13.10.2015 22:54

    Как я и подозревал скорость срезалась в основном поиском функции cube в пачке замыканий вплоть до window. Вот пример с подтягиванием поближе.


    1. Zibx
      13.10.2015 23:08

      Map мог оказаться быстрее цикла только за счёт оптимизации взятия элемента массива и оптимизации складывания в новый, но forEach всегда медленнее обычного цикла из-за накладных расходов на вызов функции. Самым оптимальным решением в данном случае будет раскрыть функцию и написать x*x*x прям в теле цикла.


      1. dom1n1k
        13.10.2015 23:31

        Насчет forEach — да, по всей логике и здравому смыслу он должен быть медленнее. Но на практике — нифига! Я как-то тестировал и довольно много сравнивал — вполне сопоставимо он работает, а иногда даже существенно быстрее. У меня нет внятного объяснения этого феномена, но факт есть факт.


      1. poxu
        13.10.2015 23:46

        А вызовы функций не инлайнит никто?


  1. Yuuri
    14.10.2015 12:18

    Эта тема подробно раскрыта в книге Гэрри Тауба Good Calories Bad Calories, а также в книге Дэвида Перлмуттера Grain Brain.

    Лучше бы она была раскрыта в научно достоверных статьях из реферируемых источников.


    1. poxu
      14.10.2015 14:21

      Она раскрыта. В книгах этих ссылок много.


      1. Yuuri
        14.10.2015 18:37

        Вас не затруднит привести несколько? Как-то не хочется только ради этого книжку покупать.


        1. poxu
          14.10.2015 20:03

          Несколько прям сразу сложно. Вот ссылка, которая оказалась под рукой www.ncbi.nlm.nih.gov/pubmed/24115747.


  1. rock
    14.10.2015 13:39
    +1

    Вы не понимаете несколько простых вещей. Да, выхлоп кофе неоптимален, но исключительно из-за использования .push вместо создания массива с заранее заданной длинной. В современных js движках массивы как раз таки массивы, а не хэши, если содержимое однотипно. В V8 методы массивов, вы не поверите, написаны на том же самом js и мало отличаются от представленного выше «оптимизированного» выхлопа кофе — тот же самый цикл и получение / установка элементов по индексу. Вот только с ненужной нам проверкой «дырки» в массиве — необходимо по стандарту. Да, и V8 инлайнит функции, но не коллбэки .map и ему подобных методов (хотя может что в последнее время и изменилось).


    1. poxu
      14.10.2015 14:01

      Да, действительно не понимал. Спасибо за комментарий и за ссылки.


  1. Valery4
    14.10.2015 16:43
    +5

    К счастью, я наделён редким даром. Я всегда узнаю зло, и неважно в какие одежды оно вырядилось на этот раз.

    После этой фразы — мне стало совершенно понятно, что автор будет выступать в роли истины в последней инстанции…


  1. seth2810
    15.10.2015 17:46

    А почему никто не попробовал убрать вычисление длины массива на каждой итерации и закэшировать его например в пременной. Кажется, что в этом есть некоторая толика отличия for от map?


    1. poxu
      16.10.2015 00:21

      Да оно кешируется.

      for (i = 0, len = list.length; i < len; i++)
      

      И в последних версиях нода и в файрфоксе for быстрее, чем map.
      Вся ценность статьи в коментаторах, которые объясняют, что в ней не так.