почему последняя функция на 10% быстрее, хотя она должна создавать переменные снова и снова?

14
var toSizeString = (function() {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

  return function(size) {
    var gbSize = size / GB,
        gbMod  = size % GB,
        mbSize = gbMod / MB,
        mbMod  = gbMod % MB,
        kbSize = mbMod / KB;

    if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
    } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
    } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
    } else {
      return size + 'B';
    }
  };
})();

И более быстрая функция: (обратите внимание, что она всегда должна вычислять одни и те же переменные kb / mb / gb снова и снова). Где это получить производительность?

function toSizeString (size) {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

 var gbSize = size / GB,
     gbMod  = size % GB,
     mbSize = gbMod / MB,
     mbMod  = gbMod % MB,
     kbSize = mbMod / KB;

 if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
 } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
 } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
 } else {
      return size + 'B';
 }
};
томия
источник
3
В любом статически типизированном языке «переменные» будут скомпилированы как константы. Возможно, современные движки JS способны выполнять такую ​​же оптимизацию. Это, кажется, не работает, если переменные являются частью замыкания.
USR
6
это деталь реализации движка JavaScript, который вы используете. Теоретическое время и пространство одинаковы, только реализация данного движка JavaScript изменит их. Поэтому, чтобы правильно ответить на ваш вопрос, вам нужно перечислить конкретный движок JavaScript, с которым вы их измеряли. Возможно, кто-то знает подробности его реализации, чтобы сказать, как / почему он сделал один более оптимальный, чем другой. Также вы должны опубликовать свой код измерения.
Джимми Хоффа
вы используете слово «вычислить» в отношении постоянных значений; там действительно нечего вычислять в том, на что вы ссылаетесь. Арифметика постоянных значений является одним из самых простых и очевидных оптимизационных компиляторов, поэтому всякий раз, когда вы видите выражение, которое имеет только постоянные значения, вы можете просто предположить, что все выражение оптимизировано до одного постоянного значения.
Джимми Хоффа
@JimmyHoffa это правда, но, с другой стороны, ему нужно создавать 3 постоянные переменные при каждом вызове функции ...
Томи
Константы @Tomy не являются переменными. Они не меняются, поэтому их не нужно создавать заново после компиляции. Константа , как правило , помещаются в памяти, и каждый будущий охват для этой константы направлен на то же место, нет никакой необходимости , чтобы воссоздать его , потому что это значение никогда не будет меняться , поэтому он не является переменным. Компиляторы обычно не генерируют код, который создает константы, компилятор выполняет создание и направляет все ссылки кода на то, что он сделал.
Джимми Хоффа

Ответы:

23

Все современные движки JavaScript выполняют компиляцию точно в срок. Вы не можете делать какие-либо предположения о том, что он «должен создавать снова и снова». Такого рода расчеты относительно легко оптимизировать в любом случае.

С другой стороны, закрытие константных переменных не является типичным случаем, для которого вы бы хотели использовать JIT-компиляцию. Обычно вы создаете замыкание, когда хотите иметь возможность изменять эти переменные при разных вызовах. Вы также создаете дополнительный разыменование указателя для доступа к этим переменным, например разницу между доступом к переменной-члену и локальным int в ООП.

Именно в такой ситуации люди выбрасывают строку «преждевременная оптимизация». Простые оптимизации уже сделаны компилятором.

Карл Билефельдт
источник
Я подозреваю, что именно обход области для переменного разрешения вызывает потери, как вы упомянули. Кажется разумным, но кто действительно знает, что за безумие в движке JavaScript JIT ...
Джимми Хоффа
1
Возможное расширение этого ответа: причина, по которой JIT игнорирует оптимизацию, которая проста для автономного компилятора, заключается в том, что производительность всего компилятора важнее, чем в необычных случаях.
Леушенко
12

Переменные дешевы. Контексты исполнения и цепочки областей видимости дороги.

Существуют различные ответы , которые в основном сводятся к «потому затворов», и те , по сути верно, но проблема не конкретно с закрытием, это тот факт , что у вас есть функция , ссылающийся переменные в другой области. У вас возникла бы та же проблема, если бы это были глобальные переменные windowобъекта, а не локальные переменные внутри IIFE. Попробуйте и посмотрите.

Итак, в вашей первой функции, когда движок видит это утверждение:

var gbSize = size / GB;

Необходимо предпринять следующие шаги:

  1. Поиск переменной sizeв текущей области. (Нашел это.)
  2. Поиск переменной GBв текущей области. (Не найден.)
  3. Поиск переменной GBв родительской области. (Нашел это.)
  4. Сделайте расчет и назначьте gbSize.

Шаг 3 значительно дороже, чем просто выделение переменной. Более того, вы делаете это пять раз , в том числе дважды для обоих GBи MB. Я подозреваю, что если вы использовали псевдонимы в начале функции (например var gb = GB) и вместо этого ссылались на псевдоним, это фактически привело бы к небольшому ускорению, хотя также возможно, что некоторые движки JS уже выполняют эту оптимизацию. И, конечно же, самый эффективный способ ускорить выполнение - просто не пересекать цепочку областей действия.

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

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

Aaronaught
источник
И даже если «оптимизация» не влияет на производительность отрицательно, то почти наверняка это будет влиять на читаемость кода отрицательно. Который, если вы не занимаетесь каким-то сумасшедшим вычислительным процессом, чаще всего является плохим компромиссом (очевидно, нет привязки к постоянной ссылке, ищите "2009-02-17 11:41"). Как резюмируется: «Выберите ясность скорости, если скорость не является абсолютно необходимой».
CVn
Даже при написании базового интерпретатора для динамических языков доступ к переменным во время выполнения, как правило, является операцией O (1), а обход области O (n) даже не требуется во время начальной компиляции. В каждой области видимости каждой вновь объявленной переменной присваивается номер, поэтому var a, b, cмы можем получить к ней доступ bкак scope[1]. Все области пронумерованы, и если эта область вложена в пять областей глубиной, то bона полностью адресована, env[5][1]что известно при синтаксическом анализе. В нативном коде области соответствуют сегментам стека. Замыкания являются более сложными, поскольку они должны выполнить резервное копирование и заменитьenv
amon
@amon: Это может быть то, как вы бы хотели, чтобы это работало в идеале , но это не совсем так. Люди гораздо более знающие и опытные, чем я написал книги об этом; в частности, я бы указал на высокопроизводительный JavaScript от Николаса С. Закаса. Вот фрагмент , и он также поговорил с тестами, чтобы подтвердить это. Конечно, он, конечно, не единственный, просто самый известный. У JavaScript есть лексическая область видимости, поэтому замыкания на самом деле не такие уж особенные - по сути, все замыкания.
Aaronaught
@Aaronaught Интересно. Поскольку этой книге 5 лет, мне было интересно, как текущий движок JS обрабатывает переменные запросы, и смотрел на бэкэнд x64 движка V8. Во время статического анализа большинство переменных разрешаются статически и им присваивается смещение памяти в их области видимости. Области функций представлены в виде связанных списков, а сборка отправляется в виде развернутого цикла для достижения правильной области. Здесь мы получили бы эквивалент кода C *(scope->outer + variable_offset)для доступа; каждый дополнительный уровень области действия функции стоит одна дополнительная разыменование указателя. Кажется , мы оба были правы :)
Амон
2

Один пример предполагает закрытие, другой - нет. Реализация замыканий довольно сложна, поскольку закрытые переменные не работают как обычные переменные. Это более очевидно в низкоуровневом языке, таком как C, но я буду использовать JavaScript, чтобы проиллюстрировать это.

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

function add(vars, y) {
  vars.x += y;
}

function getSum(vars) {
  return vars.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(adder, 2);
console.log(adder.getSum(adder));  //=> 42

Обратите внимание на неловкое соглашение о вызовах, которое closure.apply(closure, ...realArgs)требует

Поддержка встроенных объектов JavaScript позволяет опустить явный varsаргумент и позволяет использовать thisвместо него:

function add(y) {
  this.x += y;
}

function getSum() {
  return this.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

Эти примеры эквивалентны этому коду, фактически использующему замыкания:

function makeAdder(x) {
  return {
    add: function (y) { x += y },
    getSum: function () { return x },
  };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

В этом последнем примере объект используется только для группировки двух возвращаемых функций; thisсвязывание не имеет значения. Все детали создания возможных замыканий - передача скрытых данных в реальную функцию, изменение всех обращений к переменным замыкания для поиска в этих скрытых данных - решаются языком.

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

Амон
источник