Да, могут.
//.....Какой-то код
console.log(typeof str); // string
console.log(str.length); // 15
console.log(str); // zzzzzzzzzzzzzzz
Вы думаете, в этом примере строка занимает 30 байт?
А вот и нет! Она занимает 30 мегабайт!
Дьявол кроется в деталях. В данном примере — это «какой-то код». Очевидно, какой-то код что-то делает, что строка занимает много памяти. И вроде бы это вас не касается, но лишь до тех пор, пока это не ваш собственный код. Возможно, в вашем коде уже сейчас много мест, где строки занимают в десятки раз больше, чем в них содержится.
Предисловие
Сразу хочу заметить, что этот
UPD4: Firefox это, видимо, тоже касается.
Практичный пример
Сидите вы, значит, под пальмой за компьютером, пишете очередной AJAX на JavaScript, ни о чём не подозреваете, и у вас получается что-то вроде этого:
var news = [];
function checkNews() {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/WarAndPeace', true); //Проверяем сайт
xhr.onload = function() {
if (this.status != 200) return;
//Извлекаем новости
var m = this.response.match(/<div class="news">(.*?)<.div>/);
if (!m) return;
var feed = m[1]; //Новость
if (!news.find(e=>e==feed)) { //Свежая новость
news.push(feed);
document.getElementById('allnews').innerHTML += '<br>' + feed;
}
};
xhr.send();
}
setInterval(checkNews, 55000);
Написали, значит, проверили, опубликовали. Но вдруг оказывается, что сайт начинает жрать память. 200-300Мб — фигня, думаете вы, и уходите на пляж купаться, оставляя браузер открытым. Потом возвращаетесь, а ваш сайт уже 2 гигабайта! Вы удивляетесь, и сразу после этого Chrome крашится у вас на глазах.
Что-то здесь не так. Но код-то ведь простой! Вы начинаете искать утечку памяти в вашем коде и… не находите! А знаете почему? Да потому что её там нет! Вы не допустили ни одной ошибки. Однако проблема есть, заказчик будет недоволен, и решать всё равно придётся вам.
Профилирование
Без паники! Есть проблема — значит, решаем. Открываем профилировщик и видим, что там куча строк в памяти JS, которые там не должны быть. А именно — полностью загруженные страницы.
Смотрим дальше на Retainers.
Что же такое sliced string?
Суть проблемы
В общем, оказывается, что строки содержат ссылки на родительские строки! Что??
Это вроде бы не сильно на что-то влияет, но только до тех пор, пока вы не захотите из огромной строки вырезать маленький кусочек и оставить себе, а большую строку удалить. Ведь она не удалится. Она останется в памяти, потому что на неё ссылается новая строка, которую вы где-то сохранили, поэтому сборщик мусора не
Получается такая цепочка указателей:
какой-то массив или объект -> ваша новая маленькая строка -> старая большая строка
На самом деле всё ещё сложнее, у строки может быть несколько «родителей», но не будем усугублять.
Рабочий пример:
function MemoryLeak() {
let huge = "x".repeat(15).repeat(1024).repeat(1024); // 15МБ строка
let small = huge.substr(0,25); //Маленький кусочек
return small;
}
var arr = [];
var t = setInterval(e=>{ //Каждую секунду добавляем 25 байт или 15 мегабайт?
let str = MemoryLeak();
//str = clearString(str);
console.log('Добавляем памяти:',str.length + ' байт');
arr.push(str);
console.log('Текущая память страницы:',JSON.stringify(arr).length+' байт');
},1000);
//clearInterval(t);
В этом примере мы каждую секунду увеличиваем память на 25 байт. Ой ли? Смотрим диспетчер задач и видим, как память быстро растёт. Ладно, просто GC (сборщик мусора) немного запаздывает, сейчас очухается и очистит. Но нет. Проходит несколько минут, память заполняется до предела, — и браузер крашится.
Ради чистоты эксперимента можно довести до 1.5 гига, остановить таймер и оставить вкладку сайта на ночь. GC типа сам решит, когда пора чистить память, ага. Главное, дождаться.
Решение
В качестве решения можно предложить лишь «очистку» строки от внешних зависимостей. Тогда эти внешние зависимости GC сможет спокойно удалить, как недостижимые.
Простейший кейс, когда мы точно знаем, что в строке число, либо нам нужно получить число:
str = str - 0;
На этом простые кейсы кончаются. В любом случае, очевидно, что строку нужно превратить в другой тип. Это не обязательно единственное решение, но это точно поможет. Пробуем:
function clearString(str) {
return str.split('').join('');
}
Да, это работает, строка очищается.
Но можно немного улучшить решение. Если копнуть чуть глубже, то окажется, что V8 не оставляет ссылок у очень маленьких строк, меньше 13 символов. Видимо, такие маленькие строки проще скопировать целиком, чем ссылаться на область памяти в другой строке. Для Firefox это число 12. Воспользуемся этим:
function clearString(str) {
return str.length < 12 ? str : str.split('').join('');
}
Сложно сказать, изменится ли это число 13 в будущих версиях V8, и 12-24 в будущих версиях Firefox, но пока что так.
Что же это выходит? Все строки надо чистить?!
Конечно, нет. Это кэширование не просто так придумано. Оно реально ускоряет работу со строками. Просто иногда это выходит боком, как в примерах выше.
В качестве глобального фикса можно через прототипы заменить стандартные строковые фукнции, типа извлечения подстроки, но это замедлит их работу в десятки раз. Оно вам надо?
Лучшая стратегия такая. При анализе большой строки, вы обычно выделяете куски, потом из этих кусков выделяете более мелкие строки, потом их приводите в должный вид, всякие там replace(), trim() и т.п. И вот конечную маленькую строку, которая точно сохраняется в вечно-живой объект/массив, уже нужно чистить.
nickname = clearString(nickname); //как-то так.
long_live_obj.name = nickname; //уже чистая строка, всё ок.
А чистка в самом начале просто не имеет смысла. Лишняя нагрузка на процессор.
let cleared = clearString(xhr.response); //бред
Оптимальный способ очистки
function clearString(str) {
return str.split('').join('');
}
function clearString2(str) {
return JSON.parse(JSON.stringify(str));
}
function clearString3(str) {
//Но остаётся ссылка на строку ' ' + str
//То есть в итоге строка занимает чуть больше
return (' ' + str).slice(1);
}
function Test(test_arr,fn) {
let check1 = performance.now();
let a = []; //Мешаем оптимизатору.
for(let i=0;i<1000000;i++){
a.push(fn(test_arr[i]));
}
let check2 = performance.now();
return check2-check1 || a.length;
}
var huge = "x".repeat(15).repeat(1024).repeat(1024); // 15Mb string
var test_arr = [];
for(let i=0;i<1000000;i++) {
test_arr.push(huge.substr(i,25)); //Мешаем оптимизатору.
}
console.log(Test(test_arr,clearString));
console.log(Test(test_arr,clearString2));
console.log(Test(test_arr,clearString3));
console.log(Test(test_arr,clearString)); //700мс
console.log(Test(test_arr,clearString2)); //300мс
console.log(Test(test_arr,clearString3)); //280мс
UPD:
//Opera
console.log(Test(test_arr,clearString)); // 868.5000000987202
console.log(Test(test_arr,stringCopy)); // 493.80000005476177
console.log(Test(test_arr,clearString2)); // 435.4999999050051
console.log(Test(test_arr,clearString3)); // 282.60000003501773
//Firefox (ради интереса, ведь ваш сайт предназначен для всех браузеров)
console.log(Test(test_arr,clearString)); // 210
console.log(Test(test_arr,stringCopy)); // 2077
console.log(Test(test_arr,clearString2)); // 632
console.log(Test(test_arr,clearString3)); // 185
UPD2:
function clearString4(str) {
//Используем Buffer в Node.js
//По-умолчанию используется 'utf-8'
return Buffer.from(str).toString();
}
Результат:
//763.9701189994812 //clearString
//567.9718199996278 //clearString2
//218.58974299952388 //clearString3
//704.1628979993984 // Buffer.from
UPD3 Мой вывод:
function clearStringFast(str) {
return str.length < 12 ? str : (' ' + str).slice(1);
}
Существенно лучше никто не предложил. На строках 15 байт разница не большая, максимум в 2 раза. Но если увеличить строку до 150 байт, и тем более 1500 байт, разница гораздо больше. Это самый быстрый алгоритм.
Тесты: jsperf.com/sliced-string
Комментарии (159)
pallada92
24.04.2019 23:20+1Спасибо за статью, не знал об этом. Из статьи подумал, что у строки может быть только один родитель, тогда конкатенация с другой строкой должна решить проблему. Но, оказывается, все ещё сложнее: внутри V8 строки представляют собой ориентированый граф из узлов, у каждого из которых есть упорядоченное множество родителей. Тогда конкатенация делается очень быстро добавлением общего корня к двум узлам. Но иногда это может ухудшить производительность и есть библиотека github.com/davidmarkclements/flatstr, которая делает этот граф одноуровневым.
К счастью, во фронтэнд разработке редок сценарий, когда откуда-то приходят большие строки, из которых накапливаются кусочки, но в любом случае знать об этом крайне важно. Удивлён, что не встречал это в статьях типа «10 вещей о JS, которые вы не знали»Koneru
25.04.2019 09:03Работа с картинками в base64, сталкивался с такими строками(3~5МБ) буквально вчера, только там не было необходимости изменять их.
dollar Автор
25.04.2019 10:30Столкнулся с этим при разработке браузерного расширения. Все условия соблюдены: оно постоянно висит в памяти, делает периодически ajax, и складывает кусочки в объект-кэш (чтобы не повторять ajax по одним и тем же адресам).
MSC6502
24.04.2019 23:23-15Ну а что можно требовать от средства разработки, которое забацали на коленке без ТЗ, чёткого понимания целей и задач, возможностей расширения в будущем и кучи прочих тонкостей, о которых разработчики средств для веб-программирования даже и не подозревают. В итоге имеем то, что имеем, а за неимением лучших средств выдаем эту поделку уровня третьего курса провинциального вуза за better of best, надуваем щёки, проводим конференции, улыбаемся и машем, проклиная всё, когда завтра сдавать проект, а тут косячки, там косяки и вот тут громадный косячище, который за оставшиеся 8 часов никак не поправить. Вот.
avost
25.04.2019 01:03+2V8 написали на коленке без тз студенты третьего курса? Риали? Откуда дровишки?
tangro
26.04.2019 10:47Не студенты, конечно, но вот то, что без понимания целей — так это факт. Никто там в момент создания проекта не задумывался, что когда-то будет на нём нода, куча всяких поделок на Хромиуме и т.д. Это прямо вот чувствуется прямо вот даже по публичной апишке V8.
XenonDev
25.04.2019 03:04почему сразу студенты и на коленках? Это очень эффективный и производительный способ работы со строками. Использование счетчика совместных ссылок очень положительно сказывается на производительности. Более того, в ряде случаев позволяет неплохо экономить память.
Godebug
25.04.2019 10:43Ого, оптимизированного аглоритмоемкого монстра назвали поделием на коленке. Если что, v8 не фронт-энд разработчики писали :)
yarkov
25.04.2019 11:16+2Мне кажется, что MSC6502 имел ввиду не V8, а то, что JavaScript писался спонтанно за 2 недели.
Но в любом случае я не разделяю его мнения, просто уточнил.dagen
25.04.2019 11:58Не думаю, что он что-то имел ввиду: он просто хейтер, судя по его комментариям.
justme
25.04.2019 13:14Явно речь не о V8 а о языке. Но есть доля правды — то как был разработан и быстро внедрен язык сильно повлияло и ограничило разработчиков движков. Чем гибче и «проще» (для новых разработчиков) язык — тем сложнее (а значит и потенциально глючнее) его движки. За те 2 недели написания языка был заложен фундамент (или ТЗ, смотря как посмотреть) на котором уже приходится строить все движки, фреймворки, браузеры и т.д.
Godebug
25.04.2019 13:52Про JS надо помнить эпоху когда он создавался — тогда никто, даже в смелых мечтах (или унылых кошмарах) не мог бы представить то в чем мы сейчас варимся :)
п.с. К слову — в то время тоже было много споров/докладов/холиваров о MVC :)justme
25.04.2019 20:13+2Бесспорно. Валить все шишки на JavaScript определенно нельзя. Как по мне — это серия неудачных решений, «коротких путей» и недальновидных дизайнов. Взяли по-быстрому накатали язык. Потом он быстро пошел в массы и вскоре стал де-факто стандартом в web. Потом накатали движков и завоевали рынок одним браузером (а стало быть и одним главным движком JS, который имеет приоритеты в развитии сильно совпадающими с интересами одной большой коммерческой компании). А потом еще и додумались все это чудо зафигачить на сервера в качестве удобного и быстрого языка для прототипирования и небольших стартапов. Ну а дальше и это детище начало разивиаться, обретать популярность и оставаться на продакшене даже после того как проект переростает этап стартапа.
Вот и приплыли туда где мы есть сейчас, к серверам которые не умеют даже строки толком подчищать… и это я не говорю уже о бессмысленности побитовых операций а также о том что любой JS код хранит в себе еще и СТРОКУ СО ВСЕМ КОДОМ ВКЛЮЧАЯ КОММЕНТАРИИ И ПЕРЕВОДЫ СТРОК!!! (это если код чистый, не, прости господи, скомпилированный)
P.S. вероятно мой коммент заминусуют, но уж сильно меня печалит то к чему приводит вся история с JS и как этот достаточно нишевый и несколько спорный язык программирования пихается во все возможные щели (уже даже на микропроцессорах есть прошивки где можно на JS кодить!!!)Keyten
26.04.2019 00:06А какие есть неудачные решения есть в JavaScript, которые однозначно всем сильно мешают жить, и все однозначно согласны, что они плохие и лучше бы их не было?
Например, typeof null на практике сильно не мешает, а прототипы или динамическая типизация — вы не сможете сказать, что все разработчики однозначно против.homm
26.04.2019 00:47Глобальность любых переменных по умолчанию?
Keyten
26.04.2019 01:03Когда забываешь слово var? Кажется, за всё, вообще всё время это ни разу ничего у меня не ломало :). Более того, такие штуки видно сразу, а если в вашем коде видно их не сразу, например, двадцать вложенных функций каждая со своими локальными переменными, то у вас в любом случае проблемы.
Нет, я согласен, что есть море неудачных решений, которые при определённых обстоятельствах делают всё плохо, но кажется, что это вот примерно настолько же плохо, как typeof null.homm
26.04.2019 01:07То есть запишем в удачное решение?
Keyten
26.04.2019 01:11Запишем в не очень мешающее жить решение.
Я спросил, что (очень мешает жить) && (все согласны, что это плохо).justme
26.04.2019 12:32Любое решение можно оправдать и найти сторонников/защитников
Если вы изучали много разных языков программирования то могли заметить что многие современные языки (да и старые тоже) разрабатываются с точки зрения начального удобства и минимизации возможных ошибок. Так, например, в Python правильное выравнивание кода является обязательным и код будет выдавать ошибку в обратном случае. В Go табуляция принята как единственная система чтобы закрыть холи-вары (хоть я и приверженец пробелов). Также современные языки отказываются от таких принципов как перегрузка операторов, инструкция GOTO: и #define а также от множественного наследования. Все это помогает минимизировать количество потенциальных ошибок допускаемых программистами нового языка.
Возможность в non-strict режиме создать переменную которая будет публичной, невозможность НОРМАЛЬНЫМИ способами объявить приватную функцию или переменную, прототипное наследование, хранение всего кода функции в виде строки (включая переводы строк и даже комментарии) в переменной самой функции а также недостаточная проработанность языка возлагающая ответственность за оптимизацию и реализацию большинства моментов на разработчиков JS-машины (в следствии чего и произошла та проблема что описана в статье), да и вообще возможность сделать в JS практически что пожелаешь и выстрелить себе в ногу любым самым изощренным методом — это все не направлено на минимизацию потенциальных ошибок у программистов, даже напротив
justme
26.04.2019 00:51>прототипы или динамическая типизация — вы не сможете сказать, что все разработчики однозначно против
ну с таким подходом я вообще мало что смогу сказать) всегда найдутся сторонники той или иной точки зрения. динамическая против статической, ООП против функционального, компиляция против интерпретации…
Прошу заметить что то что я пишу это лишь мое мнение (которое наверняка разделит часть IT-сообщества) и оно сформировано во многом оттого что я приверженец «старой школы» (т.е. С/С++ и т.д.). Однако по моему мнению важные проблемы кроются именно в том что язык интерпретируемый и нетипизированный. Это бесспорно помогает молодым стартапам и вообще любым прототипам, PoC, MVP, MSP и т.д., но мне обидно видеть что это же используется и в финальной разработке. Когда Slack с их то масштабами грузит комп в десятки раз больше любого видео-плеера который производит потоковое декодирование, или когда очередная экранная клавиатура под Android/iOS весит больше чем весь Windows 95 — это, как по мне, просто не правильно
Тут не только JS виноват, конечно же. Но просто JS это для меня как яркий флаг символизирующий все движение в целом
А сторонники и противники появятся всегда. Тот же JS дал большой толчок в популяризации программирования в целом, т.к. начальный порог вхождения не велик, а учиться ему можно даже дольше чем тому же С (а значит и «рости» по должности и требовать ЗП повыше можно еще долго). К тому же я уже говорил что для определенных ниш этот язык и даже тот же node.js да и Electron вполне себе подходят и очень хорошо справляются со своей целью. Проблема не в том что эти решения есть а в том как и где их используютKeyten
26.04.2019 01:37Классика. Говорить, что язык объективно плохой, за то, что некоторые пишут на нём плохой код :)
Это вообще ооооочень странно — выдавать медлительность Gmail / Slack за аргумент, почему js плохой. Я вот работал с ANSYS, такой очень распространённый инженерный софт. Он умеет подвисать на минуту при right-click (нет, там точно не нужны сложные расчёты чтобы открыть контекстное меню с тремя пунктами) и тому подобных действиях. А ведь он написан, кажется, на C++. На сильно статически типизированном! И даже без Qt и т.п. штук.
Почему Slack или Gmail столько кушает — я не знаю. Правда. Всё, что я когда-либо писал, в принципе не могло столько есть. Я серьёзно не представляю, как можно написать такой простой интерфейс очень тяжёлым.
Вот это всё, надеюсь, проиллюстрирует то, что я хочу сказать.
Ну а про сторонников — нет, просто вы говорите про наличие критических всем мешающих недостатков как про что-то объективное, и с чем никто не спорит, мне интересно, что именно вы имеете в виду :). И одно дело если с этим не согласны два с половиной джуниора, другое — много процентов разработчиков.khim
26.04.2019 02:47Классика. Говорить, что язык объективно плохой, за то, что некоторые пишут на нём плохой код :)
Отрицание конечных результатов в оценке языка — такая же точно классика. Хотя ей обычно больше хаскелисты страдают.
На самом деле процент качества результата, получаемого на том или ином языке — это неплохая оценка. Она, возможно, сферически-вакуумно не очень объективна, но зато вполне практически применима: если подавляющее большинство проектов, написанном на языке A требуют чрезмерного размера ресурсов и медленно работают — то, скорее всего, и то, что вы получите — будет устроено так же.
Я серьёзно не представляю, как можно написать такой простой интерфейс очень тяжёлым.
Тем не менее на средние JS-проекты требуют сильно больше ресурсов, чем средние C++-проекты. И даже тот же ANSYS — то, что он тормозит, мы уже поняли, а вот сколько памяти он требует? Тот же гигабайт памяти, как и открытая у меня сейчас вкладка с GMail'ом?
Практика показывает, что как раз количество памяти потребляемое программой — гораздо сильнее зависит от языка, чем скорость работы. И вот тут у JS — всё плохо: написанные с его помощью проекты жрут либо много памяти, либо очень много памяти. При этом LUA какая-нибудь — таких ресурсов не требует. Несмотря на свою динамическую типизированность и сборщик мусора… Да — она гораздо медленнее, но как раз на суперскорость она и не претендует…Keyten
26.04.2019 17:36Допустим, но как вы докажете, что большинство проектов на js едят много памяти и тормозят? :) По моим данным, всё совершенно наоборот — на js пишут высоконагруженные штуки вроде сервера сообщений вк, и вообще js наверное самый оптимизированный из динамически типизированных интерпретируемых языков.
И более того, если у вас кушает много памяти что-то в браузере или в electron — почему вы уверены, что проблема в языке, а не в том самом Chrome, который вообще-то известен своей прожорливостью? К слову, на чём он там написан, на плюсах? Давайте ругать плюсы за прожорливость хрома и гмейла в частности).
ANSYS, кажется, и памяти ел больше, чем хром в брачный период. Но я точно не скажу, это было давно и в страшном сне.
Про Lua совершенно очевидно напрашивается вывод: это трейдофф память -> скорость. Разработчики V8, вероятно, решили, что работать быстро важнее, чем есть мало памяти. Это мои домыслы, но тем не менее. И высоконагруженные сервера пишут не на требующем малых ресурсов Lua, а, как ни странно, на js.khim
26.04.2019 20:21вообще js наверное самый оптимизированный из динамически типизированных интерпретируемых языков
Возмножно. Но все эти оптимизации всё равно не позволяют достичь скорость написанной на статически типизированном язуке — однако требуют кучу дополнительных ресурсов.
К слову, на чём он там написан, на плюсах?
Частично.
Давайте ругать плюсы за прожорливость хрома и гмейла в частности).
Не получится: версия десятилетней давности, написанная чисто на плюсах, особенно много и не жрёт (по современным меркам). А вот версия, в которой за последние лет 10 перенесли кучу всего с C++ на JavaScript — совсем другое дело.
И высоконагруженные сервера пишут не на требующем малых ресурсов Lua, а, как ни странно, на js.
Высоконагруженные сервера (сотни тысяч и миллионы QPS) до сих пор пишут на C++. На js пишут сервера, при создании которых хочется использовать армию дешёвых JS-программистов — а потом начинаются приседания, когда оказывается, что железо-то всё-таки небесплатное…
justme
26.04.2019 12:10+1Мне кажется вы не внимательно прочли что я написал. Про Gmail я не писал ни слова. Про Slack я четко написал что «тут не только JS виноват, конечно же». Про причины тормозов Slack (а точнее того сколько всего тянет за собой Electron и тому как он работает с процессами браузера на каждую команду писали на хабре уже много раз)
Проблема которую я описал не столько в языке сколько в том как и где его применяют. Любой язык имеет свою нишу, универсальных и идеальных не бывает. Кроме С++, конечно же :D (если что это шутка, не вырывайте из контекста, С++, к сожалению, и своих проблем хватает и применимость ограниченная)
«Некоторые пишут плохой код» — это, к сожалению, крайне частая проблема программистов JS. Отчасти дело кроется в «низком пороге входа», а отчасти в том, что, как мне кажется, чтобы написать хороший код на JS нужно потратить на его изучение и набивание шишек намнооого больше времени и усилий чем на низкоуровневых языках. Вход проще, а обучение дольше. Ну и желания доходить до конца обучения мало у кого есть, ведь ЗП у JS программистов сейчас высокие и обоснованны больше спросом а не качеством предложения, потому ЧСВ у новых программистов повышается и они не считают что им что-то еще надо. Подучить больше фреймворков, больше либ, больше подходов… и дописать в резюме строчку Senior спустя год-два программирования.
Я лично провел несколько сотен собеседований за свою жизнь и могу точно сказать что среди тех кого я собеседовал раздутое чаще всего именно у JS разработчиков. Причины я описал выше. Не считаю что это их вина или что у них плохой потенциал (честно, никого не хочу задеть или обидеть), просто пытаюсь показать как низкий порог вхождения влияет на подход и виденье индустрии в целом
Возвращаясь к JS — извините, но я твердо уверен что JS не подходит для высоконагруженных серверов с миллионами запросов в секунду и bigdata на борту. Да, построить такую систему можно (путем горизонтального скейлинга и больших серверных затрат), но зачем?
А именно так часто и получается когда продукт и нагрузка растет постепенно и момента когда «есть время чтобы все переписать» так и не наступаетkhim
26.04.2019 20:29если что это шутка, не вырывайте из контекста, С++, к сожалению, и своих проблем хватает и применимость ограниченная
На самом деле как раз эта «ограниченная применимость» и приводит к написанию качественного кода.
чтобы написать хороший код на JS нужно потратить на его изучение и набивание шишек намнооого больше времени и усилий чем на низкоуровневых языках
Не совсем так. На JS просто можно написать плохой код — и он вам это «простит». C++ гораздо более жесток: если вы не можете внятно описать ваш дизайн, то добиться того, чтобы ваша программа не падала при малейшем шевелении мышкой — будет очень сложно.
Соотвественно ваша программа будет либо хорошей, либо не доживёт до стадии релиза. Такое «программирование по бразильской системе».justme
26.04.2019 22:36На самом деле как раз эта «ограниченная применимость» и приводит к написанию качественного кода.
Не могу с вами согласиться. У JS есть своя ниша, но это не мешает всем пропихивать ее где только можно и нельзя. Качественность кода достигается качеством программистов, и вовсе не применимостью
Не совсем так. На JS просто можно написать плохой код — и он вам это «простит». C++ гораздо более жесток: если вы не можете внятно описать ваш дизайн, то добиться того, чтобы ваша программа не падала при малейшем шевелении мышкой — будет очень сложно.
Эм… с чего вы взяли что на С++ нельзя написать плохой но рабочий код? Еще и как можно, и полно такого. А JS «простит» весьма спорно. То что он не упадет а просто закончит выполнять функцию и напишет сообщение об ошибке в консоль (которое потом никто не прочтет а просто забьет) — так это непредсказуемое выполнение вместо полного падения. Что предпочтительнее — тема весьма спорная. Но называть это «простит» — я бы точно не стал
ИМХО, но написать ХОРОШИЙ код на JS куда сложнее чем на С++. Любой код, или такой который, как вы выразились, будет «прощать» — конечно же проще на JS, но это не хорошийkhim
26.04.2019 22:56А JS «простит» весьма спорно. То что он не упадет а просто закончит выполнять функцию и напишет сообщение об ошибке в консоль (которое потом никто не прочтет а просто забьет) — так это непредсказуемое выполнение вместо полного падения.
Очень много вещей, который в C++ просто не пропустит компилятор в JS породят… нечто. Не всего понятно, что, то функция не упадёт.
Например{} - []
— почему это равно-0
? Да, ответ можно прочитать в спецификации языка, но практического смысла тут никакого — и в C++ программа, содержащая подобное просто не скомпилируется.
ИМХО, но написать ХОРОШИЙ код на JS куда сложнее чем на С++.
Конечно. Чем больше «странного» и «дурацкого» кода является валидным и как-то работает и что-то таки делает — тем сложнее попасть в узкое подмножество «хорошего» кода…
justme
26.04.2019 12:45+1много процентов разработчиков
как по мне количество тут не столь важно, как качество
можете побить меня палками, но я не считаю эффективным вести обсуждения с девелоперами которые писали всю жизнь на одном языке программирования (или только на одном типе языков) о том плох или хорош JS. И опять же — я в принципе не считаю такое обсуждение логичным, т.к. у JS есть своя ниша которую он успешно закрывает
Чтобы вести эффективную и интересную дискуссию нужно чтобы человек хоть пару-тройку лет писал на строго типизированном языке (и компилируемом) и хоть еще пару-тройку на нетипизированном. А лучше чтоб в запасе было языка 3-5 на которых он и писал и глубоко изучал. В противном случае процент эффективной беседы будет минимальным, а все больше будет вкусовщины и обкидывания какашками
За себя могу сказать что я несколько лет писал на Java, еще несколько — Objective-C, пару лет на JS и на ActionScript
По мелочи на asm, C++, Perl, Python, Go, C#, bash
Также уже много лет варюсь между тех миром и миром бизнеса, потому могу сказать что именно бизнес двигает IT-рынок в сторону таких решений как node.js. Программисты все меньше влияют на развитие IT
Не считаю что этого достаточно чтобы быть рупором мира технологий или вообще претендовать на правильные взгляды, но некоторое мнение о подходах к языкам и тому куда все это идет я сложил
transcengopher
26.04.2019 16:05Программисты все меньше влияют на развитие IT
И это печально — неспециалист не может сделать ничего хорошего в узкоспециальной области в долгосрочной перспективе (в краткосрочной — может, но обычно случайно).
justme
26.04.2019 16:44И да и нет. Мне, как человеку все же больше техническому чем бизнес, это весьма печально и я полностью согласен что бизнес-люди не могут строить долгосрочные эффективные планы на развитие технологий. С другой стороны (и это уже во мне говорит моя бизнес-сторона) — технологии это лишь инструменты, а значит должны подстраиваться под тех кто их использует и под те задачи которые перед ними ставят. Без бизнеса развитие технологий было бы крайне медленным и нацеленным на гос.сектор (а тогда пути развития технологии могли бы быть не менее прискорбными, чего только запрос «выдать ключи» от ассиметричных алгоритмов шифрования стоят)
Как и все в мире тут есть несколько сил движущих в разные стороны но тем не менее их вектор идет на общее развитие. Как по мне то лучшее что мы, как технари, можем сделать — это стараться хорошо разбираться в большом количестве технологий (читай «инструментов») и понимать сильные и слабые стороны каждой (понимать и принимать, а не хейтить или бездумно защищать). Тогда в те моменты когда от нас что-то зависит мы можем применять подходяющую технологию в подходящем месте. Например заранее планировать переход на новую архитектуру и технологию при достижении числа юзеров выше X, или числа запросов к серверу выше Y, или к базе выше Z… короче планировать заранее и стараться доносить такую потребность до бизнеса. В общем искать взаимовыгодные компромисы
Например Slack — я четко понимаю что решение написать единую платфрому на JS и для десктопа использовать Electron было вполне неплохим решением на начальных этапах. Однако сейчас у них столько денег что они могут себе позволить написать и поддерживать нативного клиента, что значительно уменьшило бы его нагрузку, перестало жрать батарею а также хоть немного ускорило бы инициацию звонков
khim
26.04.2019 00:58все однозначно согласны, что они плохие и лучше бы их не было?
Так не бывает. У любого, самого идиотского решения, даже того, про которое сам автор признал, что они идиотское и хорошо бы его исправить — всегда находятся защитники. Стокгольмский синдром во всей красе.
Например, typeof null на практике сильно не мешает, а прототипы или динамическая типизация — вы не сможете сказать, что все разработчики однозначно против.
Тут есть некоторая проблема в том, что идеальных языков вообще не бывает, а их практическая применимость — очень сильно зависит от задач. И, в частности, количество попыток прикрутить в JavaScript'у статическую типизацию показываает, что режим тяп-ляп-и-в-продакшн уже многих не устраивает… а поделать с этим ничего нельзя.Keyten
26.04.2019 01:16Я разумеется не имею в виду, что все 100% разработчиков должны быть согласны, а какой-нибудь несогласный стажёр всё ломает. Я имею в виду, что не нужно выдавать субъективность за объективность, а открытые вопросы за решённые. Вы не можете выдать за объективную истину «пробелы лучше табов» или «статическая типизация лучше динамической» или «прототипы лучше классов», потому что примерно 50% (или 30% или 20% или 10%) с вами не согласятся, и однозначного ответа, почему одно лучше другого, не найдено.
количество попыток прикрутить в JavaScript'у статическую типизацию показываает, что режим тяп-ляп-и-в-продакшн уже многих не устраивает
Ага, а количество попыток затащить JS на сервер показывает, что все остальные серверные языки уже многих не устраивают.
Ничего это не показывает. Только количество разработчиков, которые попытались пересесть со статически типизированных языков на js, и ощутили, что им не нравится писать без типов. Это даже легко доказать: если человек изучает программирование начиная с js, ему и в голову не придёт тащить в него типы :)khim
26.04.2019 02:22Ага, а количество попыток затащить JS на сервер показывает, что все остальные серверные языки уже многих не устраивают.
Именно так. Несмотря на то, что «не устраивают» они, по большому счёту, ровно по одному показатели: наличию достаточно дешёвой рабочей силы — это именно так.
Это даже легко доказать: если человек изучает программирование начиная с js, ему и в голову не придёт тащить в него типы :)
Я видел массу контр-примеров. Обычно идёт «путешествие с возвратом»: вначале человек попадает, так или иначе, в проект, где тысячи программистов пишут миллионы строк кода (ни одного такого успешного проекта, написанного на языке с динамической типизацией мне лично не известно… думаю что они всё же есть — но их очень мало), а потом уже возвращается в JavaScript с осознанием того что есть статическая типизация и для чего она нужна…
justme
26.04.2019 12:20+1JS на сервер затащили во многом потому что нужна рабочая сила, а JS на подъеме и разработчиков проще найти или научить. Сейчас очень много молодых стартапов живущих по циклу: написать PoC, получить первые инвестиции, на них написать MVP/MSP, получить инвестиций побольше, расширить команду и писать конечный продукт
В таком подходе, а также в современных Agile и т.п. на начальных этапах JS выглядит как неплохое бизнс-решение. А если те же кодеры смогут писать еще и сервер = то вообще шик!!!
khim
25.04.2019 20:30Про JS надо помнить эпоху когда он создавался — тогда никто, даже в смелых мечтах (или унылых кошмарах) не мог бы представить то в чем мы сейчас варимся :)
Представить-то как раз могли. Более того — JavaScript им ровно для этого и понадобился. Чтобы можно было использовать один язык и на клиенте и на сервере (в NAS). Вот только клиент взлетел, сервер умер… и пришлось ждать ещё очень долго пока совсем другие люди первоначальную идею реализуют…
Godebug
25.04.2019 13:48Откровенно говоря, понять что имеется в виду под «средств для веб-программирования» очень сложно. Особенно дико этот коммент смотрится в топике про особенности оптимизаций v8 :)
sapfear
25.04.2019 13:54+1Ну а что можно требовать от комментатора, который пришел сюда без статьи, четкого понимания современных технологий и разработки.
В итоге имеем то, что имеем — пустобрешие.
1c80
25.04.2019 21:45Это не косяк же, а особенность поведения, вот если бы в один прогон было 30 мегов, а другой 15 байт, то тогда да, был косяк.
XenonDev
25.04.2019 02:33Я предлагаю использовать такой способ:
function clearString4(str) { return (str + '\0').substr(0, str.length) }
Не совсем понятно почему в
clearString3()
строка занимает в 2 раза больше памяти. По сути мы создаем новую строку с пробелом вначале. Когда делаемslice(1)
, то должна вернуться ссылка на созданную строку(' ' + str)
со смещенным началом на 1 символ. Т.е.(' ' + str).splice(1)
должен ссылаться на(' ' + str)
и оверхед должен быть в 1 символ.
P.S. У меня почему-то версия c.substr()
всегда чуть быстрее чем версия с.slice()
. Странно почему так получается… В данном случае они должны быть идентичны.khim
25.04.2019 03:32Я бы рекомендовал всё-таки разбирать строку на символы и «собирать» обратно.
Все вот эти «быстрые» способы — это игра с огнём. Там вверху приведен случай, когда один «быстрый» алгоритм очистки работал-работал, а потом вдруг перестал… И тут то же самое тут может случится.
На сервере же, где вы всё можете контролировать, можно просто «вытащить» соответствующую низкоуровневую функцию из V8 и вызывать её…qw1
25.04.2019 19:10И однажды оптимизатор начнёт выбрасывать конструкции split+join, как не меняющие строку.
khim
25.04.2019 20:34Это всё-таки очень спицифическая оптимизация. Такой код почти невозможно написать случайно — так что разработчики будут понимать, что это «очистка» строки.
Хотя было бы неплохо где-нибудь хотя бы оффициальную рекомендацию увидеть. Типа «делайте так — и мы гарантируем, что это не выбросят». Это да.qw1
25.04.2019 21:55Выше было официальное решение, специфичное для v8.
khim
26.04.2019 01:22+1Оно, увы, специфично не для v8, а конкретно для Node.js.
В браузере его не применить…
red_andr
25.04.2019 22:59Или сделать библиотечную функцию, которая гарантированно будет всегда работать.
Psychosynthesis
25.04.2019 05:19Ого, интересно, спасибо!
А что, если просто написать функцию, которая будет побайтово копировать нужные символы из строки?dollar Автор
25.04.2019 12:09Будет медленнее, чем встроенные функции. Но можете попробовать, напишите свою clearString() — протестируем, сравним скорость. Собственно, .split('').join('') — это и есть побайтовое копирование.
WanSpi
25.04.2019 12:39Я бы не был так уверен, тот же '.split('').join('')' создает массив, что уже нагружает память, и при больших обьемах данных, может уступать собственным решениям, вот к примеру быстро написанная функция показывает лучший результат при больших обьемах:
var stringCopy = function(str) { var strCopy = ''; for (var i = 0; i !== str.length; i++) { strCopy += String.fromCharCode(str.charCodeAt(i)); } return strCopy; }; console.time('Timer'); for (var i = 0; i !== 100000; i++) { 'Some text'.split('').join(''); } console.timeEnd('Timer'); // Timer: 55.083984375ms console.time('Timer'); for (var i = 0; i !== 100000; i++) { stringCopy('Some text'); } console.timeEnd('Timer'); // Timer: 52.052978515625ms
А если увеличить до 1 миллиона, то цифра сильно начнет перевешивать не в сторону встроенных функций:
Timer: 523.544189453125ms
Timer: 480.338623046875msdollar Автор
25.04.2019 13:14Интересно. Но всё же нет. Я добавил вашу функцию к своим тестам и вот что вышло:
console.log(Test(test_arr,clearString)); console.log(Test(test_arr,stringCopy)); console.log(Test(test_arr,clearString2)); console.log(Test(test_arr,clearString3)); 752.80500005465 1093.955000047572 309.3099999241531 262.0650000171736
Несколько прогонов — тот же результат. И у меня в цикле как раз миллион итераций. То есть ваша функцию всё же медленнее. По памяти предлагаю не считать временный массив (если он вообще создаётся), который GC обязательно удалит — это не тот расход памяти, о котором заявлено в статье в качестве проблемы.
Не знаю точно, почему у вас другие результаты, но хочу обратить ваше внимание на способ тестирования. Например,компилятороптимизатор вот это в теории вообще может выкинуть:
'Some text'.split('').join('');
Ведь результат нигде не используется.
К тому же у вас искусственная строка, а нам для чистоты эксперимента нужно чистить именно «грязные» строки.
Поэтому в моих тестах я из большой строки нарезаю много маленьких, которые ссылаются по разным адресам на большую. Это помешает оптимизатору мешать тестированию, какой бы он ни был. Далее, в самом цикле результат мы не выкидываем сразу, а обязательно куда-то сохраняем, это тоже мешает потенциальному оптимизатору искажать результаты. Я не эксперт в V8, а просто рассчитываю на разумный подход оптимизатора. Возможно, я что-то не учёл, тогда прошу обратить на это моё внимание.WanSpi
25.04.2019 13:32+1Попробовал ваш тест несколько раз, и у меня получились другие результаты:
console.log(Test(test_arr,clearString)); // 868.5000000987202 console.log(Test(test_arr,stringCopy)); // 493.80000005476177 console.log(Test(test_arr,clearString2)); // 435.4999999050051 console.log(Test(test_arr,clearString3)); // 282.60000003501773
Что то мне подсказывает что это зависит от браузера (пользуюсь оперой), так как в той же лисе:
console.log(Test(test_arr,clearString)); // 210 console.log(Test(test_arr,stringCopy)); // 2077 console.log(Test(test_arr,clearString2)); // 632 console.log(Test(test_arr,clearString3)); // 185
WanSpi
25.04.2019 13:40Что то мне подсказывает что лиса откидывает '.split('').join('')', или как то хитро оптимизирует, но могу ошибаться, так как попробовал ту же '[...str].join('')' и она выполняеться уже порядка 700мс.
GavriKos
25.04.2019 21:53Может посимвольное, а не побайтное? 1 символ в том же юникоде будет занимать больше байта.
dollar Автор
25.04.2019 22:10Формально вы правы, поэтому статью я начал с «30 байт» на 15 символов, но реально в Chrome строка из 15 млн. символов «z» занимает 15 мегабайт, судя по диспетчеру Chrome. Плюс я считаю хорошим тоном отвечать на языке того, кто задал вопрос, если это не меняет суть ответа.
Cerberuser
26.04.2019 06:01Ну, строка из 15 миллионов символов «z» и должна занимать 15 мегабайт — ASCII же в UTF-8 умещается в один байт (или в JS строка — это UTF-16?)
khim
26.04.2019 06:25или в JS строка — это UTF-16
Именно так. Как обычно: изначально думали уложиться в UCS-2, не получилось создали идиотский вариант, который одновременно и медленный и много памати жрёт… возможно V8 «втайне от пользователя» использует UTF-8?shaukote
26.04.2019 15:15+1Насколько я помню, так и есть, V8 автоматически переключается между Latin-1 и UTF-16 в качестве внутреннего представления строк.
(Пруфов, увы, не будет — не могу сходу найти.)
igormich88
25.04.2019 08:00+1Странно что нет стандартного метода который принудительно отвязывает строку от родителей. Так как по моему проблема всех самописных решений их ненадежность при смене/обновлении платформы.
ReklatsMasters
25.04.2019 11:56нет стандартного метода который принудительно отвязывает строку от родителей
Это не часть спеки языка, это делали реализиции в конкретном движке. И в v8 есть свой персональный метод
%FlattenString(s)
, правда для этого нужно включить нативный синтаксис--no-allow-natives-syntax
.igormich88
26.04.2019 18:14И в результате получаются вот такие библиотеки
github.com/davidmarkclements/flatstr/blob/master/index.jsyarkov
26.04.2019 19:34+1Смутила эта строка
var v8 = require('v' + '8')
Не подскажете для чего это?mayorovp
26.04.2019 19:52Чтобы в случае, если эта библиотека случайно окажется собрана каким-нибудь webpack — он не пытался искать и загружать несуществующий модуль во время сборки, а выкинул исключение уже при выполнении.
shaukote
28.04.2019 04:22Насколько я понимаю,
%FlattenString
(как и использующая егоflatstr
) не про то и здесь она не поможет — она "уплощает" cons strings (строки, образованные в результате конкатенации), но sliced strings она возвращает без изменений (по сути своей её реализация не слишком сложна).
Собственно, как уже написал ниже flapenguin — применительно к описанной статье проблеме
%FlattenString
поможет только если сначала к "проблемной" sliced string что-нибудь прибавить (и получится тот самый(' ' + str).slice(1)
).
jreznot
25.04.2019 08:17+1Удивительно, но в серверной Java (ещё в 8 версии) отказались от всех этих оптимизаций и сделали копирование данных при выделении подстрок. Кажется в JS придут туда же, но не сразу.
Dima_Sharihin
25.04.2019 16:43+1А в C++ есть std::string, const std::string &, std::string && и std::string_view.
И сиди думай, который где использовать
Zoolander
25.04.2019 09:30+1судя по описанию бага, такая течь может возникнуть при использовании
- concatenating with empty string
- trim()
- slice()
- match()
- search()
- replace() with no match
- split()
- substr()
- substring()
- toString()
- valueOf()
Странно, я постоянно работают с мини-парсерами, которые используют кучу этих функций и проблем с памятью не наблюдал. Вероятно, потому что мои функции работают с очень маленькими строками, поэтому heap и не превышает обычных размеров для моих приложений (8-9 мегабайт)
PS: кроме того, этот перечень, как и описание бага было составлено в 2013 — и с тех пор, возможно, часть утечек перестала воспроизводиться.khim
25.04.2019 20:50+1Странно, я постоянно работают с мини-парсерами, которые используют кучу этих функций и проблем с памятью не наблюдал.
Тут надо понимать, что этот способ даже экономит память, если строки переиспользуются.
Проблемы возникают только когда вы берёте кусок большой строки, а потом про большую «забываете» (а GC-то помнит!).
namikiri
25.04.2019 10:02Простейший кейс, когда мы точно знаем, что в строке число, либо нам нужно получить число:
str = str - 0;
А чем такая конструкция хуже? Или нет разницы?
str = +str;
radist2s
25.04.2019 10:17-2Конструкция хуже тем, что ее сложно разобрать чисто визуально и понять для других программистов, незнакомых с ней.
iliazeus
25.04.2019 12:14Тогда уж лучше писать
str = Number(str);
vsb
25.04.2019 13:12Почему не parseInt?
f0rmat1k
25.04.2019 17:42Я думаю причины скорее в семантике. parseInt намекает, что мы хотим попарсить строку и извлечь намбер, и нам ок, если 10f5 превратится в 10. Number же честно пытается 1 в 1 сделать перевод и вернет NaN в случае ошибки, что больше соответствует нашему желанию и ожиданиям.
dollar Автор
25.04.2019 10:32Формально отличий нет. Первым делом происходит автоматическое приведение к числу, которое и творит всю магию, то есть получаем либо число, либо хотя бы NaN. А дальше уже вычисляется сама операция, которая ничего не делает.
Из той же серии:
str = str * 1;
Экономим символы в коде:
str-=0; str*=1;
stereoworlder
25.04.2019 10:44Это вечная проблема, когда сайт делает «чудо-профи»… он же — дилетант
dollar Автор
25.04.2019 10:47+1Этот «дилетант» прочёл всю спецификацию JavaScript всех версий от корки до корки. И вообще, у него может быть Firefox, где проблемы нет. Но заказчик всё же позвонил этому «горе-профи».
SergeiMinaev
25.04.2019 13:07В Firefox пример с утечкой слегка работает — объем потребляемой памяти возрастает где-то на 200-250мб, а потом приходит GC.
dollar Автор
25.04.2019 15:39Это не утечка, так и должно быть. Мусор сначала накапливается, а не чистится сразу. И если наращивать память по 15Мб в секунду, то где-то несколько сотен мегабайт и должно быть в пике. С применением clearString() будет та же картина и в Chrome.
SergeiMinaev
25.04.2019 15:47Автор написал, что ссылки на родительские строки — особенность V8 и, если продолжать мысль, то в FF это проявляться вообще не должно. Но оно проявляется, просто GC приходит секунд через 20 после запуска примера MemoryLeak().
В любом случае, и в FF иногда стоит использовать clearString(), чтобы не съедать лишнюю память в ожидании сборщика мусора.homm
25.04.2019 16:06+1Исходные строки по 15 мегабайт создаются хоть в FF, хоть в Хроме. Разница в том, что в FF они как и любые созданные объекты, освобождаются, когда приходит сборщик мусора (но не раньше, поэтому успевают накопиться 200-250мб), а в Хроме нет, потому что на них остаются формально живые ссылки.
Если в будете использовать в FF clearString(), это никак не поможет, потому что исходные строки по 15 мегабайт все еще будут создаваться.
SergeiMinaev
25.04.2019 16:19Да, действительно, в FF никак не повлияло. А вот в Chrome при использовании (' ' + str).slice(1); память вообще перестала сколько-нибудь заметно увеличиваться.
Tanriol
28.04.2019 02:23+2В Firefox есть точно та же проблема, только максимальная "короткая" строка не 12 байт, как в Chrome, а 23 / 11 символов (для Latin1 / двухбайтных — см. раздел inline strings по ссылке). На 24 символах проблема воспроизводится. Кстати, следует учитывать, что выбор режима между Latin1 и двухбайтным выполняется исключительно по тому, в каком режиме была изначальная строка — то есть если там был Юникод за пределами Latin1, то и все подстроки будут оставаться ссылками по лимиту в 11 символов, а не 23.
Что ещё веселее, если внимательно посмотреть на результаты теста в JSPerf на Firefox, то окажется, что "решение" через
(' ' + str).slice(1)
работает за время, не зависящее от длины подстроки. Дело в том, что в Firefox эта операция выполняется через строку типа Rope и не приводит к "отсоединению" от базовой строки, то есть не выполняет свою задачу. Лишний раз видим, что самые быстрые решения зачастую оказываются менее надёжными.dollar Автор
28.04.2019 03:25А вот это интересно, спасибо. Проблема воспроизводится, если вырезать 25 символов. И это всё меняет. Непонятно только, почему при перезагрузке вкладки или при переходе на другой сайт память не очищается. Только если закрыть вкладку — убивается соответствующий процесс, а вместе с ним и память.
Ogoun
25.04.2019 14:32+3Еще интересный момент, как именно крашится chrome, он не анализирует есть ли утечка памяти, а тупо при превышении аллоцированной памяти в два гига на странице пишет что ВОЗМОЖНО есть утечка памяти и крашит страницу принудительно, не давая возможности что то предпринять. Соответственно держа память под контролем и просто работая с большими объемами, легко схлопотать этот краш ни за что. Edge, firefox, opera, vivaldi при этом справляются.
Cerberuser
25.04.2019 14:50+2Хех. Буквально сегодня на одном рабочем сайте внезапно обнаружил колоссальный расход памяти, при случае надо будет глянуть, оно или не оно :)
FFxSquall
25.04.2019 15:04+1Спасибо, что разложили по полочкам. Видел ваш вопрос на Тостере и принимал участье в комментариях. Оказывается иногда полезно углубиться в то как работает V8 внутри. Постоянно надо быть начеку =)
V1tol
25.04.2019 15:10Попробовал в Node.js 10.15.3 такой вариант:
function clearString4(str) { //Используем Buffer в Node.js //По-умолчанию используется 'utf-8' return Buffer.from(str).toString(); }
Результат:
763.9701189994812 567.9718199996278 218.58974299952388 704.1628979993984 // Buffer.from
homm
25.04.2019 21:52А попробуйте еще для верности UTF-16. Это внутреннее представление строк в JS (обязательное условие, прописанное в спеке) и возможно будет сильно быстрее.
ReklatsMasters
25.04.2019 22:47-1ascii
самая быстрая кодировка для буферов, на нескольких проектах убеждался. А всё потому, что там простое конвертирование 1 в 1 без всякой ерудны юникодной. А вообще это дикость конечно, из строки, потом в буфер, потом обратно...homm
25.04.2019 22:56ascii не может быть кодировкой для произвольной строки, только для специфических строк. Диапазон ascii 128 значений, диапазон внутреннего представления строк в JS — 65 тысяч.
ReklatsMasters
26.04.2019 09:54Точно. Упустил этот момент. В любом случае это глупость гонять строку через буфер. Создание типизированого массива достаточно дорогое удовольствие. А тем более конвертирование его в строку.
V1tol
26.04.2019 13:30А вообще это дикость конечно, из строки, потом в буфер, потом обратно...
Никто и не спорит. Но так же является дикостью то, что V8 не чистит «большую» строку, когда на неё нет прямых ссылок и не копирует необходимые чанки в подстроки. Если не нужно парсить мегабайты строк в секунду — то схема «строка -> буфер -> строка» является, возможно, единственным гарантированным способом в Node.js именно скопировать строку без всяких приватных функций V8 и прочих хаков.
homm
26.04.2019 11:09+2Проверил на node v8.10.0 и результат отличается в зависимости от содержимого строки.
Если строка состоит из ASCII символов, то кодирование в utf8 и utf16 занимает примерно одно время:
> str = "w".repeat(15).repeat(1024); > start = new Date(); for(var i = 0; i < 10000; i++) { Buffer.from(str, 'utf8').toString('utf8') }; (new Date() - start) 319 > start = new Date(); for(var i = 0; i < 10000; i++) { Buffer.from(str, 'utf16le').toString('utf16le') }; (new Date() - start) 330
Но если диапазон значений шире, то utf8 замедляется в 8 раз, а utf16 ускоряется в три. В результате utf16 быстрее в 26 раз.
> str = "ш".repeat(15).repeat(1024); > start = new Date(); for(var i = 0; i < 10000; i++) { Buffer.from(str, 'utf8').toString('utf8') }; (new Date() - start) 2615 > start = new Date(); for(var i = 0; i < 10000; i++) { Buffer.from(str, 'utf16le').toString('utf16le') }; (new Date() - start) 101
Очевидно, что в V8 внутреннее представление строки меняется в зависимости от его содержимого. В общем случае utf16 предпочтительнее.
V1tol
26.04.2019 13:26+1Попробовал с UTF-16
function clearString5(str) { return Buffer.from(str, 'utf16le').toString('utf16le'); }
Получилось чуть быстрее:
711.1723950000014 // utf-8 635.6162950000726 // utf-16
Ещё попробовал библиотеку flatstr. Она показывает существенный выигрыш в скорости:
53.19643200002611 // flatstr
jordansamuil
25.04.2019 15:48-1Теперь все стало ясно и понятно. Потому что берещь некоторых прогеров на работу ( а не шаришь в этом) и он как наделает делов и все, ховайся. Искал даже сео-агенство (целый список нашел http://ktoprodvinul.ru/) чтоб с полным комплекосм. Вроде уже все ок, однако все же разбираться надо! Еще раз спасибо!
Bhudh
25.04.2019 15:51
Если известно, чтоfunction MemoryLeak() { let huge = "x".repeat(15).repeat(1024).repeat(1024); // 15МБ строка let small = huge.substr(0,15); //Маленький кусочек return small; }
huge
кэшируется, почему не сделать явную очистку?
function MemoryMaybeNotLeak() { let huge = "x".repeat(15).repeat(1024).repeat(1024); // 15МБ строка let small = huge.substr(0,15); //Маленький кусочек huge = undefined; // Удаляем ненужную более огромную строку return small; // Возвращаем маленький кусочек }
dollar Автор
25.04.2019 15:54Почему не сделать явную очистку?
Потому что это не поможет. Ссылка на большую строку останется в small.
huge = undefined;Bhudh
25.04.2019 15:57А где именно? В привязанном объекте типа [[Scope]] для функции?
dollar Автор
25.04.2019 15:58Нет, ссылка находится в самой строке small. Об этом вся статья.
Bhudh
25.04.2019 16:02Хотите сказать, что я могу написать что-то вроде
small.parent;
и получить все 15 метровhuge
в консоль?dollar Автор
25.04.2019 16:10Этого нет в спецификации JavaScript, так что странно надеяться на такое. Даже если сможете, это будет возможностью конкретной среды исполнения. Для Google Chrome я не знаю способов. Но факт остаётся фактом — в V8 у строк есть скрытая ссылка на «родителя» или нескольких «родителей».
homm
25.04.2019 16:11Нет, так написать вы не можете. То, что ссылка остается, не означает, что она доступна где-то из пользовательского кода. Точно так же недоступны ссылки на дефолтные аргументы и локальные константы функций, на переменные замыканий, на много чего еще, но они все равно есть.
Bhudh
25.04.2019 16:17-1О чём и был мой вопрос выше. «Дефолтные аргументы и локальные константы функций» и «переменные замыканий» — это скрытый объект [[Scope]], который есть у функции.
Но меня заверяют, что такого скоупа у строки нет, а ссылка вдруг где-то есть. Мол, "в самой строке". Хотя строка — это иммутабельный инстанс типа String в виде массива байтов со списочным доступом к каждому. В нём технически не может быть никакой ссылки.homm
25.04.2019 16:24Но меня заверяют, что такого скоупа у строки нет
Помилуйте, где вас в чем-то подобном заверяют?
а ссылка вдруг где-то есть. Мол, "в самой строке"
Но так и есть.
Хотя строка — это иммутабельный инстанс типа String в виде массива байтов со списочным доступом к каждому. В нём технически не может быть никакой ссылки.
Ну если вы лучше знаете, как что устроено, то может быть вы объясните как тогда работают примеры из статьи.
Bhudh
25.04.2019 16:29Помилуйте, где вас в чем-то подобном заверяют?
Здесь:dollar сегодня в 15:58
Нет
Но так и есть.
Вынужден повторить вопросА где именно?
Ну если вы лучше знаете, как что устроено
Знал бы — не спрашивал бы.
Но не знаю и спрашиваю.
Знаю только, как устроена строка по спеке.homm
25.04.2019 16:40Здесь:
Вы очень своеобразно интерпретируете свой вопрос и этот ответ. Вас заверяют, что «ссылка не хранится в привязанном объекте типа [[Scope]] для функции», но вы почему-то прочитали это как «такого скоупа у строки нет». Одно с другим логически никак не связано.
А где именно?
А что вы рассчитываете получить в ответ? Описание структуры на Си? Ну посмотрите в исходниках движка. Какая разница как, сути это не меняет: строка small содержит внутри себя ссылку на строку huge.
Знаю только, как устроена строка по спеке.
Вы путаете описание интерфейса (спека) с тем, как устроено внутри (реализация).
Bhudh
25.04.2019 16:49Вы очень своеобразно интерпретируете свой вопрос и этот ответ.
Интересно, а как Вы узнали, как я интерпретирую свой вопрос? На Хабре завелись телепаты? Видимо, один из телепатов и подумал, что под «объектом типа [[Scope]]» я разумею что-то специфицированное да и влепил мне минус за незнание спеки…
А я всего лишь хотел узнать, привязан ли к строке хоть какой-нибудь объект с внутренними ссылками, пусть даже где-то в недрах V8. ПомимоString.prototype
.
Кстати, надо заметить, что хотя в статье речь о строках (и это вроде как подтверждается скриншотами консоли), во всех примерах эти строки создаются в функциях, что даёт основание утверждать, что в [[Scope]] этих функцийhuge
также может сохраняться.khim
25.04.2019 20:59+3Минусов вам влепили за обсолютное незнание предмета. Ну всё равно как если бы учитель русского языка средней школы начал обсуждать JavaScript и заводить баги на тему того, что
'ё' < 'ж'
— этоfalse
.
Точно так же как учитель русского языка не может себе представить, что буквы могут быть отсортированы как-то иначе, чем в русском алфивите — вы не можете представить себе, что в природе могут существовать сущности, не отражённые в JavaScript.
Обсуждать что-либо в обоих случаях бесполезно. И механизм голосования на Хабре, собственно, и предназначен для того, чтобы от подобным горе-спорщиков избавиться.Bhudh
26.04.2019 02:34-12 khim
Не знаю, у кого тут "обсолютное" незнание, я уже 10 лет на JS пишу.
И прекрасно знаю, что в JS бывают сущности, не отображённые в спеке, хотя быdocument
иwindow
в браузере или типfxobj
в PDF.
--------------------
2 vassabi
По спецификации String всё-таки тип, несахарных классов в JS нет.
А в реализации String это функция-конструктор (как и любая другая функция, объявляемая с сахарным ключевым словомclass
).
И это не объекты String ведут себя как строки, а строки — представители простого типа — ведут себя как объекты-обёртки, например, при вызове методов.
массив байтов — это массив байтов (и его размер может быть больше, чем размер строки в нем).
Зависит от того, в каких единицах считать строку. Если её считать в байтах, размер будет одинаковый. Если в символах Уникода, то, понятно, число уменьшится. А уж если кто-то решит считать строку в тех эмодзи, что отображаются на странице в браузере, а массив байтов: в тех байтах, что заключают в себе всю строку в имплементации (30 MiB согласно статье)…
--------------------
2 all
Я с самого начала спрашивал про реализацию (где и как), а не про спецификацию.
Но минусовать-то, конечно, проще, чем объяснять. И да, я знаю про Великое Правило «помянул минус — получи минус». Но мне на него как-то с Пизанской башни.
Всем читающим и пишущим желаю писать на хорошем и правильном русском языке и помнить, что ружьё бывает разряжённое, а воздух разрежённым, а не наоборот.mayorovp
26.04.2019 06:41Как раз
window
иdocument
в спеке есть, только смотреть надо не спеку на javascript, а спеку на HTML.
Я с самого начала спрашивал про реализацию (где и как), а не про спецификацию.
Так вам с самого начала и объяснили. А дальше вы встали в позицию "не верю, так не бывает"...
Bhudh
26.04.2019 17:51-1Я ответил, что "так не бывает" именно по спецификации, в которой строка (которая не объект) есть «ordered sequence of zero or more 16-bit unsigned integer values (“elements”) up to a maximum length of 253-1 elements». В это определение ссылки на родителей "не помещаются".
Почему и спросил, где они и как реализованы.homm
26.04.2019 18:18+1Еще раз, вы путаете спецификацию (интерфейс) и реализацию. Спецификация описывает, как должен себя вести тот или иной объект, а не как он должен быть устроен. Указанное требование «ordered sequence of zero or more …» никак не нарушается данной реализацией.
homm
26.04.2019 18:22+1Я ответил, что "так не бывает"
Вы живете в каком-то выдуманном мире. Вам объясняют, как это устроено, вы отвечаете «так не бывает». Ваше дело, конечно.
Bhudh
26.04.2019 18:47-3А «так не бывает» это и не мои слова, я сам их зацитировал.
И просил я именно объяснить, «как устроено» и каким образом помещаются в строку ссылки минуя «ordered sequence of elements».
Как вся фигня, к которой нет доступа в функциях, помещается в функцию, понятно: скрытый объект.
Как вся фигня, к которой нет доступа в строках, помещается в строку:? Скрытое что?homm
27.04.2019 00:06+1Как вся фигня, к которой нет доступа в строках, помещается в строку?
Ну подождите, а как в строку помещается вся фигня, к которой есть доступ в строках? Вы это понимаете? Там не скрытое что?
Как вся фигня, к которой нет доступа в функциях, помещается в функцию, понятно: скрытый объект.
Удивительно, что вам это понятно. Там же объект! А какой у него класс? Какие методы? Время жизни? Ваше объяснение, которе вас удовлетворяет для функций, на самом деле не объясняет вообще ничего, о том, как это устроено. Но почему-то точно такое же объяснение для строк (там скрытая ссылка на родительский объект) вас вдруг ставит в тупик.
Bhudh
27.04.2019 07:59Ну подождите, а как в строку помещается вся фигня, к которой есть доступ в строках? Вы это понимаете? Там не скрытое что?
Не скрытое то, что по спецификации для конечного пользователя должно выглядеть упорядоченной последовательностью целых беззнаковых 16-битных чисел и методом доступа[]
. На уровне языка реализации и интерпретации имеем класс для выделения, чтения, записи и освобождения памяти под эти последовательности. Достаточно и необходимо.
точно такое же объяснение для строк (там скрытая ссылка на родительский объект)
О,[[Scope]]
уже стал родительским объектом? Или в чём объяснение "точно такое же"? Я же это первым делом спросил: есть там что-то типа [[Scope]] или нет?khim
27.04.2019 16:40На уровне языка реализации и интерпретации имеем класс для выделения, чтения, записи и освобождения памяти под эти последовательности. Достаточно и необходимо.
Достаточно, но не необходимо. Более того: в V8 строки, поддерживая иллюзию того, что внутри них просто массив (иначе это всё не соответствовало бы спецификацию) имеют сложную структуру, особенности которой иногда прорываются наружу.
Я же это первым делом спросил: есть там что-то типа [[Scope]] или нет?
Осталось только понять что вы подразумеваете под «чем-то типа [[Scope]]». Ничего явно видимого в Developer Tools нету ибо, в отличие от [[Scope]], строка может менять своё строение «на лету» в то время как вы на неё смотрите, так что смотреть на её внутреннее устройство не остановив JavaScript-мир проблематично. Но, разумеется, внутри V8-строки есть богатый внутренний мир… со своими особенностями.
Собственно вся статья — об этих особенностях.
dollar Автор
25.04.2019 16:24+1Вы рассуждаете с точки зрения логики языка JavaScript. Но эта особенность — не часть языка, а часть конкретной системы управления памятью. Эта система вам ничего не «должна». В частности, у вас нет гарантий того, какой объем памяти будет ассоциирован со строкой, и как там вообще всё будет оптимизировано.
vassabi
25.04.2019 21:18+2строка — это строка,
массив байтов — это массив байтов (и его размер может быть больше, чем размер строки в нем).
а String — это класс, объекты которого ведут себя, как будто они строки. Как они устроены внутри на самом деле, какие внутри них ссылки, байты и массивы — это глубокие детали реализации.
ehots
25.04.2019 16:18-1Жду я значит уже минут 10 в мозилле, вклада увеличила аппетит с 375 Мб до ~600.
Нагрузка на ОЗУ конечно выросла, но далеко до краша.khim
25.04.2019 21:02-1Ещё один чукча-писатель? Статью читать не пробовали?
Конкретно вот это:Это особенность движка V8, которая позволяет ускорить работу со строками в ущерб, естественно, памяти. То есть это касается Google Chrome и прочих хромиум-браузеров, а также Node.js. Этого уже достаточно, чтобы отнестись серьёзно к этому явлению.
Нафига медитировать на мозиллой, где этой проблемы нет (там свои, другие, проблемы есть)?
ExplosiveZ
25.04.2019 18:54Золотые времена
IE6Chrome
Писать workaround'ы для кривого runtime, тут хотя бы исходники есть.sumanai
27.04.2019 00:52+1Притом полностью забили на браузеры на альтернативных движках. Во времена IE6 хотя бы иногда заботились о других браузерах ((
shaukote
27.04.2019 22:08FGJ, все вышеперечисленные хаки, предназначенные для компенсации проблемы в V8, навроде str.split('').join(''), никак не ломают поведение в других браузерах/движках.
rpiontik
25.04.2019 20:26На знал о такой проблеме, но догадывался. Потому, что в хроме богато ресурсов сайты жрут. У себя на проектах, честно сказать не замечал. Возможно, дело в том, что используется VUE. И там это как-то решается в рективном двигле. Но нужно потратить время и перепроверить это.
Вопрос который у меня возник — я не видел за последнее время ни одного сайта, который сожрал бы память до краша. Я также не видел сайта, который бы жестко тормозил. Да, раньше дело было. Сам с таким сталкивался. Сейчас все получшело. Но… я сталкиваюсь с тормозами когда открыт devtool.
Вопрос №1 — а не может ли такое поведение быть специфичным для режима отладки? Когда по каким-то причинам двигло пытается сохранить побольше инфы для, ну например, профайлера. Буду проверять. Интересно…
Вопрос №2 — не является ли это осмысленным перерасходом памяти для оптимизации, но при этом память начнет высвобождаться в свободные процессорные тики? Т.е. сначала работаем на производительность, затем на оптимизацию. Нужно тоже проверять…dollar Автор
25.04.2019 21:25+1Devtool кэширует тела ответов ajax, из-за чего память процесса (вкладки) в Chrome, естественно, растёт, породой до огромных размеров. Но чтобы анализировать проблемы памяти именно в JS, нужно смотреть на память JS, она называется «JS Heap» или «Память JavaScript».
В Devtool во вкладке памяти предлагается анализировать как раз память JS, так что проблемы нет.
В диспетчере задач Chrome нужно отдельно включить столбик с памятью JS, для этого кликните правой кнопкой по заголовкам и выберите соответствующий пункт:
Скриншот3axap4eHko
25.04.2019 22:13dollar с учетом того что строка итерируется, то нет нужды ее разбивать через
split
, можно просто сделать[...string].join('')
flapenguin
25.04.2019 23:49+2Если посмотреть в сорцы то все еще хуже:
В
Factory::NewProperSubString
где создается подстрока (SlicedString
) исходную строку плющат в последовательную строку.
А это значит, что память течет еще сильнее при использовании
ConsString
'ов:
x = new (function TrackMe() {})() x.a = 'a'.repeat(100); x.b = 'b'.repeat(100); x.concat = x.a.repeat(1000) + x.b.repeat(1000); x.substr = (x.a.repeat(1000) + x.b.repeat(1000)).substring(80, 120);
На скриншоте ниже видно, что
a
иb
— маленькие няшныеConsString
(они же известны как string rope'ы). Строка из 1000 повторений каждой из них — тоже.
Но для подстроки из 40 символов такую же строку из 1000 повторений сплющило, и теперь в памяти лежит 200 кубов ради 40 символов.
flapenguin
26.04.2019 00:25Еще интересно, что самый быстрый способ
(' ' + str).slice(1)
— самый быстрый ровно по той же причине почему происходит эта утечка.NewProperSubString
насильно плющит строку. Итоговый перерасход памяти — 1 символ.
Остальные варианты создают куда большие промежуточные объекты.
tbl
26.04.2019 10:30+1Интересно, что в Java уже сталкивались с такими утечками (особенно в мире Java EE, когда выгрузка приложения из контейнера неожиданно не освобождала часть памяти), и, начиная с версии 1.7.0_06, провели деоптимизацию: String.substring(int, int) не шарит нижележащий массив символов с новой строкой, а создает новый массив для новой подстроки. Но если тебе все-таки очень надо не создавать копию подстроки (например, исходная строка очень длинная, а подстрока включает большую ее часть и нужна лишь на короткое время), и ты понимаешь и принимаешь все возможные риски подобной оптимизации, то предусмотрели обходной маневр: CharBuffer.wrap(str).subSequence(int, int).
По-моему, в JavaScript тоже могли бы пойти по подобному пути, причем, это не требует особых приседаний со стороны движка.transcengopher
26.04.2019 12:21+1Альтернативой может стать усложнение GC по отношению к строкам — тогда можно сохранить внутреннее представление в виде ссылки на родительский элемент, но при этом ссылка на родителя не должна препятствовать его сборке GC — и при сборке демон должен будет прозрачно заменить ссылку+оффсеты на функционально и логически эквивалентный «настоящий» массив, полученный в результате слайса.
В Java такое сделать проблематично из-за строгости реализации — при таких операциях GC должен иметь эксклюзивный доступ к объекту, чтобы другие потоки не могли увидеть её в неконсистентном состоянии (а они могут, т.к. массив и оффсеты это три операции записи, причём в несинхронизированые поля, которые ещё и синхронизировать нельзя из соображений производительности). Это подразумевает чуть ли не подмену значения по всем ссылкам, которая должна быть полностью незаметна для всего пользовательского кода, и прочие весёлости, вроде следующей из такого требования двойной индирекции в самих указателях. В общем, проще деоптимизировать и вернуться к проблеме если она станет совсем уж невыносимой.
Рискну предположить, что в JS подобные трюки проворачивать всё же проще, так что вдруг у V8 получится.tbl
26.04.2019 12:38Решили, наверно, не заморачиваться, пока не найдется PoC эксплоита, кладущий целиком ноду.жс, как это было с Java EE контейнерами.
tbl
26.04.2019 12:43Кстати, в java serial gc и parallel gc для old generation умеют выполнять дефрагментацию хипа, останавливая весь мир, так что встроить туда компактификацию строк с заменой нескольких указателей — не очень космическая проблема.
transcengopher
26.04.2019 15:59Уметь-то умеют, но инфраструктура в целом сейчас движется к уменьшению пауз GC — так что надеяться на очистку внутри STW решение не очень хорошее, потому что STW в идеале наступать не должен никогда.
Я вроде бы где-то читал, что работа над строками по-прежнему идёт. Вот в 9 версии, к примеру, сделали компактификацию в смысле перекодирования в LATIN-1 если в строке находятся только совместимые символы — это уже должно уменьшить потребление по меньшей мере процентов на 20, иногда на 40. Следующим этапом, вроде бы, должна стать замена эквивалентныхchar[]
в разных строках.
Может быть, когда-нибудь и доживём до триумфального возвращения шаренных массивов. Но однозначно не в виде STW-обработки, и не в ближайшие два-три года. Пусть сначала Coin и Loom закончат.
unC0Rr
26.04.2019 11:34Технически это не memory leak, а space leak. Отличается тем, что память всё же можно освободить, удалив строку.
keenondrums
Прикольно. Но будем честны, большинство веб приложений на NodeJS стараются делать stateless. Т.е. в какой-то момент ссылки не будет и gc все почистит.
Однако, буду иметь ввиду, если вдруг появится стейт. Спасибо!
Багу на v8 не заводили? Мне кажется, по хорошему, gc должен со временем реально копировать только часть строки и прочищать родительские ссылки.
dagen
bugs.chromium.org/p/v8/issues/detail?id=2869 (в 2013 открыта, в 2019 закрыта без каких-либо изменений в коде V8). Там же есть ссылки на другие похожие посты.
А вообще у mraleph есть отличная статья с картиночками, написанная с другой целью, но организацию строк и ссылки на исходные строки немного освещающая: mrale.ph/blog/2016/11/23/making-less-dart-faster.html (про утечки искать в конце первой части по строке «it leads to surprising memory leaks»).
enabokov
Status: Assigned (Open)
dagen
И верно, перепутал со связанной нодовской issue.
nrgian
Да я бы не сказал.
При сравнительно медленном движке (не Go, не Java) оставлять код работать как можно дольше, а не очищать все на каждый запрос — это нормальная возможность ускорить ваш сервис.
keenondrums
Бенчмарки V8 и Node.js в целом поспорят с утверждениями о медленном движке.
OlegTar
Это не бага
Opaspap
А что такое бага? Если интерпретатор роняет хост, неожиданным образом это бага или нет? В среке такого нет, что надо чистить строки, наоборот, строка примитив и, по идее, это не забота програмиста, что конкретные реализации языка ведут себя подобным образом. Поведение iframe в ios тоже не бага, но тоже удивительно, когда блок, плевав на явно указанные размеры, увеличивает сам себя.