Pull to refresh

Comments 71

Вот спасибо!
Будет теперь чем заткнуть недовольных try/catch и switch/case блоками.
Если у Вас больше 128 case'ов, стоит все же задуматься.
если у Вас больше 128 case'ов — задуматься стоит в любом случае.
Теперь даже 500 case не вызывают деоптимизацию, 600 тоже.

А вот 666 case способны вызвать сатану. Разработчик, написавший такое количество условий получит персональное место. Есть хотя бы два вменяемых случая, когда такое количество допустимо?
Есть хотя бы два вменяемых случая, когда такое количество допустимо?

Едва ли. Даже для автоматически генерируемого кода это, ИМХО, неприемлемо. Если требуется такое количество условий, то, вероятно, нужно строить индекс. Иначе это же будет адски тормозить. Разве что если и код автоматически сгенерированный и требований к производительности никаких.

В некоторых случаях компилятор С++ может оптимизировать такую портянку до двух сравнений и трех переходов:
исходный код
switch(lParam)
{
case 1: ...
case 2: ...
...
case 555: ...
default:...
}


вариант компиляции
cmp ax, 0
jbe ...
cmp ax, 555
ja ...
jmp [dx + ax]


Я могу представить это только для цикла обработки событий ОС.
Однажды я видел «админку» react+redux в которой был всего ОДИН редьюсер и switch/case из 666 типов action
Недовольных try/catch и switch/case блоками в подавляющем большинстве случаев можно заткнуть простой цитатой Кнута:
«We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%»
UFO just landed and posted this here
и даже try-catch больше не является проблемой.

На данный момент не оптимизируются:
  • функции, содержащие выражение try-catch;

Хм...?

То цитата из давнишней статьи про "убийц оптимизации".

Упс, читал по-диагонали… Спасибо. :-)

Вот тоже тупил минуту, пока понял, что это цитаты

UFO just landed and posted this here
Ну вы же сами сделали этот выбор
Возврат ко временам «Сайт предназначен для просмотра в IE версии 4.0 и выше» какой-то.
Да почему? Оптимизации V8 относятся исключительно к семейству хромов. Почему человек возмущается, что в FF, который он, на минуточку, выбрал сам как браузер, отсутствуют эти оптимизации? (заметьте, я не сказал, что они там вообще отсутствуют, там другой движок и другие оптимизации)

«Сайт предназначен для просмотра в IE версии 4.0 и выше»
Ощущение складывается, что хром виноват еще и во всех технологиях, в него зашитых, которые не поддерживаются в FF. Ну вообще класс, что уж.

Вовсе не возврат. Язык-то один и тот же, просто разные конструкции в разных браузерах работают с разной скоростью. Это нормально.

Мне кажется другие браузеры не отстают в этом вопросе, но в контексте убийц оптимизации, клиентский js код не факт что имеет ощутимый вклад в отзывчивость страниц, там ведь в первую очередь проблема с DOM и детекцией видимости объектов и скрытием невидимых.
Тут важнее, чтобы все браузеры внедрили IntersectionObserver, и популярные фреймворки/библиотеки стали поддерживать эту технологию — тогда страдать никому не придется =)

А с v8 всё просто, если оптимизировали там, значит в node код станет быстрее, и писать его будет проще
Так ведь речь идёт об убийцах оптимизации именно по отношению к v8. Для Firefox упоминаемый список предупреждений может быть вообще не актуальным. Это всё зависит от движка.

Для ноды есть небольшой модуль will-it-optimize. Он позволяет быстро посмотреть, что убивает оптимизацию для текущей версии ноды, а что нет.

Провел тесты на node v6.9.4 v8 v5.1.281.89

Исходники тестов:
'use strict';

function tryCatch() {
  try {
    return null;
  } catch(e) {
    return e;
  }
}
checkOptimizationStatus(tryCatch);

function forOf() {
  var r = 0, a = [1,2,3];
  for(var e of a) r += e;
  return r;
}
checkOptimizationStatus(forOf);

function forIn() {
  var r = 0, a = {1:1, 2:2, 3:3};
  for(var e in a) if(a.hasOwnProperty(e)) r += a[e];
  return r;
}
checkOptimizationStatus(forIn);

function constDecl() {
  const a = 5;
  return a;
}
checkOptimizationStatus(constDecl);

function letDecl() {
  let a = 5;
  return a;
}
checkOptimizationStatus(letDecl);

function argsRest(...args) {
  return args;
}
checkOptimizationStatus(argsRest);

function argumentsRewrite(a) {
  a = a || 5;
  return a;
}
checkOptimizationStatus(argumentsRewrite);

function argumentsLeak1() {
  return arguments;
}
checkOptimizationStatus(argumentsLeak1);

function argumentsLeak2() {
  return Array.prototype.slice.call(arguments);
}
checkOptimizationStatus(argumentsLeak2);

function argumentsLeakToSecondArgOfApply() {
  return Array.prototype.concat.apply([], arguments);
}
checkOptimizationStatus(argumentsLeakToSecondArgOfApply);

function defaultArguments(a, b = 5) {
  return a + b;
}
checkOptimizationStatus(defaultArguments);

function arrayDestruct() {
  const [a, b, c] = [1, 2, 3];
  return a + b + c;
}
checkOptimizationStatus(arrayDestruct);

function objectDestruct() {
  const {a, b, c} = {a: 1, b: 2, c: 3};
  return a + b + c;
}
checkOptimizationStatus(objectDestruct);

function* generatorSimple() {
  yield 1;
  yield 2;
  yield 3;
}
checkOptimizationStatus(generatorSimple);

function* generatorInfinity() {
  var i = 0;
  while(true) yield i++;
}
checkOptimizationStatus(generatorInfinity);

function infinityLoop(n = 10) {
  var a = false;
  while(true) {
    if(a) break;
    if(n-- === 0) a = true;
  }
}
checkOptimizationStatus(infinityLoop);

function containsObjectLiteralWithProto() {
  return {__proto__: String.prototype};
}
checkOptimizationStatus(containsObjectLiteralWithProto);

function containsObjectLiteralWithGetter() {
  return {
    get prop() {
      return 3;
    }
  };
}
checkOptimizationStatus(containsObjectLiteralWithGetter);

function containsObjectLiteralWithSetter() {
  return {
    set prop(val) {
      this.val = val;
    }
  };
}
checkOptimizationStatus(containsObjectLiteralWithSetter);

class ClassStatic {
  static classStaticMethod() {}
}
checkOptimizationStatus(ClassStatic.classStaticMethod);

class ClassConstructor {
  constructor() {}
}
checkOptimizationStatus(ClassConstructor, true);


Чекер:
function checkOptimizationStatus(fn, withNew = false) {
  if(withNew) {
    new fn();
    new fn();
    %OptimizeFunctionOnNextCall(fn);
    new fn();
  } else {
    fn();
    fn();
    %OptimizeFunctionOnNextCall(fn);
    fn();
  }

  switch (%GetOptimizationStatus(fn)) {
    case 1: console.log(fn.name, 'Function is optimized'); break;
    case 2: console.log(fn.name, 'Function is not optimized'); break;
    case 3: console.log(fn.name, 'Function is always optimized'); break;
    case 4: console.log(fn.name, 'Function is never optimized'); break;
    case 6: console.log(fn.name, 'Function is maybe deoptimized'); break;
    case 7: console.log(fn.name, 'Function is optimized by TurboFan'); break;
    default: console.log(fn.name, 'Unknown optimization status'); break;
  }
}


Результат:
tryCatch Function is not optimized
forOf Function is not optimized
forIn Function is optimized
constDecl Function is optimized
letDecl Function is optimized
argsRest Function is optimized by TurboFan
argumentsRewrite Function is optimized
argumentsLeak1 Function is not optimized
argumentsLeak2 Function is not optimized
argumentsLeakToSecondArgOfApply Function is optimized
defaultArguments Function is optimized
arrayDestruct Function is not optimized
objectDestruct Function is optimized
generatorSimple Function is not optimized
generatorInfinity Function is not optimized
infinityLoop Function is optimized
containsObjectLiteralWithProto Function is not optimized
containsObjectLiteralWithGetter Function is not optimized
containsObjectLiteralWithSetter Function is not optimized
classStaticMethod Function is optimized
ClassConstructor Function is optimized
Эти тесты на LTS node ожидаемо ничем не отличаются от прошлогодних выводов, речь про chrome 55 и node v8.0.0

Если заглянуть еще дальше, то в Chrome 57 canary уже оптимизируются даже async-await функции (юху!)

async function delayAsync(delay) {
	return new Promise(resolve => {
		setTimeout(() => resolve(), delay)
	})
}

async function asyncTest() {
	return 'habrahabr'
}

async function exampleFunction() {
	let result = await asyncTest()
	await delayAsync(500)
	console.log(`after 500ms: ${result}`)
}

Function is optimized by TurboFan: exampleFunction
Function is optimized by TurboFan: asyncTest
Function is optimized by TurboFan: delayAsync
//ждем 500ms, после этого результат
(3) after 500ms: habrahabr
Правильнее конечно будет пример сразу с try-catch
async function exampleFunction() {
    try {
        let result = await asyncTest()
        await delayAsync(500)
        console.log(`result after 500ms: ${result}`)
    }
    catch (err) {
        console.error(err)
    }
}

Function is optimized by TurboFan: exampleFunction
node v8.0.0

это где такую взять? на офф сайте только 6.9.4 и 7.4.0 на текущий момент
На текущий момент у 7.4.0 и 8.0.0 nightly одна версия v8 — 5.4 с отличающимися копейками.
На самом деле не все так хорошо как описано в статье, во первых compound let assigments все еще не оптимизируются (и возможно что-то еще, let менял на днях потому в нем уверен)

let s = 0;
  for(let i = 0; i < 1000; i++) {
    s += i;
  }
  console.log(s);
// то, что это деоптимизируется можно увидеть и в профайлере без всяких флагов


во вторых то, что код не вызывает деоптимизации и компилируется, еще не значит, что он будет работать быстро, так что для написания производительного кода все еще нужно будет держать в голове много информации о работе движка (тут к счастью все +- одинаково для все движков, хотя есть места на которых некоторые движки тупят и им приходится помогать уже отдельно, привет Сафари)
Почему нет?

function exampleFunction() {
    let s = 0;
    for (let i = 0; i < 1000; i++) {
        s += i;
    }
    console.log(s);
}

Function is optimized by TurboFan: exampleFunction


В текущей версии браузер не оптимизирует, через пару обновлений будет

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

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

Может да, а может нет. Держать все нет смысла, так как оптимизатор развивается быстрее, чем мы пишем код

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

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

Есть всего 3-5 простых некритичных правила, чтобы js-код не страдал потерей производительности:
1. Не переназначать переменные другим типом
2. Мегаморфизм функций
3. Не добавлять динамически свойства в объект
4. for быстрее forEach
5. Большое количество конкатенаций мелких строк (например, посимвольно) ведет к перерасходу памяти

Решение:
1. использовать const
2. редкое явление
3. потери производительности незначительны
4. использовать for..of
5. использовать arr.push вместо +=

Нужно понимать, как работает vm
Не нужно, vm это черный ящик, сегодня он так работает, завтра по другому

чтоб его ускорить приходится расписывать его некрасиво и дублировать конструкции
Раньше делали inline asm вставки в С код, сейчас чтобы сделать inline вставку, которая будет быстрее, чем clang заоптимизирует, настолько сложно, что этим мало кто занимается

Вы предлагаете заниматься тем же для js, ускорять то, что не внесет даже 0.1% в итоговую производительность, а может даже замедлит
Это всё интересно (знать про внутреннее устройство, про оптимизации, про ускоряющие хаки), но нужен взгляд и с другой стороны

Сейчас парадокс языка в том, что красивый код обычно означает медленный код
Наоборот, именно сейчас можно писать компактный, красивый, с async-await код, который будет быстрым, который удобно писать и понимать
Есть всего 3-5 простых некритичных правила, чтобы js-код не страдал потерей производительности

Все так, но не для всех задач этого достаточно. При написании парсеров или игровой логики прийдется думать о том как выделяется память и куда расходуются циклы процессора. Мегаморфизм может быть по типам Float32 vs Uint8 и прийдется писать две одинаковые функции для каждого из них.

Не нужно, vm это черный ящик, сегодня он так работает, завтра по другому

И тем не менее пока у нас нет встроенных типов вы никуда не денетесь от скрытых классов и память будет выделятся по прежним правилам. И это вам не нужно пока у вас нет таких задач, а как только появятся и Safari будет проигрывать Chrome по производительности в 100 раз, сразу прийдется думать что с ним не так и где компилятор ступил.

Раньше делали inline asm вставки в С код, сейчас чтобы сделать inline вставку, которая будет быстрее, чем clang заоптимизирует, настолько сложно, что этим мало кто занимается

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

Вы предлагаете заниматься тем же для js, ускорять то, что не внесет даже 0.1% в итоговую производительность, а может даже замедлит

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

Наоборот, именно сейчас можно писать компактный, красивый, с async-await код, который будет быстрым, который удобно писать и понимать

До какого-то придела да, а потом вы решите, что функциональный код красивее императивного async-await или у вас появятся задачи где производительность критична и код резко испортится.

Вот пару примеров когда код будет далек от идеалов красоты:
0. DataView не инлайнится и его приходится переписывть через типизированные массивы
1. Попробуйте организовать классы Vec extends Array, Vec2, Vec3, Vec4 без дублирования и потери производительности. И тут уже как минимум прийдется возвращаться к Array.prototype.call(this.constructor.size) вместо extend, так как в super объект this не доступен. А по хорошему вообще нужно выделять кусок памяти и работать внутри него, чтоб не вскипятить процессор, и все что можно сбросить на gpu.
Кстати

1. Не переназначать переменные другим типом

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

Долгое время у const и let были проблемы с производительностью на v8 из за реализации TDZ.
3. Не добавлять динамически свойства в объект
3. потери производительности незначительны

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

4. использовать for..of

Генераторы пока не оптимизируются, если вы пишете красивый код в функциональном стиле то for-of вам не поможет.

5. Большое количество конкатенаций мелких строк (например, посимвольно) ведет к перерасходу памяти
5. использовать arr.push вместо +=

Строки вообще дорогие в js, и этот совет палка о двух концах, сборщик мусора в принципе легко справится с короткоживущими строками, а вот arr.join может стоить гораздо дороже +=, тут нужно смотреть на конкретный случай. Но сразу arr.push дороже чем new Array(size) и arr[i] =…
Вот так как раз делать нельзя так как создаются новые скрытые классы для этих объектов и потери производительности значительны.

Именно так, и я проверила как это влияет на производительность, вместо 140мс, код выполнился за 180мс, добавляла 20 свойств динамически

Вы теряете суть и уходите в крайности. Вспоминать что у const и let были проблемы в рамках этой статьи странно. Генераторы тоже оптимизируются, как и async-await
function* exampleFunction() {
  yield 1;
  yield 2;
  return 3;
}

Function is optimized by TurboFan: exampleFunction


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

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

Я вам как раз и говорю, что изучать оптимизации не нужно, эвристики для vm меняются часто, но вот принципы работы остаются на долго. Оптимизировать нужно лишь тогда когда это действительно нужно и тут важно иметь тесты производительности ибо какие-то факты о хаках в v8 вряд ли помогут вам с другими браузерами. Уже разводили эту дискуссию в статье об убийцах оптимизации и я как раз и говорил, что все устаревает и все нужно тестировать =)
По сути я ответил чуть выше
Именно это я и назвала «Вы теряете суть и уходите в крайности»

В вопросах производительности js очень сложный язык
Нет

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

Нет

С вашим нет я не соглашусь. Для UI нет, для игр и вычислений — да. Каждому свое.
Именно так, и я проверила как это влияет на производительность, вместо 140мс, код выполнился за 180мс, добавляла 20 свойств динамически

дайте кодпен и посмотрим что там за код и как вы это меряли, в любом случае это уже свыше 20% запустите это в цикле и будет вам счастье.
С вашим нет я не соглашусь. Для UI нет, для игр и вычислений — да. Каждому свое.

То, что вы пишете на нем сложным путем, не делает его сложным и медленным для нормального использования (и игры и вычисления сюда входят)

в любом случае это уже свыше 20% запустите это в цикле и будет вам счастье

Это и было в цикле на 1000000 итераций. Выигрыш в 40мс на 1кк итераций — это не та «награда», которая компенсирует неудобство при написании js кода

Вы так и не поняли про что статья, то что вчера было медленным, завтра будет в 50 раз быстрее без вашего вмешательства. И не факт, что вы закончите проект раньше, чем выйдут новые оптимизации

Всё так, но, справедливости ради, стоит отметить, что те люди, которые пишут на JS игры, вынуждены сталкиваться с такими ограничениями, которые обычным JS-разработчикам встречаются не часто. Дело в том, что нужно выжать большой FPS (Frames per second). А для этого все средства хороши. Графические возможности будут тем выше, чем оптимальнее работает код. В особенности это касается тех самых упомянутых векторов, т.к. в ряде случаев 90+% работы ЦП будет завязано на различные мат. операции над этими векторами.


И таким программистам просто необходимо заморачиваться на даже 10% прирост производительности. Но, всё же, такие вот ситуации редкость для большинства задач в JS-мире.

Но даже им надо оптимизировать начиная с самых узких мест.

Правильно, а не слепо проверять возможность компиляции какой-то части языка.
А почему нельзя писать всю логику на чем-то LLVM-friendly, а JS оставить то, что он умеет хорошо — работу с браузером? Нет, в целом, понятно почему, поддержка, единая кодовая база и вот это все. Но, правда, если нужен перфоманс, не дешевле ли будет скомпилить что-нибудь шустрое (ocaml, c++, c...) через emscripten в круто заоптимизированный код, чем тыкать палкой в js, который и так уже близок к пределу своих возможностей (если писать его руками, естественно)?
Касательно вычислений зависит от того, что вы делаете, иногда дешевле распараллелить их на gpu, а emscripten для этой части кода вам все равно читый js выдаст.

С играми тоже не все однозначно, проще скомпилировать, но как правило менее производительно (компиляторы пока не настолько умны и некоторые места всегда будет проще писать руками, чем заставить компилятор понять что вы имели в виду) и пока не добавили кросс взаимодействие js — wasm вы получите минимум контроля над скомпилированым модулем, отлаживать который та еще радость.

Хорошо работает комбинация этих подходов — часть asmjs/wasm модулем, часть на gpu на glsl, остальное js.
для этой части кода вам все равно читый js выдаст.
Я особо не копался, но особо «чистым» он мне не показался, если ставить в таргет тот же asmjs. Выигрываем-то как раз на отсутствии необходимости vm (ну, на сколько я помню спеку asmjs) самой приводить все типы.

Насколько я помню трюки с glsl, там тоже не все можно запихнуть. Вроде, нужен большой массив чисел, и тогда можно какую-то операцию над ними распараллелить. Сходу намек на 3d расчеты, но не проще ли их сразу webgl отдать?

WASM все же сыроват пока, у asmjs лучше рекомендации.

Хорошо работает комбинация этих подходов — часть asmjs/wasm модулем, часть на gpu на glsl, остальное js.
Именно это я и имею в виду.
В асм нет объектов, так что будет часть кода на js для получения и работы с gl контекстом.

Если компилятор скомпилировал js код то он в принципе тоже перестает проверять типы и предполагает, что они какие-то конкретные пока не обнаруживается обратного. Основной выигрыш получается за счет отсутствия объектов и динамических свойств (кроме таблицы функций).

Ну сейчас можно выдать wasm с фолбеком на asmjs. Сыроват только из за поддержки браузерами. asmjs кажется забросили и это обидно, скажем поддержка i64 есть только в wasm.

js часть все равно может работать с векторами и требовать производительности, так что знать как она работает бывает полезно. Да и верить компиляторам не стоит, юнити в первых версиях выдавал дичайший js код обертку над asmjs модулем, приходилось частями переписывать, чтоб это вообще как-то работало.
Чтоб что-то отдать webgl вам нужно написать минимум два шейдера на glsl, это не имеет ни какого отношения к 3д, webgl ничего о 3д не знает и не должен. webgl действительно работает с типизированными массивами и текстурами, в них можно хранить свои данные.
И таким программистам просто необходимо заморачиваться на даже 10% прирост производительности
Точно необходимо? Создатели AAA игр с вами не согласны, создатели http://slither.io/ с вами не согласны и т. д.

Вот вы потратили 90% времени разрабатывания вашей игры выискивая оптимизации для специфичной логике работы браузера, чтобы выжать fps на 10% больше, через неделю выходит оптимизатор который изменил логику работы и вектора больше не являются проблемой и ускорились на 30%, в обоих случаях это внесло 3.5% к результирующей производительности. А потом выясняется, что другие браузеры и вовсе по другому ведут себя с этой логикой.
И в итоге игра не готова, и ваши оптимизационные хаки оказались не так хороши

Вы ведь наверняка поняли о чем статья. Браузерные хаки ускоряющие не нужны, которые приводили к деоптимизации, а не то, что теперь можно плохие алгоритмы использовать, а v8 за вас все разгонит
Вот вы потратили 90% времени … чтобы выжать fps на 10% больше

Нет, ну если ставить вопрос таким вот образом, то ясное дело, к чёрту такую оптимизацию. Я скорее про то, что если +10% производительности браузерной игры дались вам за час другой, то, вероятно, оно очень даже стоит того. А если вам хватило 10 минут (знали заранее, что и где искать, и по какому принципу исправить), то тем более.


Вы ведь наверняка поняли о чем статья

Угу, я посыл вполне себе понял. И я даже не спорю. Просто прошу быть чуточку менее категоричными.


К тому же я согласен с Large в том, что если у вас серьёзные требования к производительности, то с объектами в JavaScript нужно работать с умом. От того в каком виде будут храниться ваши объекты в памяти будет сильно зависеть скорость работы с ними. Да и asm.js не грех воспользоваться, если он подошёл для ваших нужд.

если у вас серьёзные требования к производительности, то с объектами в JavaScript нужно работать с умом

Нужно писать эффективные алгоритмы
Просто к теме убийц оптимизаций это не относится

Эмм. Ну, дык, алгоритмы, вообще говоря, сильно варьируются, в зависимости от того, как и с каким типом данных, вы работаете. Если я вас правильно понимаю, то вы ошибочно воспринимаете JavaScript, как некий очень абстрактный язык. Но это далеко не так. Да он высокоуровневый. Но тем не менее. Одни и те же, по сути, действия вы можете выполнить с весьма разными затратами CPU.


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


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

Просто к теме убийц оптимизаций это не относится

Ещё как относится. Положим у вас есть некий {}-объект. В зависимости от того, как вы с ним работали, он может быть представлен в памяти в виде экземпляра класса, который для него на лету собрал JIT. А может быть представлен в виде словаря. В зависимости от того, как вы работаете с тем или иным объектом, JIT может ускорить или замедлить ваш код во многие разы. Хотя идеоматически всё будет в порядке. Просто в одном случае вы учли подкапотные нюансы, а в другом написали так, как было удобнее. Полагаю, что в обоих случаях вы увидите в своих тестах заветное "optimized".

Ещё пример, по поводу убийц оптимизации.


Скажем, если вы будете в цикле вызывать метод, в котором будет всякий создаваться новая стрелочная функция, внутри которой будет запрос к вышележащей переменной (т.е. по сути полноценное замыкание)… То js-движок просто не сможет оптимизировать этот код в полной мере. Алгоритм с анонимкой и без оной будет одним и тем же. А вот скорость будет радикально отличаться. Просто нюансы работы v8-движка (да и полагаю любых других).


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


На всякий случай отмечу, если кто-то ещё нас читает. Что заморачиваться подобными вещами в полный рост имеет смысл ТОЛЬКО в высоконагруженных участках кода. НЕ ЗАНИМАЙТЕСЬ ПРЕЖДЕВРЕМЕННОЙ ОПТИМИЗАЦИЕЙ.

Но JavaScript не Haskel. Вот простейший пример. Разница в производительности в 2600 раз.
И это в 2600 раз ускорение внесло лишь 1% к общему ускорению

Полагаю, что в обоих случаях вы увидите в своих тестах заветное «optimized»
Если бы функция была deopt, то даже дикие ухищрения с алгоритмом вам не помогли бы ее разогнать, только хаки для браузера

Ещё как относится
Можно сортировку делать пузырьком и приговаривать «Ах этот v8, не разогнал мой код»

Прощу прощения, не могу больше поддержать разговор на эту тему
И это в 2600 раз ускорение внесло лишь 1% к общему ускорению

Отчего же? Зависит от того где такой код лежит. Разница на 2 порядка даже в крошечных случаях, будучи в цикле, даст такой overhead, что мало не покажется.


Если бы функция была deopt, то даже дикие ухищрения с алгоритмом вам не помогли бы ее разогнать, только хаки для браузера

Избавиться от пресловутого deopt это только 1-ый уровень оптимизации.


Можно сортировку делать пузырьком и приговаривать «Ах этот v8, не разогнал мой код»

Можно. К чему вы это привели? Тут никто и не отрицает необходимость выбора правильных алгоритмов. Это же ясно как божий день. Зачем это вообще обсуждать?

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

Вы уже в который раз пишите мне, что я делаю и опять-таки ошибаетесь. Я пишу не сложным путем, я пишу максимально просто и потом вместо каких-то синтетических тестов на оптимизацию пользуюсь профайлером и уже в зависимости от его результатов делаю те или иные оптимизации основываясь на том как работает движок. Те или иные оптимизации не означает написать for(var i = 0, l = arr.length; i < l; i++) {...}, это означат уменьшить выделение памяти, оптимизировать работу путем линейного выделения памяти, уменьшить количество вызовов и т.д. В некоторых конкретных случаях, приходится делать микрооптимизации, чтоб вывести конкретный движек из ступора, но тут опять же нужен профайлер так как обычно все равно компилируется какая-то часть программы или нет, важно насколько она критична и как быстро работает и ускорение вам нужно сегодня, а не через пару версий так как релиз сегодня.

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

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

Те или иные оптимизации не означает написать for(var i = 0, l = arr.length; i < l; i++) {...}, это означат уменьшить выделение памяти, оптимизировать работу путем линейного выделения памяти, уменьшить количество вызовов и т.д.
И снова вы про плохие алгоритмы, а не про «убийц оптимизаций» в рамках статьи

покажите код, давать результаты тестирования в абсолютных значениях и без кода нет смысла. динамически менять свойства объекта это моветон, а не удобство, программист ожидает одну форму объекта, а у вас приходит другая так как вы где-то динамически что-то поменяли.
Вот код из соседней темы про wasm:
https://habrahabr.ru/company/ruvds/blog/319834/#comment_10020032

Усложним немного код оригинала, чтобы было еще очевиднее факт динамического добавления в объект:
const fiboJsMemo = (num, memo) => {
  memo = memo || function(){return 'hello'}

  if (memo[num]) return memo[num]
  if (num <= 1) return 1

  return memo[num] = fiboJsMemo(num - 1, memo) + fiboJsMemo(num - 2, memo)
}


Проверим на 100000 итераций, попутно с вариантами для Map и Set:
time for fiboJsMemoDynamicObj: 160.85888671875ms
time for fiboJsMemoArray: 112.782958984375ms
time for fiboJsMemoMap: 470.0009765625ms
time for fiboJsMemoSet: 175.534912109375ms
fiboJsMemoResult: 1100000
fiboJsMemoArrayResult: 1100000
fiboJsMemoMapResult: 1100000
fiboJsMemoSetResult: 1100000

Код целиком: http://codepen.io/anon/pen/vgZoWe

Разница между динамическим добавлением свойств и отсутствием всего 48-100мс

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

А вся статья (эта и оригинальная) совершенно не про плохие алгоритмы
Это не про убийц оптимизации, а про плохие алгоритмы

Убийцы оптимизации это участки кода, которые приводят компилятор в ступор. Мой первый комментарий с которым вы продолжаете спорить как раз вам говорил о том, что даже если ваш код оптимизируется без знания работы вм он не обязательно будет быстрым, более того, если вы пишите UI то турбофан с большой вероятностью и не дойдет до вашего кода и он будет просто переведен в байт код и выполнен игнишн без всяких оптимизаций и это вам эти синтетические тесты вообще даром не нужны. Если же вы пишите игры, то эти синтетические тесты вам тоже даром не нужны, но вам нужно понимать как вм работает с памятью, потому что
const a = new Array(size); for(...) a[i] = ...; и const a = []; for(...) a.push(...); это две большие разницы и алгоритмы тут вообще не при чем. Так же ни при чем алгоритмы и к созданию временных переменных скажем с помощью Array.prototype.slice или к дополнительным вызовам в Array.prototype.reduce, просто одни языковые конструкции эффективнее других, хотя и те и другие будут оптимизироваться компилятором.

По поводу вашего примера:
0. Вы меряете подсчет фибоначи, сумму и округление кроме всего прочего, но работаете всего с одним объектом, честнее было бы мерять результат работы нормального конструктора и конструктора с добавлением свойств и создавать тысячи объектов.
1. Для Set код вообще не правильный и вы по сути меряете производительность динамического объекта.
2. Вы и так видите, что массив быстрее, а теперь вынесите инициализацию за функцию и сразу выделите память new Array(it) метод станет быстрее еще в два раза вот так вот бесплатно и никаких алгоритмов тут нет.

Map и Set пока плохо оптимизируются компилятором и вряд ли будут, скорей всего их постигнет участь DataView, но не смотря на это их нужно использовать там где нужны динамические таблицы потому как это красиво и правильно, а вот если уже нужна производительность — заменять на собственные быстрые реализации на авл-деревьях или массивах, в зависимости от потребностей.

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

Я не хочу больше спорить на эту тему и предпочту общение с создателями ААА игр.
Map и Set пока плохо оптимизируются компилятором и вряд ли будут

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

Надежда умирает последней, у меня кстати весьма симпатичная реализация на авл деревьях, мне больно называть ее кривым велосипедом =) Вообще Map и Set очень ограничены, вы не сможете итерировать в обратном порядке или начиная с какого-то объекта, так что никуда от своих реализаций мы не денемся.

А вам не кажется, что вы своим же примером перечёркиваете свой же собственный комментарий. tenbits внёс пару изменений в код, учитывая нюансы работы js-движка и получил огромное ускорение. Алгоритм же он не поменял ни на грамм. Насколько я понимаю, главным ключевым изменением оказалось уменьшение число аллокаций. Он заранее выделил необходимую область памяти под индекс массива (new Array(10000)), зная, что таким образом, он уменьшит и число аллокаций. И, бинго, это сработало. Быть может даже тут JIT javascript-а показал свою мощь. Мне так кажется, т.к. массив не типизированный. А код из c++ скорее всего использует типизированные.


Правда вот если нужно выделить необходимую память под некий объект… я вот попросту не знаю как. С "массивами" тут проще, есть метод из кробки. В кавычках, ибо массивы в js и в c++ принципиально отличаются.

Разница между типизированными и не типизированными не значительна пока мы остаемся в рамках одного типа. Для временных объектов нетипизированные оказываются даже быстрее.

Хм. Т.е я не правильно понимаю, когда трактую вот так:


const typedArray = new Uint32Array(len);
const array = new Array(len);

int[] typedArray = new int[len];
vector<int> array = new vector<int>(len, 0);
Насколько я знаю — нет, компилятор сначала решит, что вы не сволочь и хотели именно массив целых чисел, если вы в него запишите дробное, он скажет, ок, наверное это массив дробных чисел, я ошибся, с кем не бывает, пересоздаст его и перенесет значения туда, а если вы потом туда запишете строку, то он проклянет вас и уже тогда создаст список.
В примере с new Array(10000) смысл ускорения не в том, что аллокаций нет, а в том, что массив заполнен и в цикле на 100000 итераций данные берутся уже готовые без вычислений

Я, для чистоты эксперимента, привела пример кода когда этот «недостаток» исключен, чтобы показать, что динамическое добавление в объект снижает производительность, но совершенно не критично, чтобы помнить об этом

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

Поэтому прощу прощения, я бы с удовольствием обсудила реальные проблемы деоптимизаций, а не проблемы с недостатком мастерства или проблемы алгоритмов

Когда рассуждения на таком уровне идут, мне всегда вспоминается история как компания вложила много денег и времени чтобы ускорить код на golang v1.4, ничего путного не получилось, а потом вышел golang 1.5 и всё ускорилось само собой в 28 раз
https://www.youtube.com/watch?v=z0MXK7bzXuU

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


Более того, эта разница в производительности [] и {}, похоже вызвана, какими-то хитрыми оптимизациями в v8, ибо массив там используется как хеш.


я бы с удовольствием обсудила реальные проблемы деоптимизаций, а не проблемы с недостатком мастерства или проблемы алгоритмов

Поясните, что именно вы имеете в виду под "деоптимизациями", плз. Похоже, мы говорим о разных вещах. Вы имеете ввиду именно отсутствие оптимизации со стороны turboFan? Чтобы код без оной был на порядок медленнее, чем с ней? Боюсь, что и я и Large говорили именно о последующих стадиях ускорения. Оттого и все эти разговоры про вектора, asm, и прочее. Когда нужны микросекунды.

Не знаю, что там по поводу соседней ветки и примера из нее, но если вы в приведенном коде память выделите сразу, то получите ускорение х2 для массива и по сравнению с хешем это х3.5 и никаких глобальных кешей. Пример опять таки не показательный, так как вы создаете всего один array-like object, для тестирования этого случая нужно сравнивать конструктор и конструктор с динамически добавляемым свойством и создавать тысячи объектов, мерять нужно только то, что вы меряете и ничего лишнего, но даже приведенный код дает понять, что разница есть и она ощутима.
но если вы в приведенном коде память выделите сразу, то получите ускорение х2 для массива

Так?


- memo = memo || []
+ memo = memo || new Array(num)
- 187.182ms
+ 166.065ms

Но, нельзя не отметить, что тут массив используется как хеш. Никаких push, unshift и пр.


Пример опять таки не показательный …

Это да. В точку. Я поигрался с предварительным созданием массива нужной длины. Но, из-за необходимости обнулять его на каждой итерации, получил регресс в 5 раз.

Можно прямо при вычислениях обнулять mem[n — 3] при n > 2 и тогда не будет выделяться столько памяти, можно то же самое и для объекта сделать. Массив все равно будет быстрее, но пример плохой, мои примеры с векторами и конструкторами видеть не хотят, в кодпен они не влезут так как там полноценное приложение, а не парочка тестов, но если начать реализовывать, то видно во что превращается код даже на синтетическом тестировании.
Очень полезный, классный и крутой пост. Это как раз то, что мне сегодня нужно.
Огромное спасибо.
Sign up to leave a comment.

Articles