Производительность на входных данных из статьи на Node v12: Внимание: бенчмарк на единственных входных данных на единственной версии интерпретатора JS. Не делайте решающих выводов на основании только этого бенчмарка
keys + for-of x 7,210,543 ops/sec ±2.47% (85 runs sampled)
values + flat x 747,610 ops/sec ±0.82% (91 runs sampled)
Fastest is keys + for-of
(Чем больше ops/sec тем лучше)
Код бенчмарка
const Benchmark = require("benchmark");
const suite = new Benchmark.Suite();
const phoneBook = {
andrew: ["+345356245254", "+313232312312"],
vasilina: ["+132313123123"],
serhiy: ["+587234878234", "+321323124123"],
};
suite
.add("keys + for-of", function () {
const phoneNumbers = [];
const personNames = Object.keys(phoneBook);
for (const personName of personNames) {
const personPhoneNumbers = phoneBook[personName];
phoneNumbers.push(...personPhoneNumbers);
}
return phoneNumbers;
})
.add("values + flat", function () {
return Object.values(phoneBook).flat();
})
.on("cycle", function (event) {
console.log(String(event.target));
})
.on("complete", function () {
console.log("Fastest is " + this.filter("fastest").map("name"));
})
.run({ async: true });
Читаемость вашего варианта очень хороша.
В виду этих различий я бы рассматривал насколько каждое из них важнее в вашем случае.
На мой взгляд читаемость важнее. Так что мой голос идёт в сторону Object.values(phoneBook).flat()
Возникла идея использовать библиотеку валидации для декларативного описания стратегии итерации по вложений структуре данных объекту.
Эта идея меня позабавила. Я хотел такой идеей поделиться. Об этом и статья
А так как эта идея обладает отрицательными качествами, я решил о них написать, чтобы не возникло ощущения что я предлагаю использовать валидации для итерации в «серьезном» коде.
Я хотел передать, что даже если бы такая оптимизация и была имплементирована в браузере — то изначальная функция не подпадала бы под эту оптимизацию, и её необходимо поправить, так чтобы вызов рекурсии был последней операцией в функции.
Спасибо за поправку, необходимо было упомянуть, что эта оптимизация не во всех интерпретаторах/компиляторах есть, и в том числе в большинстве Javascript интерпретаторов.
Мемоизация поможет с глубиной только на последующих вызовах. Первый вызов — будет иметь полную глубину рекурсии.
Эта история с ошибкой в рекурсии произошла не просто в вакууме. Значит эта ошибка была допущена некоторым агентом.
Есть три варианта кто им может быть: писатель, читатель или третье лицо («Максим»).
Если это будет писатель, то текст будет выглядеть так:
"Я написал что то и оно сломалось"
Читатель, который сталкивался с такой проблемой сразу начнет воспринимать писателя как не опытного, даже не осознанно. И это плохо для писателя, ведь он хочет поделиться опытом, который у него есть.
Предположим что этот агент — читатель. Тогда текст будет выглядеть так:
"Вот вы написали вот это, и оно сломалось"
Читатель воспримет это негативно, ведь ему вменили «преступление». И это плохо для читателя, которому станет тяжелее воспринимать описанный опыт. Это плохо и для писателя, ведь он хочет поделиться опытом, а не чувством вины или ненависти и ТД.
Таким образом, если действие имеет плохую окраску лучше использовать третье лицо. Это не будет окрашивать читателя или писателя в темные тона.
Про этот литературный прием я прочитал в книге «Пиши и сокращай» Ильяхова.
Удаление с мутацией основного дерева без рекурсии. Очень сложный но вроде работает:
function findMostRightNodeWithParent<T>(tree: INode<T>): [INode<T>, INode<T> | null] {
let parent: INode<T> | null = null
let currentNode = tree
while (currentNode.right) {
parent = currentNode
currentNode = currentNode.right
}
return [currentNode, parent]
}
function mutableRemove<T>(tree: Tree<T>, value: T): Tree<T> {
if (!tree) return null
const nodesWithParent: [INode<T>, INode<T> | null][] = [[tree, null]]
let foundTree: INode<T> | null = null
let foundTreeParent: INode<T> | null = null
while (nodesWithParent.length > 0) {
const [node, nodeParent] = nodesWithParent.pop() as [INode<T>, INode<T> | null]
if (node.value === value) {
foundTree = node
foundTreeParent = nodeParent
break
}
if (node.right) {
nodesWithParent.push([node.right, node])
}
if (node.left) {
nodesWithParent.push([node.left, node])
}
}
// Такого элемента нет в дереве
if (!foundTree) return tree
// Такой элемент это корень дерева
if (!foundTreeParent) {
// если нет левого поддерева возвращаем правое поддерево
if (!foundTree.left) return foundTree.right
// если нет правого поддерева возвращаем левое поддерево
if (!foundTree.right) return foundTree.left
// Если есть и правое и левое, то ищем самый правый элемент левого поддерева, и ставим его в голову дерева. Удаляем его снизу
const [mostRight, mostRightParent] = findMostRightNodeWithParent(foundTree.left)
if (mostRightParent) {
mostRightParent.right = mostRight.left
}
foundTree.value = mostRight.value
return foundTree
}
const direction = foundTreeParent.left === foundTree ? 'left' : 'right'
// Если это лист - удаляем его
if (!foundTree.left && !foundTree.right) {
foundTreeParent[direction] = null
return tree
}
// Если это элемент - не содержит одной из ветвей - то ту ветвь которая есть присвоить родителю найденой ветви
if (!foundTree.left) {
foundTreeParent[direction] = foundTree.right
return tree
}
if (!foundTree.right) {
foundTreeParent[direction] = foundTree.left
return tree
}
let [mostRightChild, mostRightChildParent] = findMostRightNodeWithParent(foundTree.left)
// если есть промежуточный родитель до самого правого слева
if (mostRightChildParent) {
mostRightChildParent.right = mostRightChild.left
} else {
// промежуточного родителя слева нет
foundTree.left = mostRightChild.left
}
foundTree.value = mostRightChild.value
return tree
}
В первом примере у вас ручная реализация абстракции call stack
Вы заметили основную суть. Именно ручную реализацию кол стека я и написал.
В чём тогда профит?
Профит в том. что такая функция не упадёт так рано, как рекурсивная реализация. Я об этом написал в фразе "Зависит от размера задачи". Когда нужно посчитать факториал для чисел меньше 1K-10K нужно юзать рекурсию. И это будет более ефективно. Но когда размер задачи достаточно велик — рекурсивная версия может просто слишком рано упасть. В то время как размер кучи обычно гораздо выше, а значит памяти хватит на большее.
при переполнении ему грозит не Stack overflow error, а Out of memory error
Итеративную версию можно уронить, вопрос только при каких размерах входных данных.
Спасибо за комментарий и проделанный анализ, надеюсь читатель моей статьи ознакомится и с вашим комментарием, чтобы не возникало ощущение, что это серебрянная пуля и не уронимая скала.
Пример итерации по дереву на Typescript. На Javascript было бы точно так, только без типов.
type Tree<T> = INode<T> | null
interface INode<T> {
value: T,
left: Tree<T>,
right: Tree<T>
}
function breadthFirstIterate<T>(tree: Tree<T>, callback: (value: T) => void) {
if (!tree) return
const nodes: INode<T>[] = [tree]
while (nodes.length > 0) {
const node = nodes.shift() as INode<T>
callback(node.value)
if (node.left) {
nodes.push(node.left)
}
if (node.right) {
nodes.push(node.right)
}
}
}
function depthFirstIterate<T>(tree: Tree<T>, callback: (value: T) => void) {
if (!tree) return
const nodes: INode<T>[] = [tree]
while (nodes.length > 0) {
const node = nodes.pop() as INode<T>
callback(node.value)
if (node.right) {
nodes.push(node.right)
}
if (node.left) {
nodes.push(node.left)
}
}
}
Здесь итерация в ширину и в длину. Итерация в ширину не оптимально быстрая так как массив в JS не очень хорош для организации очереди. Лучше создать односвязный список, чтобы получение очередной ноды была операция за O(1). А не О(n).
Насчёт удаления или замены. Тут зависит от того, хочу ли я на выходе получить то же дерево что изначально с изменениями, или построить новое дерево с применёнными измениями.
Такого раньше не писал итеративно, но сейчас попробую
Не знаю насчёт развёрнутой итерации в сложных случаях будет ли она проще для дебагинга, но согласен, что когда stack-trace огромный это бывает сбивает с толку.
Насчёт оптимизации добавлю, что нужно чтобы Максим знал, каким требованиям должна соответствовать рекурсия, чтобы компилятор/интерпретатор мог оптимизировать её с помощью Tail Call Optimization. Если вы о ней.
Чтобы функция факториала была оптимизирована её нужно будет переписать вот так:
function factorial(n, acc = 1) {
if (n <= 1) return acc
return factorial(n-1, acc * n)
}
Изначальная вариант не будет оптимизирован в итерацию. Насколько я знаю, конечно.
Насчёт второго, мне больнее всего за читаемость, при таких разворотах.
Что вы подразумеваете под "парсингJSX шаблонов на клиенте"?
Да, действительно, хорошее решение. Я не против такого решения.
Могу отметить три различия: поддержка браузеров, производительность и читаемость.
Поддержка браузеров:
Object.keys + for-of
: 94%Object.values+.flat()
: 91%Производительность на входных данных из статьи на Node v12:
Внимание: бенчмарк на единственных входных данных на единственной версии интерпретатора JS. Не делайте решающих выводов на основании только этого бенчмарка
(Чем больше ops/sec тем лучше)
Читаемость вашего варианта очень хороша.
В виду этих различий я бы рассматривал насколько каждое из них важнее в вашем случае.
На мой взгляд читаемость важнее. Так что мой голос идёт в сторону
Object.values(phoneBook).flat()
Возникла идея использовать библиотеку валидации для декларативного описания стратегии итерации по вложений структуре данных объекту.
Эта идея меня позабавила. Я хотел такой идеей поделиться. Об этом и статья
А так как эта идея обладает отрицательными качествами, я решил о них написать, чтобы не возникло ощущения что я предлагаю использовать валидации для итерации в «серьезном» коде.
Интересно)
Да!
Я хотел передать, что даже если бы такая оптимизация и была имплементирована в браузере — то изначальная функция не подпадала бы под эту оптимизацию, и её необходимо поправить, так чтобы вызов рекурсии был последней операцией в функции.
В любом случае такая оптимизация предусмотрена стандартом:
https://www.ecma-international.org/ecma-262/11.0/index.html#sec-evaluatecall
Спасибо за поправку, необходимо было упомянуть, что эта оптимизация не во всех интерпретаторах/компиляторах есть, и в том числе в большинстве Javascript интерпретаторов.
Мемоизация поможет с глубиной только на последующих вызовах. Первый вызов — будет иметь полную глубину рекурсии.
Да(
Ваш первый комментарий касался формы, а не содержания.
Спасибо, за критику формы подачи. Я подумаю, как это можно улучшить.
Надеюсь, что этот приём не ухудшил ваше восприятие сути. Будет интересно узнать ваше мнение и по содержанию.
Да, я об этом думал, но предпочёл использовать этот приём. Он принёс больше пользы, чем вреда в статью, по причинам описанным выше.
Я не считаю читателя «нежным».
Это ложное противопоставление. Использование этого приёма не ухудшает восприятие сути. И у меня есть основания полагать, что улучшает.
Здравствуйте, это осознанный выбор.
Эта история с ошибкой в рекурсии произошла не просто в вакууме. Значит эта ошибка была допущена некоторым агентом.
Есть три варианта кто им может быть: писатель, читатель или третье лицо («Максим»).
Если это будет писатель, то текст будет выглядеть так:
"Я написал что то и оно сломалось"
Читатель, который сталкивался с такой проблемой сразу начнет воспринимать писателя как не опытного, даже не осознанно. И это плохо для писателя, ведь он хочет поделиться опытом, который у него есть.
Предположим что этот агент — читатель. Тогда текст будет выглядеть так:
"Вот вы написали вот это, и оно сломалось"
Читатель воспримет это негативно, ведь ему вменили «преступление». И это плохо для читателя, которому станет тяжелее воспринимать описанный опыт. Это плохо и для писателя, ведь он хочет поделиться опытом, а не чувством вины или ненависти и ТД.
Таким образом, если действие имеет плохую окраску лучше использовать третье лицо. Это не будет окрашивать читателя или писателя в темные тона.
Про этот литературный прием я прочитал в книге «Пиши и сокращай» Ильяхова.
Спасибо за комментарий!
Добавлю ссылку на эту ветку комментариев в конец, спасибо за ваши комментарии! Я так мозгом давно не воротил)
Удаление с мутацией основного дерева без рекурсии. Очень сложный но вроде работает:
Спасибо большое за проявленное внимания!
Вы заметили основную суть. Именно ручную реализацию кол стека я и написал.
Профит в том. что такая функция не упадёт так рано, как рекурсивная реализация. Я об этом написал в фразе "Зависит от размера задачи". Когда нужно посчитать факториал для чисел меньше 1K-10K нужно юзать рекурсию. И это будет более ефективно. Но когда размер задачи достаточно велик — рекурсивная версия может просто слишком рано упасть. В то время как размер кучи обычно гораздо выше, а значит памяти хватит на большее.
Итеративную версию можно уронить, вопрос только при каких размерах входных данных.
Спасибо за комментарий и проделанный анализ, надеюсь читатель моей статьи ознакомится и с вашим комментарием, чтобы не возникало ощущение, что это серебрянная пуля и не уронимая скала.
У меня тоже возник такой диссонанс, но мне показалось, что я просто не понял мысль комментатора и/или он имел в виду некий другой аспект математиков.
Пример итерации по дереву на Typescript. На Javascript было бы точно так, только без типов.
Здесь итерация в ширину и в длину. Итерация в ширину не оптимально быстрая так как массив в JS не очень хорош для организации очереди. Лучше создать односвязный список, чтобы получение очередной ноды была операция за O(1). А не О(n).
Насчёт удаления или замены. Тут зависит от того, хочу ли я на выходе получить то же дерево что изначально с изменениями, или построить новое дерево с применёнными измениями.
Такого раньше не писал итеративно, но сейчас попробую
В глубину или в ширину?
Да, функция Аккермана была бы очень в тему для модельного примера, спасибо за ответ!
Люблю пролог. Самое необычное из того на чем приходилось писать.
Интересно в какой области у вас возникли такие задачи?
Да!
Не знаю насчёт развёрнутой итерации в сложных случаях будет ли она проще для дебагинга, но согласен, что когда stack-trace огромный это бывает сбивает с толку.
Спасибо за комментарий!
Считаю, что если нечто можно написать просто — необходимо писать просто.
Также считаю, что если абстракция не упрощает — то необходимо доказать, зачем она нужна прежде чем добавлять её в проект.
Интересно узнать, что именно вы бы поменяли в этом решении. Хотя бы в общих чертах.
В любом случае, спасибо за комментарий!
Абсолютно согласен с вами!
Насчёт оптимизации добавлю, что нужно чтобы Максим знал, каким требованиям должна соответствовать рекурсия, чтобы компилятор/интерпретатор мог оптимизировать её с помощью Tail Call Optimization. Если вы о ней.
Чтобы функция факториала была оптимизирована её нужно будет переписать вот так:
Изначальная вариант не будет оптимизирован в итерацию. Насколько я знаю, конечно.
Насчёт второго, мне больнее всего за читаемость, при таких разворотах.
Спасибо за комментарий, очень полезные примечания