Search
Write a publication
Pull to refresh

Comments 18

ordered: 5.959ms random: 156.761ms

Неплохая такая разница, правда? Это всё из за мега-морфного доступа IC.

Сорри за странный вопрос, а эта разница точно не из‑за работы Math.random() при каждом вызове? У меня без всяких объектов примерно такое же соотношение по времени выполнения показала пара функций — одна, которая возвращает x и другая, которая возвращает в половине случаев x, а в половине y

В нашем случае влияние Math.random() на замеры минимально, потому что сама функция вызывается внутри цикла, и мы измеряем именно время выполнения всего цикла.

Мы видим, что функция с разным порядком свойств (и разными Map) работает значительно медленнее, а не с небольшой разницей в пару пунктов. Это говорит о том, что разница связана именно с перестройкой скрытых классов и ухудшением IC, а не с накладными расходами

Мы видим, что функция с разным порядком свойств (и разными Map) работает значительно медленнее, а не с небольшой разницей в пару пунктов. Это говорит о том, что разница связана именно с перестройкой скрытых классов и ухудшением IC, а не с накладными расходами

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

function makeObjOrdered() {
    return (Math.random() > 0.5) ? {a: 1, b: 2, c: 3} : {a: 1, b: 2, c: 3}
}

function makeObjRandom() {
    return (Math.random() > 0.5) ? {a: 1, b: 2, c: 3} : {o: 3, a: 1, b: 2}
}

const arr1 = []
console.time("ordered")
for (let i = 0; i < 1e7; i++) {
    const o = makeObjOrdered()
    arr1.push(o.a)
}
console.timeEnd("ordered")
console.log(arr1)
   
const arr2 = []
console.time("random")
for (let i = 0; i < 1e7; i++) {
    const o = makeObjRandom()
    arr2.push(o.a)
}
console.timeEnd("random")
console.log(arr2)

ordered: 202.108ms
random: 229.607ms

Спасибо за уточнение. Да, вы правы. Однако именно эта 15% просадка — это и есть цена за разные Hidden Classes и полиморфный IC, которая становится заметна при большом количестве вызовов. В моём примере была гораздо сильнее — из-за мега-морфного кеша и деоптимизаций. Это я и хотел показать, почему важно держать объекты в максимально унифицированой форме. Наверное моё упущение, что я не показал пример с более равными условиями

Однако именно эта 15% просадка — это и есть цена за разные Hidden Classes и полиморфный IC, которая становится заметна при большом количестве вызовов

10 миллионов вызовов, куда уж больше. И в реальном проекте не будет даже 15% разницы, как только пройдет jit-оптимизатор, разница будет смазана до нескольких процентов, которые будут не важны, так как основным тормозом будет i/o, который съест все эти микрооптимизации. Эти микрооптимизации работают только в таких бенчмарках с гигантскими циклами.

В 2016 это может и было актуально, когда вышла статья Убийцы оптимизации, на данный момент все эти деоптимизации уже не актуальны, а сами разработчики V8 говорят, что пишите как вам удобно.

В моём примере была гораздо сильнее — из-за мега-морфного кеша и деоптимизаций.

В вашем случае разница потому, что в 1 случае оптимизатор просто выкинул цикл вообще, поэтому и получилось 5 ms.

Не проблема создать пример, когда ordered будет куда медленнее, чем random:

Код 1
function makeObjOrdered() {
  return { a: 1, b: 2, c: 3 };
}

function makeObjRandom() {
  return Math.random() > 0.5 ? { a: 1, b: 2, c: 3 } : {o: 3, a: 1, b: 2}
}

let arr1 = []
console.time("ordered");
for (let i = 0; i < 1e7; i++) {
  const o = makeObjOrdered();
  arr1.push(o)
}
console.timeEnd("ordered");
console.log(arr1.slice(0,10))

let arr2 = []
console.time("random");
for (let i = 0; i < 1e7; i++) {
  const o = makeObjRandom();
  arr2.push(o)
}
console.timeEnd("random");
console.log(arr2.slice(0,10))

ordered: 411.947ms
random: 251.125ms

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

Чтобы увидеть работу более тяжелого jit-оптимизатора, который подключается после прогона функции несколько раз, прогоните 3-4 цикла, и у вас уже не будет никакой разницы в итоге, кроме погрешности:

Код 2
function makeObjOrdered() {
    return (Math.random() > 0.5) ? {a: 1, b: 2, c: 3} : {a: 1, b: 2, c: 3}
}

function makeObjRandom() {
    return (Math.random() > 0.5) ? {a: 1, b: 2, c: 3} : {o: 3, a: 1, b: 2}
}

function getA(obj) {
    return obj.a
}

for(let i = 0; i < 3; i+=1) {
    const arr1 = []
    console.time("ordered")
    for (let i = 0; i < 1e7; i++) {
        const o = makeObjOrdered()
        arr1.push(getA(o))
    }
    console.timeEnd("ordered")
    console.log(arr1)
    
    const arr2 = []
    console.time("random")
    for (let i = 0; i < 1e7; i++) {
        const o = makeObjRandom()
        arr2.push(getA(o))
    }
    console.timeEnd("random")
    console.log(arr2)
}

ordered: 230.519ms
random: 236.495ms

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

В целом, внезапно неплохой сайт для бенчей у Карловского сделан.

Не проблема создать пример, когда ordered будет куда медленнее, чем random:

Для собственного понимания, а на чём тут могло быть такое замедление? С ходу не улавливаю, куда вообще смотреть.

В v8 есть 3 уровня запуска кода: как есть, первый быстрый оптимизатор, второй тяжелый jit-оптимизатор. В общем "прогрев" кода.

Время затраченное на работу оптимизатора просто суммируется к времени работы 1 цикла, если блоки кода поменять местами, картина изменится на противоположную (только надо учесть, что в одном случае есть Math.random, а во втором нет).

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

На хабре есть статья про мономорфизм и полиморфизм прям от одного из разработчиков V8, там всё это куда более детально рассказано: https://habr.com/ru/articles/303542/

Минимально нужно хотя бы делать 3 холостых прогона, делать паузу на сколько-то секунд, сделать ещё прогон, снова пауза, и только после этого можно делать финальный прогон, где цифры уже будут ближе к реальности.
Если выполнить такой ритуал, дать всем оптимизаторам время на выполнение, то разница будет ожидаемо никакой:

Код 3

function makeObjOrdered() {
return (Math.random() > 0.5) ? {a: 1, b: 2, c: 3} : {a: 1, b: 2, c: 3}
}

function makeObjRandom() {
return (Math.random() > 0.5) ? {a: 1, b: 2, c: 3} : {o: 3, a: 1, b: 2}
}

const delay = ms => new Promise(d => setTimeout(d, ms))

function getA(obj) {
return obj.a
}

function run1() {
let arr1 = []
console.time("ordered");
for (let i = 0; i < 1e7; i++) {
const o = makeObjOrdered();
arr1.push(o)
}
console.timeEnd("ordered");
console.log(arr1.slice(0,10))
}

function run2() {
let arr2 = []
console.time("random");
for (let i = 0; i < 1e7; i++) {
const o = makeObjRandom();
arr2.push(o)
}
console.timeEnd("random");
console.log(arr2.slice(0,10))
}

for(let i = 0; i < 4; i+=1) {
run1()
run2()
}

await delay(10000)

run1()
run2()

await delay(10000)

run1()
run2()

ordered: 332.928ms
random: 331.127ms

"Уточнение"? Да вы мастер преуменьшений. Вашу статью по сути похоронили одним очевидным любому программисту вопросом, показав вашу некомпетентность в освещаемом вопросе, а вы называете это "уточнением"?!

function makeObjOrdered() {
  const o = {};
  if (Math.random() > 0.5) {
    o.a = 1; o.b = 2; o.c = 3;
  } else {
    o.a = 1; o.b = 2; o.c = 3;
  }
  return o;
}
ordered: 91.914ms
random: 243.027ms

Разница конечно есть, но не такая драмматичная.

Разница реально незначительная, как показали выше.
Таких оптимизаций в js может быть много (for вместо других итераций и т.п.), но суммарно они дают очень мало выгоды, можно легко свести на нет все оптимизации одним непродуманным кусочком кода или плохой архитектурой.

Помню было время, в PHP одиночные кавычки были быстрее двойных. Это из того же поля.

Знать полезно разве что для общего развития, а применять не стоит (оно того не стоит).

Про random уже сказали, но всё равно тесты некорректны.
Главная ошибка - нельзя просто гонять в цикле бессмысленные операции доступа. Джит сочтет это мертвым кодом и порежет. Нужно как-то использовать данные и куда-то их выводить.
Кроме того, желательно запускать тесты более одного раза, чтобы видеть прогрев джита (а иногда бывает, что видно деоптимизацию).
Ниже мой вариант, который тоже далеко не идеален, но показал разницу в скорости 4-5x.


const N = 1e7;
const arr1 = Array(N).fill(0).map(item => {
    return { a: 1, b: 2, c: 3 };
});
const arr2 = Array(N).fill(0).map(item => {
    const r = Math.random();
    if (r < 0.3) return { a: 1, b: 2, c: 3 };
    if (r < 0.5) return { c: 3, a: 1, b: 2 };
    if (r < 0.8) return { b: 2, c: 3, a: 1 };
    return { b: 2, a: 1, c: 3 };
});

function test1 () {
    console.time("ordered");
    let sum = 0;
    for (let i = 0; i < N; i++) {
        sum += arr1[i].a;
    }
    console.timeEnd("ordered");
    console.log(sum);
}

function test2 () {
    console.time("random");
    let sum = 0;
    for (let i = 0; i < N; i++) {
        sum += arr2[i].a;
    }
    console.timeEnd("random");
    console.log(sum);
}

test1();
test2();
test1();
test2();
ordered: 15.375ms
10000000
random: 80.626ms
10000000
ordered: 12.484ms
10000000
random: 75.734ms
10000000

Экономия на спичках неактуальна для JS. Не устраивает время работы? Профилируем, находим бутылочное горлышко, переписываем на C++ (N-API/WebAssembly). 10 млн итераций заняли 200 мс, что приемлемо для запуска в Web Worker. Пользователь может и 5 секунд посмотреть на спиннер.

Да и как часто нужно обрабатывать такие объёмы данных? Показывать в UI более 100 (ну, пусть 1000) объектов бессмысленно. На бэкенде это скорее всего будет храниться в БД или обрабатываться джобой — проще масштабировать, чем заниматься микрооптимизациями. А если игра стоит свеч, то добро пожаловать в C++.

Экономия на спичках неактуальна для JS. [...] Профилируем, находим бутылочное горлышко

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

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

Я конечно понимаю, что это просто наглядный пример, но разве это не решается местным ООП с наследованием?

Sign up to leave a comment.

Articles