Комментарии 56
Идея неплохая, да.
Функция генерируется через eval
Не надо так пугать =). Через new Function
же.
Посмотрел код — конкатенировать много мелких строк через +=
не очень хорошо, но вы результат не храните, как я понял — так что это несущественно.
Но это далеко не самая злая оптимизация, что я видел. Ещё вот такие штуки бывают:
https://github.com/petkaantonov/bluebird/blob/ee247f1a04b5ab7cc8a283bedd13d2e83d28f936/src/util.js#L201-L213
Подскажите: а как правильно поступать когда нужна конкатенация множества мелких строк? [].join
? И как когда есть конкатенация множества больших строк (порядка 20-100 MiB на каждую)?
Я где-то читал, что движки уже давно сами распознают и оптимизируют работу со строками и ухищрения типа Array#join
и String#concat
уже не так критичны.
Если конкатенация множества мелких строк — да, [].join
.
Строки в JS, как и во многих других языках, immutable и pooled. +=
создаёт новую строку и добавляет её в пул. Причём строки ссылаются на старые, которые уже были в пуле — поэтому если мы строим посимвольно огромную строку — это худшее, что можно придумать — все её компоненты будут в пуле (пока мы её не нормализуем руками или не освободим, конечно).
Про множество больших строк — не скажу точно, надо проверять для вашего конкретного юзкейса, это зависит от того, каким образом вы их собираете и что вы с ними делаете потом. Например, если у вас есть строка A в 20 MiB, строка B в 20 MiB, и вы сохраняете две строки C = A + B и D = B + A + B + A + B, у вас всё равно в сумме получается занято 40 MiB — тут лучше складывать. Такое поведение оптимально для большинства частых случаев, кроме тех, когда складывается именно очень большое количество мелких строк — тогда накладные расходы становятся очень большими.
См. https://github.com/pieroxy/lz-string/issues/46#issuecomment-80531018, например — это пример посимвольной сборки был.
new Function работает так же как и eval
Вы ошибаетесь. eval
наследует текущую область видимости, а у new Function
она своя и в неё не попадает всё окружение, как в eval
.
new Function отличается от eval только тем, что он не может использовать переменные из области видимости, в которой он был вызван. Все остальное (область видимости, контекст, остальные переменные) точно такие же как у eval. Механизм действия данных подходов одинаков.
То что вы написали, это вообще какая-то околесица.
— Когда это eval стал наследовать текущую область видимости? Он использует глобальную область.
— new Function использует так же глобальную область. Возможно, вы имели в виду, что new Function создает область видимости данной функции. Но и eval может сделать точно также, если ему передать соответствующую конструкцию.
Когда это eval стал наследовать текущую область видимости? Он использует глобальную область.
> var x; (function() { var x; eval('x=10'); console.log(x)})(); console.log(x);
10
undefined
x
в какой области поменялся? В текущей (той, из которой был вызван). А вы сказали — в глобальной.
> (function() { eval('var y=10;'); console.log(y)})(); console.log(y);
10
ReferenceError: y is not defined
y
объявился в какой области видимости? В текущей (той, из которой был вызван). А вы сказали — в глобальной. И это явно не просто использование переменной, на которое вы ссылаетесь тут:
new Function отличается от eval только тем, что он не может использовать переменные из области видимости, в которой он был вызван.
И да, то, что через eval можно сэмулировать поведение new Function
— верно: засунув туда new Function
, например. Но я не вижу никаких разумных причин вызывать eval
вместо new Function
— используя new Function
, вы можете быть уверены, что у вас не захватится текущая область видимости, без дополнительных костылей.
Плюс не забывайте про оптимизации — eval
всегда вызывает деоптимизацию функции, которая его содержит (угадайте, почему).
И да, см. http://www.ecma-international.org/ecma-262/6.0/#sec-eval-x и http://www.ecma-international.org/ecma-262/6.0/#sec-function-constructor.
Во-первых, вы путаете понятия «выполнить» и «присвоить».
var y; (function() { var x; eval('y=10'); console.log(x)})(); console.log(y);
undefined
10
Во-вторых, думаете, что я не проверю ваш код:
(function() { eval('var y=10;'); console.log(y)})(); console.log(y);
10
10
В-третьих, в JavaScript есть только один механизм выполнения произвольного кода, который работает не как eval. И вы его, как я вижу не знаете. Все остальные способы: начиная с new Function заканчивая
Во-вторых, думаете, что я не проверю ваш код:
В чём выполняете, если не секрет? Я не могу воспроизвести такого поведения, как у вас, независимо от браузера или режима. Вы точно очистили окружение после предыдущей команды (в которой вы задали глобальный y
в 10
)?
Поведение eval
действительно зависит от режима, и в strict mode он ведёт себя несколько не так — объявленные в нём переменные не добавляются в окружающий контекст. Но и совсем не так, как вы показали. И это не решает всех его проблем.
Зависимость поведения eval
от strict mode — ещё один повод не использовать eval
, кстати говоря.
var y; (function() { var x; eval('y=10'); console.log(x)})(); console.log(y);
undefined
10
А какого поведения Вы ожидали? Вы подтвердили, что код, переданный в eval, выполняется в том контексте, в котором вызван eval.
(function() { eval('var y=10;'); console.log(y)})(); console.log(y);
10
Uncaught ReferenceError: y is not defined(…)
var y='main'; (function(){var y='func'; (new Function('','console.log(y)'))(); console.log(y)})(); console.log(y);
main
func
main
var y='main'; (function(){var y='func'; eval('console.log(y)'); console.log(y)})(); console.log(y);
func
func
main
Легко видеть, что Function работает в глобальном окружении. Это штатный способ создать функцию, не замкнув ничего лишнего.
Вот вам ещё один пример, с режимами, кстати:
'use strict';
eval('console.log((function() { return !this; })())');
(new Function('console.log((function() { return !this; })())'))();
выдаёт
true
false
Как видно из примера, eval
наследует текущий режим strict mode, а new Function
— нет.
Ещё раз — eval
и new Function
не одинаковые, они имеют разное влияние на окружение, они выполняются в разных режимах, они наследуют разные области видимости.
Вы оба продолжаете путать абстрактные понятия «выполнил» и «присвоил».
Рассмотрим пример:
var x = new Function(тыры-пыры);
Как это работает? Сначала выполняется код функции (тыры-пыры) в глобальной области. Поэтому этот код имеет доступ к глобальным переменным, но не имеет доступа к локальным переменным.
Затем выполняется присвоение, которое ограничивает область видимости данной функции.
Вот так. Все легко и просто. Главное представлять все на уровне абстракций.
Этим и объясняется такое поведение приведенного вами примера:
'use strict';
eval('console.log((function() { return !this; })())');
(new Function('console.log((function() { return !this; })())'))();
Во-первых, вы не ответили на вопрос, в чём вы выполняете код, что он даёт такие результаты, как у вас выше. Или вы в этом всё-таки ошиблись?
Во-вторых, в вашем примере var x = new Function(тыры-пыры);
вообще не выполняет код внутри функции, как несложно увидеть. Пока мы её не вызовем, конечно. Не верите — напишите там console.log
, что ли. Не верите в console.log
— напишите там долгий цикл.
Во-третьих, я советую вам разобрать именно по шагам все приведённые примеры, и понять, что вы ожидаете в них получить следуя вашей логике. Как минимум в одном случае вы уже явно удивились результату — когда сказали «думаете, что я не проверю ваш код».
— Код я проверяю в Firefox dev.
— Я привел пример, который показывает ошибку ваших представлений о понятиях «выполнил» и «присвоил».
— Я не зря привел пример с 2-х летним ребенком, которого учат читать. Научить двух летнего ребенка читать не возможно, т.к. восприятие его недостаточно сформировано.
Точно так же и с вами — вы не понимаете абстрактных понятий. И я с этим ничего не могу сделать. Я не могу дать вам пример кода, после понимания которого вы вдруг «прозреете» и начнете понимать работу js на уровне абстракций.
— Последующий ваш комментарий я не понял. Что вы хотели мне показать? Он работает предсказуемо. Если он вас ставит в недоумение или удивляет, то это потому что вы не понимаете абстракций.
Так. Давайте заново. Я утверждаю, что этот код в условии чистого окружения (в котором не было заранее объявлено переменной y
):
(function() { eval('var y=10;'); console.log(y)})(); console.log(y);
вне strict mode бросит исключение на втором console.log
, а в strict mode — на первом.
Вы мне написали, что он выводит два раза 10
, и сказали что я пытаюсь вас ввести в заблуждение:
Во-вторых, думаете, что я не проверю ваш код
Очевидно, вы не ожидаете увидеть там исключения. На самом деле — оно там есть, проверьте ещё раз.
Скорее всего, вы неправильно что-то сделали, когда проверяли первый раз (например, заранее объявили глобальный y
равный 10) — отсюда и неверные выводы. Попробуйте назвать переменную yyy
, например.
Я сказал, что поведение вас удивляет ровно потому, что вы не согласились с копипастой из командрой строки, решили, что я вас ввожу в заблуждение, и показали мне свой результат неправильной проверки с заверениями, что так и должно быть. Так как поведение вас удивляет — у вас неверное понимание того, как это работает.
(function() { eval('var y=10;'); console.log(y)})(); console.log(y);
10
ReferenceError: y is not defined
это встроеным dev tools. firebug завести не удалось, он говорит что теперь будет dev tools использоваться по умолчанию.
Так вот, в старых версиях хрома ~ v10 — v15 хромовский dev tools а так же firebug в консоли все делали через eval если мне не изменяет память.
Года 4 назад я наткнулся на это и с тех пор проверял либо запуская код из файла либо в nodejs REPL
и еще вопрос, вы со всеми так по хамски общаетесь или просто день не удался?
Покажите, где в моих примерах присвоение.
Вот вам ещё пример с eval
, для размышлений:
var z = 20;
function x() {
z = 10;
eval('var z');
console.log(z);
}
x();
console.log(z);
Угадаете, что будет?
Это, кстати, пример того, почему eval нельзя считать полностью равноценным вставке кода в тело функции — вызов eval
изменил привязку (примечание для всех: пожалуйста, не делайте так в реальном коде).
Попробовал клонирование через JSON.parse(JSON.stringify())
. У этого способа есть существенный минус — даты и регулярки потребуют особой обработки, функции пропадут, а циклические структуры вовсе свалят код в эксепшен. Но иногда этого хватает. Скорость в ~2 раза выше чем у lodash и jQuery.
Справедливости для, JSON.parse
и JSON.stringify
поддерживают replacer
/reviver
, которые как раз и нужны, чтобы сохранить дополнительные типы. Но тогда всё будет заметно медленнее работать, скорее всего.
for..in
в лоб даст только shallow-клон, для глубокого надо писать рекурсивную функцию — а зачем, если можно просто JSON.parse(JSON.stringify())
.
Когда я решал для себя этот вопрос, я предполагал, что в зависимости от количества свойств буду использовать разный способ копирования и написал две функции, но так случилось, что объектов, достойных JSON.parse(JSON.stringify()) — у меня так и не завелось.
Кстати, о птичках (то есть попугаях).
CloneFactory.prototype = Object.create(null);
даёт ещё 10-15% прироста в скорости.
Правда, у вас при этом не будет наследуемых от Object
методов, но в оптимизированном коде они не очень-то и нужны и без них можно обойтись.
Держите: https://jsfiddle.net/1sq3uhmo/.
Мне кажется, что если вы упираетесь в такие вещи, как скорость клонирования (!) объектов, вам надо переходить с JS на что-то более cтатичное. А пытаться писать хайлоад на динамическом языке, ещё и таком, как JS — не лучшая идея.
Это не так. v8 очень хорошо оптимизирован, и не особо сложными телодвижениями там можно достичь очень больших скоростей (как, например, показано в этой статье). Вполне можно посмотреть машинный код того, что получится — и вряд ли вы сделаете заметно быстрее.
По поводу «статичности» и того, как работает оптимизатор внутри — см, например http://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html.
Да, оптимизация V8 хорошая. Но статический компилятор сможет лучше. Просто из-за более полной информации.
Впрочем, я не хочу превращать эту ветку в холивар "статика против динамики". Мне действительно интересно, почему вы полагаетесь на такую сравнительно зыбкую штуку как JIT и не выделяете нагруженную часть в нативный аддон.
Выделение нагруженной части в нативный аддон автоматически выигрыша в скорости не приносит — его надо ещё тщательно оптимизировать, чтобы оказаться быстрее JITа. При этом ошибки в нативном аддоне могут обойтись дороже, из-за ручной работы с памятью.
На тестовых задачах с числодробилками — да, возможно. Но в реальной жизни такое случается не так часто, обычно время уходит не на алгоритмы, стоящие за логикой работы сервера.
Другое дело — может быть полезной выделение какой-то части в асинхронную нить, но этого можно добиться и не выходя из JS.
И да, стоимость переписывания на нативный код и поддержки нативного аддона в человеко-часах в большинстве случаев будет больше, чем стоимость сэкономленных ресурсов. Кроме случаев очень больших компаний со множеством серверов (Facebook, вон, предпочёл форкнуть PHP). Если можно добавить в код волшебный костыль для ускорения работы какого-то нагруженного места на порядок и забыть про это — почему бы и нет. Переписывать всё на нативном — зачем?
В целом — в большей части случаев не стоит связываться, а когда стоит — вы в этом уже точно будете уверены и перепробуете все остальные способы.
По поводу "тщательной оптимизации" — вы ею уже занимаетесь, только при этом полагаетесь на такие неочевидные штуки как поведение JIT. Вы и так уже вставили "волшебный костыль".
По поводу "ручной работы с памятью" — я нигде не упоминал С. В С++ есть масса средств для полу-автоматического управления памятью. Кроме него, есть Golang — статика со сборкой мусора. Рекламировать крутизну некоего языка на R тут не буду.
По поводу "тестовых задач и числодробилок" — вот как раз на них JIT себя и показывает хорошо, из-за однообразного кода.
Но правда и в том, что вашего конкретно случая я не знаю. Может, у вас эти кучи объектов потом перетасовываются так, что в нативе это действительно заморочно сделать.
Ну а "не стоит связываться" — да, не стоит. Потому что нативный V8 API местами проектировали в горячечном бреду, не иначе. Один только C++ only чего стоит.
const shallowClone = (object) => Object.create(
Object.getPrototypeOf(object),
Object.getOwnPropertyDescriptors(object)
);
Хорошая новость, т.к. Купертох node-v8-clone
забросил. Но бенчмарки и тесты вы оттуда притащили бы все-таки, они очень наглядные, плюс там очень хорошее разбираение того, что можно так скоприровать, и что нельзя.
вскоре могу наткнуться на такую же проблему, есть бэкенд на Sequelize в который заезжает 42МБ статистических данных из внешнего сервиса, часть из них доезжает в базу и делаются выборки.
AbstractQuery.prototype.handleSelectQuery
и для raw queries используем fast-clone. Ещё, к стати, если говорить о Sequelize, мы используем адаптер для БД mysql2 вместо стандартного mysql, т.к. он более быстро разбирает протокол.Сравнение с lodash здесь не к тому, что эта супер-библиотека лучше lodash и должна быть использована всегда, а к тому, чтобы показать, какой выигрыш от использования библиотеки (и этого подхода в целом) можно получить на частном случае, когда структура объектов одинакова (и циклических ссылок нет).
Быстрое клонирование объектов в JavaScript