Нужно ли чистить строки в JavaScript? +184



Что? Строки могут быть «грязными»?

Да, могут.

//.....Какой-то код
console.log(typeof str); // string
console.log(str.length); // 15
console.log(str); // zzzzzzzzzzzzzzz 

Вы думаете, в этом примере строка занимает 30 байт?

А вот и нет! Она занимает 30 мегабайт!

Дьявол кроется в деталях. В данном примере — это «какой-то код». Очевидно, какой-то код что-то делает, что строка занимает много памяти. И вроде бы это вас не касается, но лишь до тех пор, пока это не ваш собственный код. Возможно, в вашем коде уже сейчас много мест, где строки занимают в десятки раз больше, чем в них содержится.

Предисловие


Сразу хочу заметить, что этот баг фича давно известна. Я не открыл ничего нового. Это особенность движка V8, которая позволяет ускорить работу со строками в ущерб, естественно, памяти. То есть это касается Google Chrome и прочих хромиум-браузеров, а также Node.js. Этого уже достаточно, чтобы отнестись серьёзно к этому явлению.

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));


Примерное время работы в Chrome 73
console.log(Test(test_arr,clearString)); //700мс
console.log(Test(test_arr,clearString2)); //300мс
console.log(Test(test_arr,clearString3)); //280мс


UPD:
Замеры в Opera и Firefox от @WanSpi
//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:
Замеры Node.js от @V1tol
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):

  1. keenondrums
    /#20072588 / +2

    Прикольно. Но будем честны, большинство веб приложений на NodeJS стараются делать stateless. Т.е. в какой-то момент ссылки не будет и gc все почистит.
    Однако, буду иметь ввиду, если вдруг появится стейт. Спасибо!
    Багу на v8 не заводили? Мне кажется, по хорошему, gc должен со временем реально копировать только часть строки и прочищать родительские ссылки.

    • dagen
      /#20072598

      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
        /#20077400

        Status: Assigned (Open)

        • dagen
          /#20077556

          И верно, перепутал со связанной нодовской issue.

    • nrgian
      /#20076068 / +1

      Но будем честны, большинство веб приложений на NodeJS стараются делать stateless.

      Да я бы не сказал.
      При сравнительно медленном движке (не Go, не Java) оставлять код работать как можно дольше, а не очищать все на каждый запрос — это нормальная возможность ускорить ваш сервис.

      • keenondrums
        /#20076628 / +1

        Бенчмарки V8 и Node.js в целом поспорят с утверждениями о медленном движке.

    • OlegTar
      /#20077216

      Это не бага

      • Opaspap
        /#20083536

        А что такое бага? Если интерпретатор роняет хост, неожиданным образом это бага или нет? В среке такого нет, что надо чистить строки, наоборот, строка примитив и, по идее, это не забота програмиста, что конкретные реализации языка ведут себя подобным образом. Поведение iframe в ios тоже не бага, но тоже удивительно, когда блок, плевав на явно указанные размеры, увеличивает сам себя.

  2. pallada92
    /#20072610 / +1

    Спасибо за статью, не знал об этом. Из статьи подумал, что у строки может быть только один родитель, тогда конкатенация с другой строкой должна решить проблему. Но, оказывается, все ещё сложнее: внутри V8 строки представляют собой ориентированый граф из узлов, у каждого из которых есть упорядоченное множество родителей. Тогда конкатенация делается очень быстро добавлением общего корня к двум узлам. Но иногда это может ухудшить производительность и есть библиотека github.com/davidmarkclements/flatstr, которая делает этот граф одноуровневым.

    К счастью, во фронтэнд разработке редок сценарий, когда откуда-то приходят большие строки, из которых накапливаются кусочки, но в любом случае знать об этом крайне важно. Удивлён, что не встречал это в статьях типа «10 вещей о JS, которые вы не знали»

    • Koneru
      /#20073616

      Работа с картинками в base64, сталкивался с такими строками(3~5МБ) буквально вчера, только там не было необходимости изменять их.

    • dollar
      /#20074094

      Столкнулся с этим при разработке браузерного расширения. Все условия соблюдены: оно постоянно висит в памяти, делает периодически ajax, и складывает кусочки в объект-кэш (чтобы не повторять ajax по одним и тем же адресам).

  3. MSC6502
    /#20072624 / -15

    Ну а что можно требовать от средства разработки, которое забацали на коленке без ТЗ, чёткого понимания целей и задач, возможностей расширения в будущем и кучи прочих тонкостей, о которых разработчики средств для веб-программирования даже и не подозревают. В итоге имеем то, что имеем, а за неимением лучших средств выдаем эту поделку уровня третьего курса провинциального вуза за better of best, надуваем щёки, проводим конференции, улыбаемся и машем, проклиная всё, когда завтра сдавать проект, а тут косячки, там косяки и вот тут громадный косячище, который за оставшиеся 8 часов никак не поправить. Вот.

    • avost
      /#20072894 / +2

      V8 написали на коленке без тз студенты третьего курса? Риали? Откуда дровишки?

      • tangro
        /#20079248

        Не студенты, конечно, но вот то, что без понимания целей — так это факт. Никто там в момент создания проекта не задумывался, что когда-то будет на нём нода, куча всяких поделок на Хромиуме и т.д. Это прямо вот чувствуется прямо вот даже по публичной апишке V8.

    • XenonDev
      /#20073070

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

    • Godebug
      /#20074160

      Ого, оптимизированного аглоритмоемкого монстра назвали поделием на коленке. Если что, v8 не фронт-энд разработчики писали :)

      • yarkov
        /#20074392 / +2

        Мне кажется, что MSC6502 имел ввиду не V8, а то, что JavaScript писался спонтанно за 2 недели.
        Но в любом случае я не разделяю его мнения, просто уточнил.

        • dagen
          /#20074588

          Не думаю, что он что-то имел ввиду: он просто хейтер, судя по его комментариям.

        • justme
          /#20074952

          Явно речь не о V8 а о языке. Но есть доля правды — то как был разработан и быстро внедрен язык сильно повлияло и ограничило разработчиков движков. Чем гибче и «проще» (для новых разработчиков) язык — тем сложнее (а значит и потенциально глючнее) его движки. За те 2 недели написания языка был заложен фундамент (или ТЗ, смотря как посмотреть) на котором уже приходится строить все движки, фреймворки, браузеры и т.д.

          • Godebug
            /#20075156

            Про JS надо помнить эпоху когда он создавался — тогда никто, даже в смелых мечтах (или унылых кошмарах) не мог бы представить то в чем мы сейчас варимся :)

            п.с. К слову — в то время тоже было много споров/докладов/холиваров о MVC :)

            • justme
              /#20076990 / +2

              Бесспорно. Валить все шишки на JavaScript определенно нельзя. Как по мне — это серия неудачных решений, «коротких путей» и недальновидных дизайнов. Взяли по-быстрому накатали язык. Потом он быстро пошел в массы и вскоре стал де-факто стандартом в web. Потом накатали движков и завоевали рынок одним браузером (а стало быть и одним главным движком JS, который имеет приоритеты в развитии сильно совпадающими с интересами одной большой коммерческой компании). А потом еще и додумались все это чудо зафигачить на сервера в качестве удобного и быстрого языка для прототипирования и небольших стартапов. Ну а дальше и это детище начало разивиаться, обретать популярность и оставаться на продакшене даже после того как проект переростает этап стартапа.
              Вот и приплыли туда где мы есть сейчас, к серверам которые не умеют даже строки толком подчищать… и это я не говорю уже о бессмысленности побитовых операций а также о том что любой JS код хранит в себе еще и СТРОКУ СО ВСЕМ КОДОМ ВКЛЮЧАЯ КОММЕНТАРИИ И ПЕРЕВОДЫ СТРОК!!! (это если код чистый, не, прости господи, скомпилированный)

              P.S. вероятно мой коммент заминусуют, но уж сильно меня печалит то к чему приводит вся история с JS и как этот достаточно нишевый и несколько спорный язык программирования пихается во все возможные щели (уже даже на микропроцессорах есть прошивки где можно на JS кодить!!!)

              • Keyten
                /#20077816

                А какие есть неудачные решения есть в JavaScript, которые однозначно всем сильно мешают жить, и все однозначно согласны, что они плохие и лучше бы их не было?

                Например, typeof null на практике сильно не мешает, а прототипы или динамическая типизация — вы не сможете сказать, что все разработчики однозначно против.

                • homm
                  /#20077904

                  Глобальность любых переменных по умолчанию?

                  • Keyten
                    /#20077940

                    Когда забываешь слово var? Кажется, за всё, вообще всё время это ни разу ничего у меня не ломало :). Более того, такие штуки видно сразу, а если в вашем коде видно их не сразу, например, двадцать вложенных функций каждая со своими локальными переменными, то у вас в любом случае проблемы.
                    Нет, я согласен, что есть море неудачных решений, которые при определённых обстоятельствах делают всё плохо, но кажется, что это вот примерно настолько же плохо, как typeof null.

                    • homm
                      /#20077948

                      То есть запишем в удачное решение?

                      • Keyten
                        /#20077956

                        Запишем в не очень мешающее жить решение.
                        Я спросил, что (очень мешает жить) && (все согласны, что это плохо).

                        • justme
                          /#20079924

                          Любое решение можно оправдать и найти сторонников/защитников

                          Если вы изучали много разных языков программирования то могли заметить что многие современные языки (да и старые тоже) разрабатываются с точки зрения начального удобства и минимизации возможных ошибок. Так, например, в Python правильное выравнивание кода является обязательным и код будет выдавать ошибку в обратном случае. В Go табуляция принята как единственная система чтобы закрыть холи-вары (хоть я и приверженец пробелов). Также современные языки отказываются от таких принципов как перегрузка операторов, инструкция GOTO: и #define а также от множественного наследования. Все это помогает минимизировать количество потенциальных ошибок допускаемых программистами нового языка.
                          Возможность в non-strict режиме создать переменную которая будет публичной, невозможность НОРМАЛЬНЫМИ способами объявить приватную функцию или переменную, прототипное наследование, хранение всего кода функции в виде строки (включая переводы строк и даже комментарии) в переменной самой функции а также недостаточная проработанность языка возлагающая ответственность за оптимизацию и реализацию большинства моментов на разработчиков JS-машины (в следствии чего и произошла та проблема что описана в статье), да и вообще возможность сделать в JS практически что пожелаешь и выстрелить себе в ногу любым самым изощренным методом — это все не направлено на минимизацию потенциальных ошибок у программистов, даже напротив

                • justme
                  /#20077912

                  >прототипы или динамическая типизация — вы не сможете сказать, что все разработчики однозначно против

                  ну с таким подходом я вообще мало что смогу сказать) всегда найдутся сторонники той или иной точки зрения. динамическая против статической, ООП против функционального, компиляция против интерпретации…

                  Прошу заметить что то что я пишу это лишь мое мнение (которое наверняка разделит часть IT-сообщества) и оно сформировано во многом оттого что я приверженец «старой школы» (т.е. С/С++ и т.д.). Однако по моему мнению важные проблемы кроются именно в том что язык интерпретируемый и нетипизированный. Это бесспорно помогает молодым стартапам и вообще любым прототипам, PoC, MVP, MSP и т.д., но мне обидно видеть что это же используется и в финальной разработке. Когда Slack с их то масштабами грузит комп в десятки раз больше любого видео-плеера который производит потоковое декодирование, или когда очередная экранная клавиатура под Android/iOS весит больше чем весь Windows 95 — это, как по мне, просто не правильно
                  Тут не только JS виноват, конечно же. Но просто JS это для меня как яркий флаг символизирующий все движение в целом

                  А сторонники и противники появятся всегда. Тот же JS дал большой толчок в популяризации программирования в целом, т.к. начальный порог вхождения не велик, а учиться ему можно даже дольше чем тому же С (а значит и «рости» по должности и требовать ЗП повыше можно еще долго). К тому же я уже говорил что для определенных ниш этот язык и даже тот же node.js да и Electron вполне себе подходят и очень хорошо справляются со своей целью. Проблема не в том что эти решения есть а в том как и где их используют

                  • Keyten
                    /#20078042

                    Классика. Говорить, что язык объективно плохой, за то, что некоторые пишут на нём плохой код :)

                    Это вообще ооооочень странно — выдавать медлительность Gmail / Slack за аргумент, почему js плохой. Я вот работал с ANSYS, такой очень распространённый инженерный софт. Он умеет подвисать на минуту при right-click (нет, там точно не нужны сложные расчёты чтобы открыть контекстное меню с тремя пунктами) и тому подобных действиях. А ведь он написан, кажется, на C++. На сильно статически типизированном! И даже без Qt и т.п. штук.

                    Почему Slack или Gmail столько кушает — я не знаю. Правда. Всё, что я когда-либо писал, в принципе не могло столько есть. Я серьёзно не представляю, как можно написать такой простой интерфейс очень тяжёлым.

                    Вот это всё, надеюсь, проиллюстрирует то, что я хочу сказать.

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

                    • khim
                      /#20078182

                      Классика. Говорить, что язык объективно плохой, за то, что некоторые пишут на нём плохой код :)
                      Отрицание конечных результатов в оценке языка — такая же точно классика. Хотя ей обычно больше хаскелисты страдают.

                      На самом деле процент качества результата, получаемого на том или ином языке — это неплохая оценка. Она, возможно, сферически-вакуумно не очень объективна, но зато вполне практически применима: если подавляющее большинство проектов, написанном на языке A требуют чрезмерного размера ресурсов и медленно работают — то, скорее всего, и то, что вы получите — будет устроено так же.

                      Я серьёзно не представляю, как можно написать такой простой интерфейс очень тяжёлым.
                      Тем не менее на средние JS-проекты требуют сильно больше ресурсов, чем средние C++-проекты. И даже тот же ANSYS — то, что он тормозит, мы уже поняли, а вот сколько памяти он требует? Тот же гигабайт памяти, как и открытая у меня сейчас вкладка с GMail'ом?

                      Практика показывает, что как раз количество памяти потребляемое программой — гораздо сильнее зависит от языка, чем скорость работы. И вот тут у JS — всё плохо: написанные с его помощью проекты жрут либо много памяти, либо очень много памяти. При этом LUA какая-нибудь — таких ресурсов не требует. Несмотря на свою динамическую типизированность и сборщик мусора… Да — она гораздо медленнее, но как раз на суперскорость она и не претендует…

                      • Keyten
                        /#20081666

                        Допустим, но как вы докажете, что большинство проектов на js едят много памяти и тормозят? :) По моим данным, всё совершенно наоборот — на js пишут высоконагруженные штуки вроде сервера сообщений вк, и вообще js наверное самый оптимизированный из динамически типизированных интерпретируемых языков.
                        И более того, если у вас кушает много памяти что-то в браузере или в electron — почему вы уверены, что проблема в языке, а не в том самом Chrome, который вообще-то известен своей прожорливостью? К слову, на чём он там написан, на плюсах? Давайте ругать плюсы за прожорливость хрома и гмейла в частности).

                        ANSYS, кажется, и памяти ел больше, чем хром в брачный период. Но я точно не скажу, это было давно и в страшном сне.

                        Про Lua совершенно очевидно напрашивается вывод: это трейдофф память -> скорость. Разработчики V8, вероятно, решили, что работать быстро важнее, чем есть мало памяти. Это мои домыслы, но тем не менее. И высоконагруженные сервера пишут не на требующем малых ресурсов Lua, а, как ни странно, на js.

                        • khim
                          /#20082306

                          вообще js наверное самый оптимизированный из динамически типизированных интерпретируемых языков
                          Возмножно. Но все эти оптимизации всё равно не позволяют достичь скорость написанной на статически типизированном язуке — однако требуют кучу дополнительных ресурсов.

                          К слову, на чём он там написан, на плюсах?
                          Частично.

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

                          И высоконагруженные сервера пишут не на требующем малых ресурсов Lua, а, как ни странно, на js.
                          Высоконагруженные сервера (сотни тысяч и миллионы QPS) до сих пор пишут на C++. На js пишут сервера, при создании которых хочется использовать армию дешёвых JS-программистов — а потом начинаются приседания, когда оказывается, что железо-то всё-таки небесплатное…

                    • justme
                      /#20079818 / +1

                      Мне кажется вы не внимательно прочли что я написал. Про Gmail я не писал ни слова. Про Slack я четко написал что «тут не только JS виноват, конечно же». Про причины тормозов Slack (а точнее того сколько всего тянет за собой Electron и тому как он работает с процессами браузера на каждую команду писали на хабре уже много раз)
                      Проблема которую я описал не столько в языке сколько в том как и где его применяют. Любой язык имеет свою нишу, универсальных и идеальных не бывает. Кроме С++, конечно же :D (если что это шутка, не вырывайте из контекста, С++, к сожалению, и своих проблем хватает и применимость ограниченная)

                      «Некоторые пишут плохой код» — это, к сожалению, крайне частая проблема программистов JS. Отчасти дело кроется в «низком пороге входа», а отчасти в том, что, как мне кажется, чтобы написать хороший код на JS нужно потратить на его изучение и набивание шишек намнооого больше времени и усилий чем на низкоуровневых языках. Вход проще, а обучение дольше. Ну и желания доходить до конца обучения мало у кого есть, ведь ЗП у JS программистов сейчас высокие и обоснованны больше спросом а не качеством предложения, потому ЧСВ у новых программистов повышается и они не считают что им что-то еще надо. Подучить больше фреймворков, больше либ, больше подходов… и дописать в резюме строчку Senior спустя год-два программирования.
                      Я лично провел несколько сотен собеседований за свою жизнь и могу точно сказать что среди тех кого я собеседовал раздутое чаще всего именно у JS разработчиков. Причины я описал выше. Не считаю что это их вина или что у них плохой потенциал (честно, никого не хочу задеть или обидеть), просто пытаюсь показать как низкий порог вхождения влияет на подход и виденье индустрии в целом

                      Возвращаясь к JS — извините, но я твердо уверен что JS не подходит для высоконагруженных серверов с миллионами запросов в секунду и bigdata на борту. Да, построить такую систему можно (путем горизонтального скейлинга и больших серверных затрат), но зачем?
                      А именно так часто и получается когда продукт и нагрузка растет постепенно и момента когда «есть время чтобы все переписать» так и не наступает

                      • khim
                        /#20082326

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

                        чтобы написать хороший код на JS нужно потратить на его изучение и набивание шишек намнооого больше времени и усилий чем на низкоуровневых языках
                        Не совсем так. На JS просто можно написать плохой код — и он вам это «простит». C++ гораздо более жесток: если вы не можете внятно описать ваш дизайн, то добиться того, чтобы ваша программа не падала при малейшем шевелении мышкой — будет очень сложно.

                        Соотвественно ваша программа будет либо хорошей, либо не доживёт до стадии релиза. Такое «программирование по бразильской системе».

                        • justme
                          /#20082774

                          На самом деле как раз эта «ограниченная применимость» и приводит к написанию качественного кода.


                          Не могу с вами согласиться. У JS есть своя ниша, но это не мешает всем пропихивать ее где только можно и нельзя. Качественность кода достигается качеством программистов, и вовсе не применимостью

                          Не совсем так. На JS просто можно написать плохой код — и он вам это «простит». C++ гораздо более жесток: если вы не можете внятно описать ваш дизайн, то добиться того, чтобы ваша программа не падала при малейшем шевелении мышкой — будет очень сложно.

                          Эм… с чего вы взяли что на С++ нельзя написать плохой но рабочий код? Еще и как можно, и полно такого. А JS «простит» весьма спорно. То что он не упадет а просто закончит выполнять функцию и напишет сообщение об ошибке в консоль (которое потом никто не прочтет а просто забьет) — так это непредсказуемое выполнение вместо полного падения. Что предпочтительнее — тема весьма спорная. Но называть это «простит» — я бы точно не стал

                          ИМХО, но написать ХОРОШИЙ код на JS куда сложнее чем на С++. Любой код, или такой который, как вы выразились, будет «прощать» — конечно же проще на JS, но это не хороший

                          • khim
                            /#20082856

                            А JS «простит» весьма спорно. То что он не упадет а просто закончит выполнять функцию и напишет сообщение об ошибке в консоль (которое потом никто не прочтет а просто забьет) — так это непредсказуемое выполнение вместо полного падения.
                            Очень много вещей, который в C++ просто не пропустит компилятор в JS породят… нечто. Не всего понятно, что, то функция не упадёт.

                            Например {} - [] — почему это равно -0? Да, ответ можно прочитать в спецификации языка, но практического смысла тут никакого — и в C++ программа, содержащая подобное просто не скомпилируется.

                            ИМХО, но написать ХОРОШИЙ код на JS куда сложнее чем на С++.
                            Конечно. Чем больше «странного» и «дурацкого» кода является валидным и как-то работает и что-то таки делает — тем сложнее попасть в узкое подмножество «хорошего» кода…

                    • justme
                      /#20079980 / +1

                      много процентов разработчиков

                      как по мне количество тут не столь важно, как качество
                      можете побить меня палками, но я не считаю эффективным вести обсуждения с девелоперами которые писали всю жизнь на одном языке программирования (или только на одном типе языков) о том плох или хорош JS. И опять же — я в принципе не считаю такое обсуждение логичным, т.к. у JS есть своя ниша которую он успешно закрывает

                      Чтобы вести эффективную и интересную дискуссию нужно чтобы человек хоть пару-тройку лет писал на строго типизированном языке (и компилируемом) и хоть еще пару-тройку на нетипизированном. А лучше чтоб в запасе было языка 3-5 на которых он и писал и глубоко изучал. В противном случае процент эффективной беседы будет минимальным, а все больше будет вкусовщины и обкидывания какашками

                      За себя могу сказать что я несколько лет писал на Java, еще несколько — Objective-C, пару лет на JS и на ActionScript
                      По мелочи на asm, C++, Perl, Python, Go, C#, bash
                      Также уже много лет варюсь между тех миром и миром бизнеса, потому могу сказать что именно бизнес двигает IT-рынок в сторону таких решений как node.js. Программисты все меньше влияют на развитие IT

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

                      • transcengopher
                        /#20081134

                        Программисты все меньше влияют на развитие IT

                        И это печально — неспециалист не может сделать ничего хорошего в узкоспециальной области в долгосрочной перспективе (в краткосрочной — может, но обычно случайно).

                        • justme
                          /#20081352

                          И да и нет. Мне, как человеку все же больше техническому чем бизнес, это весьма печально и я полностью согласен что бизнес-люди не могут строить долгосрочные эффективные планы на развитие технологий. С другой стороны (и это уже во мне говорит моя бизнес-сторона) — технологии это лишь инструменты, а значит должны подстраиваться под тех кто их использует и под те задачи которые перед ними ставят. Без бизнеса развитие технологий было бы крайне медленным и нацеленным на гос.сектор (а тогда пути развития технологии могли бы быть не менее прискорбными, чего только запрос «выдать ключи» от ассиметричных алгоритмов шифрования стоят)
                          Как и все в мире тут есть несколько сил движущих в разные стороны но тем не менее их вектор идет на общее развитие. Как по мне то лучшее что мы, как технари, можем сделать — это стараться хорошо разбираться в большом количестве технологий (читай «инструментов») и понимать сильные и слабые стороны каждой (понимать и принимать, а не хейтить или бездумно защищать). Тогда в те моменты когда от нас что-то зависит мы можем применять подходяющую технологию в подходящем месте. Например заранее планировать переход на новую архитектуру и технологию при достижении числа юзеров выше X, или числа запросов к серверу выше Y, или к базе выше Z… короче планировать заранее и стараться доносить такую потребность до бизнеса. В общем искать взаимовыгодные компромисы

                          Например Slack — я четко понимаю что решение написать единую платфрому на JS и для десктопа использовать Electron было вполне неплохим решением на начальных этапах. Однако сейчас у них столько денег что они могут себе позволить написать и поддерживать нативного клиента, что значительно уменьшило бы его нагрузку, перестало жрать батарею а также хоть немного ускорило бы инициацию звонков

                • khim
                  /#20077926

                  все однозначно согласны, что они плохие и лучше бы их не было?
                  Так не бывает. У любого, самого идиотского решения, даже того, про которое сам автор признал, что они идиотское и хорошо бы его исправить — всегда находятся защитники. Стокгольмский синдром во всей красе.

                  Например, typeof null на практике сильно не мешает, а прототипы или динамическая типизация — вы не сможете сказать, что все разработчики однозначно против.
                  Тут есть некоторая проблема в том, что идеальных языков вообще не бывает, а их практическая применимость — очень сильно зависит от задач. И, в частности, количество попыток прикрутить в JavaScript'у статическую типизацию показываает, что режим тяп-ляп-и-в-продакшн уже многих не устраивает… а поделать с этим ничего нельзя.

                  • Keyten
                    /#20077974

                    Я разумеется не имею в виду, что все 100% разработчиков должны быть согласны, а какой-нибудь несогласный стажёр всё ломает. Я имею в виду, что не нужно выдавать субъективность за объективность, а открытые вопросы за решённые. Вы не можете выдать за объективную истину «пробелы лучше табов» или «статическая типизация лучше динамической» или «прототипы лучше классов», потому что примерно 50% (или 30% или 20% или 10%) с вами не согласятся, и однозначного ответа, почему одно лучше другого, не найдено.

                    количество попыток прикрутить в JavaScript'у статическую типизацию показываает, что режим тяп-ляп-и-в-продакшн уже многих не устраивает

                    Ага, а количество попыток затащить JS на сервер показывает, что все остальные серверные языки уже многих не устраивают.

                    Ничего это не показывает. Только количество разработчиков, которые попытались пересесть со статически типизированных языков на js, и ощутили, что им не нравится писать без типов. Это даже легко доказать: если человек изучает программирование начиная с js, ему и в голову не придёт тащить в него типы :)

                    • khim
                      /#20078124

                      Ага, а количество попыток затащить JS на сервер показывает, что все остальные серверные языки уже многих не устраивают.
                      Именно так. Несмотря на то, что «не устраивают» они, по большому счёту, ровно по одному показатели: наличию достаточно дешёвой рабочей силы — это именно так.

                      Это даже легко доказать: если человек изучает программирование начиная с js, ему и в голову не придёт тащить в него типы :)
                      Я видел массу контр-примеров. Обычно идёт «путешествие с возвратом»: вначале человек попадает, так или иначе, в проект, где тысячи программистов пишут миллионы строк кода (ни одного такого успешного проекта, написанного на языке с динамической типизацией мне лично не известно… думаю что они всё же есть — но их очень мало), а потом уже возвращается в JavaScript с осознанием того что есть статическая типизация и для чего она нужна…

                    • justme
                      /#20079870 / +1

                      JS на сервер затащили во многом потому что нужна рабочая сила, а JS на подъеме и разработчиков проще найти или научить. Сейчас очень много молодых стартапов живущих по циклу: написать PoC, получить первые инвестиции, на них написать MVP/MSP, получить инвестиций побольше, расширить команду и писать конечный продукт
                      В таком подходе, а также в современных Agile и т.п. на начальных этапах JS выглядит как неплохое бизнс-решение. А если те же кодеры смогут писать еще и сервер = то вообще шик!!!

                • alix_ginger
                  /#20079534

                  Приведение типов могло бы быть более традиционным

                  • Zenitchik
                    /#20080476

                    Тогда порог вхождения стал бы ещё ниже, и говнокода стало бы ещё больше.

            • khim
              /#20077032

              Про JS надо помнить эпоху когда он создавался — тогда никто, даже в смелых мечтах (или унылых кошмарах) не мог бы представить то в чем мы сейчас варимся :)
              Представить-то как раз могли. Более того — JavaScript им ровно для этого и понадобился. Чтобы можно было использовать один язык и на клиенте и на сервере (в NAS). Вот только клиент взлетел, сервер умер… и пришлось ждать ещё очень долго пока совсем другие люди первоначальную идею реализуют…

        • Godebug
          /#20075144

          Откровенно говоря, понять что имеется в виду под «средств для веб-программирования» очень сложно. Особенно дико этот коммент смотрится в топике про особенности оптимизаций v8 :)

    • sapfear
      /#20075176 / +1

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

    • 1c80
      /#20077350

      Это не косяк же, а особенность поведения, вот если бы в один прогон было 30 мегов, а другой 15 байт, то тогда да, был косяк.

  4. XenonDev
    /#20073038

    Я предлагаю использовать такой способ:


    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
      /#20073104

      Я бы рекомендовал всё-таки разбирать строку на символы и «собирать» обратно.

      Все вот эти «быстрые» способы — это игра с огнём. Там вверху приведен случай, когда один «быстрый» алгоритм очистки работал-работал, а потом вдруг перестал… И тут то же самое тут может случится.

      На сервере же, где вы всё можете контролировать, можно просто «вытащить» соответствующую низкоуровневую функцию из V8 и вызывать её…

      • qw1
        /#20076724

        И однажды оптимизатор начнёт выбрасывать конструкции split+join, как не меняющие строку.

        • khim
          /#20077044

          Это всё-таки очень спицифическая оптимизация. Такой код почти невозможно написать случайно — так что разработчики будут понимать, что это «очистка» строки.

          Хотя было бы неплохо где-нибудь хотя бы оффициальную рекомендацию увидеть. Типа «делайте так — и мы гарантируем, что это не выбросят». Это да.

          • red_andr
            /#20077608

            Или сделать библиотечную функцию, которая гарантированно будет всегда работать.

  5. Psychosynthesis
    /#20073214

    Ого, интересно, спасибо!

    А что, если просто написать функцию, которая будет побайтово копировать нужные символы из строки?

    • dollar
      /#20074648

      Будет медленнее, чем встроенные функции. Но можете попробовать, напишите свою clearString() — протестируем, сравним скорость. Собственно, .split('').join('') — это и есть побайтовое копирование.

      • WanSpi
        /#20074792

        Я бы не был так уверен, тот же '.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.338623046875ms

        • dollar
          /#20074954

          Интересно. Но всё же нет. Я добавил вашу функцию к своим тестам и вот что вышло:

          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
            /#20075056 / +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
              /#20075108

              Что то мне подсказывает что лиса откидывает '.split('').join('')', или как то хитро оптимизирует, но могу ошибаться, так как попробовал ту же '[...str].join('')' и она выполняеться уже порядка 700мс.

      • GavriKos
        /#20077380

        Может посимвольное, а не побайтное? 1 символ в том же юникоде будет занимать больше байта.

        • dollar
          /#20077452

          Формально вы правы, поэтому статью я начал с «30 байт» на 15 символов, но реально в Chrome строка из 15 млн. символов «z» занимает 15 мегабайт, судя по диспетчеру Chrome. Плюс я считаю хорошим тоном отвечать на языке того, кто задал вопрос, если это не меняет суть ответа.

          • Cerberuser
            /#20078340

            Ну, строка из 15 миллионов символов «z» и должна занимать 15 мегабайт — ASCII же в UTF-8 умещается в один байт (или в JS строка — это UTF-16?)

            • khim
              /#20078358

              или в JS строка — это UTF-16
              Именно так. Как обычно: изначально думали уложиться в UCS-2, не получилось создали идиотский вариант, который одновременно и медленный и много памати жрёт… возможно V8 «втайне от пользователя» использует UTF-8?

              • shaukote
                /#20080862 / +1

                Насколько я помню, так и есть, V8 автоматически переключается между Latin-1 и UTF-16 в качестве внутреннего представления строк.
                (Пруфов, увы, не будет — не могу сходу найти.)

  6. melodyn
    /#20073230

    Я правильно понял, что join эту проблему решает?

    А как дела с интерполяцией?

  7. igormich88
    /#20073470 / +1

    Странно что нет стандартного метода который принудительно отвязывает строку от родителей. Так как по моему проблема всех самописных решений их ненадежность при смене/обновлении платформы.

    • ReklatsMasters
      /#20074574

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

      Это не часть спеки языка, это делали реализиции в конкретном движке. И в v8 есть свой персональный метод %FlattenString(s), правда для этого нужно включить нативный синтаксис --no-allow-natives-syntax.

      • igormich88
        /#20081862

        И в результате получаются вот такие библиотеки
        github.com/davidmarkclements/flatstr/blob/master/index.js

        • yarkov
          /#20082130 / +1

          Смутила эта строка

          var v8 = require('v' + '8')

          Не подскажете для чего это?

          • mayorovp
            /#20082180

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

            • yarkov
              /#20082228

              Хм, интересный способ, не встречал раньше. Спасибо за объяснение.

      • shaukote
        /#20085462

        Насколько я понимаю, %FlattenString (как и использующая его flatstr) не про то и здесь она не поможет — она "уплощает" cons strings (строки, образованные в результате конкатенации), но sliced strings она возвращает без изменений (по сути своей её реализация не слишком сложна).


        Собственно, как уже написал ниже flapenguin — применительно к описанной статье проблеме %FlattenString поможет только если сначала к "проблемной" sliced string что-нибудь прибавить (и получится тот самый (' ' + str).slice(1)).

  8. jreznot
    /#20073502 / +1

    Удивительно, но в серверной Java (ещё в 8 версии) отказались от всех этих оптимизаций и сделали копирование данных при выделении подстрок. Кажется в JS придут туда же, но не сразу.

    • vsb
      /#20074496

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

      • Prototik
        /#20075058

        Да особо смысла нет. char[] + offset + length хватит для обработки чего угодно, результат уже можно упаковать в String.

    • Dima_Sharihin
      /#20076108 / +1

      А в C++ есть std::string, const std::string &, std::string && и std::string_view.
      И сиди думай, который где использовать

  9. Zoolander
    /#20073730 / +1

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

    • concatenating with empty string
    • trim()
    • slice()
    • match()
    • search()
    • replace() with no match
    • split()
    • substr()
    • substring()
    • toString()
    • valueOf()


    Странно, я постоянно работают с мини-парсерами, которые используют кучу этих функций и проблем с памятью не наблюдал. Вероятно, потому что мои функции работают с очень маленькими строками, поэтому heap и не превышает обычных размеров для моих приложений (8-9 мегабайт)

    PS: кроме того, этот перечень, как и описание бага было составлено в 2013 — и с тех пор, возможно, часть утечек перестала воспроизводиться.

    • khim
      /#20077086 / +1

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

      Проблемы возникают только когда вы берёте кусок большой строки, а потом про большую «забываете» (а GC-то помнит!).

  10. namikiri
    /#20073920

    Простейший кейс, когда мы точно знаем, что в строке число, либо нам нужно получить число:
    str = str - 0;


    А чем такая конструкция хуже? Или нет разницы?
    str = +str;

    • radist2s
      /#20074012 / -2

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

      • iliazeus
        /#20074672

        Тогда уж лучше писать

        str = Number(str);

        • vsb
          /#20074940

          Почему не parseInt?

          • Aquahawk
            /#20075284

            Потому что 1.5 тоже число

            • vsb
              /#20075804

              Ну тогда parseFloat, если нужны дробные части (почти никогда не нужны).

          • f0rmat1k
            /#20076366

            Я думаю причины скорее в семантике. parseInt намекает, что мы хотим попарсить строку и извлечь намбер, и нам ок, если 10f5 превратится в 10. Number же честно пытается 1 в 1 сделать перевод и вернет NaN в случае ошибки, что больше соответствует нашему желанию и ожиданиям.

    • dollar
      /#20074102

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

      Из той же серии:

      str = str * 1;

      Экономим символы в коде:
      str-=0;
      str*=1;
      

  11. stereoworlder
    /#20074166

    Это вечная проблема, когда сайт делает «чудо-профи»… он же — дилетант

    • dollar
      /#20074190 / +1

      Этот «дилетант» прочёл всю спецификацию JavaScript всех версий от корки до корки. И вообще, у него может быть Firefox, где проблемы нет. Но заказчик всё же позвонил этому «горе-профи».

  12. vassabi
    /#20074580

    вот тут еще предлагают
    var string_copy = original_string.slice(0);
    const newStr = Object.assign("", myStr);
    var nName = String(name);

  13. SergeiMinaev
    /#20074916

    В Firefox пример с утечкой слегка работает — объем потребляемой памяти возрастает где-то на 200-250мб, а потом приходит GC.

    • dollar
      /#20075708

      Это не утечка, так и должно быть. Мусор сначала накапливается, а не чистится сразу. И если наращивать память по 15Мб в секунду, то где-то несколько сотен мегабайт и должно быть в пике. С применением clearString() будет та же картина и в Chrome.

      • SergeiMinaev
        /#20075764

        Автор написал, что ссылки на родительские строки — особенность V8 и, если продолжать мысль, то в FF это проявляться вообще не должно. Но оно проявляется, просто GC приходит секунд через 20 после запуска примера MemoryLeak().

        В любом случае, и в FF иногда стоит использовать clearString(), чтобы не съедать лишнюю память в ожидании сборщика мусора.

        • homm
          /#20075890 / +1

          Исходные строки по 15 мегабайт создаются хоть в FF, хоть в Хроме. Разница в том, что в FF они как и любые созданные объекты, освобождаются, когда приходит сборщик мусора (но не раньше, поэтому успевают накопиться 200-250мб), а в Хроме нет, потому что на них остаются формально живые ссылки.


          Если в будете использовать в FF clearString(), это никак не поможет, потому что исходные строки по 15 мегабайт все еще будут создаваться.

          • SergeiMinaev
            /#20075990

            Да, действительно, в FF никак не повлияло. А вот в Chrome при использовании (' ' + str).slice(1); память вообще перестала сколько-нибудь заметно увеличиваться.

      • Tanriol
        /#20085428 / +2

        В Firefox есть точно та же проблема, только максимальная "короткая" строка не 12 байт, как в Chrome, а 23 / 11 символов (для Latin1 / двухбайтных — см. раздел inline strings по ссылке). На 24 символах проблема воспроизводится. Кстати, следует учитывать, что выбор режима между Latin1 и двухбайтным выполняется исключительно по тому, в каком режиме была изначальная строка — то есть если там был Юникод за пределами Latin1, то и все подстроки будут оставаться ссылками по лимиту в 11 символов, а не 23.


        Что ещё веселее, если внимательно посмотреть на результаты теста в JSPerf на Firefox, то окажется, что "решение" через (' ' + str).slice(1) работает за время, не зависящее от длины подстроки. Дело в том, что в Firefox эта операция выполняется через строку типа Rope и не приводит к "отсоединению" от базовой строки, то есть не выполняет свою задачу. Лишний раз видим, что самые быстрые решения зачастую оказываются менее надёжными.

        • dollar
          /#20085450

          А вот это интересно, спасибо. Проблема воспроизводится, если вырезать 25 символов. И это всё меняет. Непонятно только, почему при перезагрузке вкладки или при переходе на другой сайт память не очищается. Только если закрыть вкладку — убивается соответствующий процесс, а вместе с ним и память.

  14. Ogoun
    /#20075350 / +3

    Еще интересный момент, как именно крашится chrome, он не анализирует есть ли утечка памяти, а тупо при превышении аллоцированной памяти в два гига на странице пишет что ВОЗМОЖНО есть утечка памяти и крашит страницу принудительно, не давая возможности что то предпринять. Соответственно держа память под контролем и просто работая с большими объемами, легко схлопотать этот краш ни за что. Edge, firefox, opera, vivaldi при этом справляются.

    • OlegTar
      /#20077584

      Почему Вы выделили слово «возможно»?

      • Ogoun
        /#20081404

        Потому что chrome в консоли пишет maybe. Т.е. сам показывает что он не уверен, но на всякий закрашит страницу.

        • OlegTar
          /#20083224

          а как он может быть уверен? если программа много отожрала памяти, это ещё не значит, что это неконтролируемый объем памяти.

  15. Cerberuser
    /#20075434 / +2

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

  16. FFxSquall
    /#20075508 / +1

    Спасибо, что разложили по полочкам. Видел ваш вопрос на Тостере и принимал участье в комментариях. Оказывается иногда полезно углубиться в то как работает V8 внутри. Постоянно надо быть начеку =)

  17. V1tol
    /#20075548

    Попробовал в 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
      /#20077376

      А попробуйте еще для верности UTF-16. Это внутреннее представление строк в JS (обязательное условие, прописанное в спеке) и возможно будет сильно быстрее.

      • ReklatsMasters
        /#20077566 / -1

        ascii самая быстрая кодировка для буферов, на нескольких проектах убеждался. А всё потому, что там простое конвертирование 1 в 1 без всякой ерудны юникодной. А вообще это дикость конечно, из строки, потом в буфер, потом обратно...

        • homm
          /#20077596

          ascii не может быть кодировкой для произвольной строки, только для специфических строк. Диапазон ascii 128 значений, диапазон внутреннего представления строк в JS — 65 тысяч.

          • ReklatsMasters
            /#20078966

            Точно. Упустил этот момент. В любом случае это глупость гонять строку через буфер. Создание типизированого массива достаточно дорогое удовольствие. А тем более конвертирование его в строку.

        • V1tol
          /#20080234

          А вообще это дикость конечно, из строки, потом в буфер, потом обратно...

          Никто и не спорит. Но так же является дикостью то, что V8 не чистит «большую» строку, когда на неё нет прямых ссылок и не копирует необходимые чанки в подстроки. Если не нужно парсить мегабайты строк в секунду — то схема «строка -> буфер -> строка» является, возможно, единственным гарантированным способом в Node.js именно скопировать строку без всяких приватных функций V8 и прочих хаков.

      • homm
        /#20079428 / +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
        /#20080210 / +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
        

  18. jordansamuil
    /#20075766 / -1

    Теперь все стало ясно и понятно. Потому что берещь некоторых прогеров на работу ( а не шаришь в этом) и он как наделает делов и все, ховайся. Искал даже сео-агенство (целый список нашел http://ktoprodvinul.ru/) чтоб с полным комплекосм. Вроде уже все ок, однако все же разбираться надо! Еще раз спасибо!

  19. Bhudh
    /#20075798

    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
      /#20075810

      Почему не сделать явную очистку?
      huge = undefined;
      Потому что это не поможет. Ссылка на большую строку останется в small.

      • Bhudh
        /#20075826

        А где именно? В привязанном объекте типа [[Scope]] для функции?

        • dollar
          /#20075832

          Нет, ссылка находится в самой строке small. Об этом вся статья.

          • Bhudh
            /#20075862

            Хотите сказать, что я могу написать что-то вроде small.parent; и получить все 15 метров huge в консоль?

            • dollar
              /#20075924

              Этого нет в спецификации JavaScript, так что странно надеяться на такое. Даже если сможете, это будет возможностью конкретной среды исполнения. Для Google Chrome я не знаю способов. Но факт остаётся фактом — в V8 у строк есть скрытая ссылка на «родителя» или нескольких «родителей».

            • homm
              /#20075932

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

              • Bhudh
                /#20075980 / -1

                О чём и был мой вопрос выше. «Дефолтные аргументы и локальные константы функций» и «переменные замыканий» — это скрытый объект [[Scope]], который есть у функции.
                Но меня заверяют, что такого скоупа у строки нет, а ссылка вдруг где-то есть. Мол, "в самой строке". Хотя строка — это иммутабельный инстанс типа String в виде массива байтов со списочным доступом к каждому. В нём технически не может быть никакой ссылки.

                • homm
                  /#20076010

                  Но меня заверяют, что такого скоупа у строки нет

                  Помилуйте, где вас в чем-то подобном заверяют?


                  а ссылка вдруг где-то есть. Мол, "в самой строке"

                  Но так и есть.


                  Хотя строка — это иммутабельный инстанс типа String в виде массива байтов со списочным доступом к каждому. В нём технически не может быть никакой ссылки.

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

                  • Bhudh
                    /#20076030

                    Помилуйте, где вас в чем-то подобном заверяют?
                    Здесь:
                    dollar сегодня в 15:58
                    Нет

                    Но так и есть.
                    Вынужден повторить вопрос
                    А где именно?

                    Ну если вы лучше знаете, как что устроено
                    Знал бы — не спрашивал бы.
                    Но не знаю и спрашиваю.
                    Знаю только, как устроена строка по спеке.

                    • homm
                      /#20076092

                      Здесь:

                      Вы очень своеобразно интерпретируете свой вопрос и этот ответ. Вас заверяют, что «ссылка не хранится в привязанном объекте типа [[Scope]] для функции», но вы почему-то прочитали это как «такого скоупа у строки нет». Одно с другим логически никак не связано.


                      А где именно?

                      А что вы рассчитываете получить в ответ? Описание структуры на Си? Ну посмотрите в исходниках движка. Какая разница как, сути это не меняет: строка small содержит внутри себя ссылку на строку huge.


                      Знаю только, как устроена строка по спеке.

                      Вы путаете описание интерфейса (спека) с тем, как устроено внутри (реализация).

                      • Bhudh
                        /#20076124

                        Вы очень своеобразно интерпретируете свой вопрос и этот ответ.
                        Интересно, а как Вы узнали, как я интерпретирую свой вопрос? На Хабре завелись телепаты? Видимо, один из телепатов и подумал, что под «объектом типа [[Scope]]» я разумею что-то специфицированное да и влепил мне минус за незнание спеки…
                        А я всего лишь хотел узнать, привязан ли к строке хоть какой-нибудь объект с внутренними ссылками, пусть даже где-то в недрах V8. Помимо String.prototype.
                        Кстати, надо заметить, что хотя в статье речь о строках (и это вроде как подтверждается скриншотами консоли), во всех примерах эти строки создаются в функциях, что даёт основание утверждать, что в [[Scope]] этих функций huge также может сохраняться.

                        • khim
                          /#20077124 / +3

                          Минусов вам влепили за обсолютное незнание предмета. Ну всё равно как если бы учитель русского языка средней школы начал обсуждать JavaScript и заводить баги на тему того, что 'ё' < 'ж' — это false.

                          Точно так же как учитель русского языка не может себе представить, что буквы могут быть отсортированы как-то иначе, чем в русском алфивите — вы не можете представить себе, что в природе могут существовать сущности, не отражённые в JavaScript.

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

                          • Bhudh
                            /#20078154 / -1

                            2 khim
                            Не знаю, у кого тут "обсолютное" незнание, я уже 10 лет на JS пишу.
                            И прекрасно знаю, что в JS бывают сущности, не отображённые в спеке, хотя бы document и window в браузере или тип fxobj в PDF.
                            --------------------
                            2 vassabi
                            По спецификации String всё-таки тип, несахарных классов в JS нет.
                            А в реализации String это функция-конструктор (как и любая другая функция, объявляемая с сахарным ключевым словом class).
                            И это не объекты String ведут себя как строки, а строки — представители простого типа — ведут себя как объекты-обёртки, например, при вызове методов.

                            массив байтов — это массив байтов (и его размер может быть больше, чем размер строки в нем).
                            Зависит от того, в каких единицах считать строку. Если её считать в байтах, размер будет одинаковый. Если в символах Уникода, то, понятно, число уменьшится. А уж если кто-то решит считать строку в тех эмодзи, что отображаются на странице в браузере, а массив байтов: в тех байтах, что заключают в себе всю строку в имплементации (30 MiB согласно статье)…
                            --------------------
                            2 all
                            Я с самого начала спрашивал про реализацию (где и как), а не про спецификацию.
                            Но минусовать-то, конечно, проще, чем объяснять. И да, я знаю про Великое Правило «помянул минус — получи минус». Но мне на него как-то с Пизанской башни.
                            Всем читающим и пишущим желаю писать на хорошем и правильном русском языке и помнить, что ружьё бывает разряжённое, а воздух разрежённым, а не наоборот.

                            • mayorovp
                              /#20078370

                              Как раз window и document в спеке есть, только смотреть надо не спеку на javascript, а спеку на HTML.


                              Я с самого начала спрашивал про реализацию (где и как), а не про спецификацию.

                              Так вам с самого начала и объяснили. А дальше вы встали в позицию "не верю, так не бывает"...

                              • Bhudh
                                /#20081748 / -1

                                Я ответил, что "так не бывает" именно по спецификации, в которой строка (которая не объект) есть «ordered sequence of zero or more 16-bit unsigned integer values (“elements”) up to a maximum length of 253-1 elements». В это определение ссылки на родителей "не помещаются".
                                Почему и спросил, где они и как реализованы.

                                • homm
                                  /#20081878 / +1

                                  Еще раз, вы путаете спецификацию (интерфейс) и реализацию. Спецификация описывает, как должен себя вести тот или иной объект, а не как он должен быть устроен. Указанное требование «ordered sequence of zero or more …» никак не нарушается данной реализацией.

                                • homm
                                  /#20081898 / +1

                                  Я ответил, что "так не бывает"

                                  Вы живете в каком-то выдуманном мире. Вам объясняют, как это устроено, вы отвечаете «так не бывает». Ваше дело, конечно.

                                  • Bhudh
                                    /#20081960 / -3

                                    А «так не бывает» это и не мои слова, я сам их зацитировал.
                                    И просил я именно объяснить, «как устроено» и каким образом помещаются в строку ссылки минуя «ordered sequence of elements».
                                    Как вся фигня, к которой нет доступа в функциях, помещается в функцию, понятно: скрытый объект.
                                    Как вся фигня, к которой нет доступа в строках, помещается в строку:? Скрытое что?

                                    • homm
                                      /#20083072 / +1

                                      Как вся фигня, к которой нет доступа в строках, помещается в строку?

                                      Ну подождите, а как в строку помещается вся фигня, к которой есть доступ в строках? Вы это понимаете? Там не скрытое что?


                                      Как вся фигня, к которой нет доступа в функциях, помещается в функцию, понятно: скрытый объект.

                                      Удивительно, что вам это понятно. Там же объект! А какой у него класс? Какие методы? Время жизни? Ваше объяснение, которе вас удовлетворяет для функций, на самом деле не объясняет вообще ничего, о том, как это устроено. Но почему-то точно такое же объяснение для строк (там скрытая ссылка на родительский объект) вас вдруг ставит в тупик.

                                      • Bhudh
                                        /#20083578

                                        Ну подождите, а как в строку помещается вся фигня, к которой есть доступ в строках? Вы это понимаете? Там не скрытое что?
                                        Не скрытое то, что по спецификации для конечного пользователя должно выглядеть упорядоченной последовательностью целых беззнаковых 16-битных чисел и методом доступа []. На уровне языка реализации и интерпретации имеем класс для выделения, чтения, записи и освобождения памяти под эти последовательности. Достаточно и необходимо.

                                        точно такое же объяснение для строк (там скрытая ссылка на родительский объект)
                                        О, [[Scope]] уже стал родительским объектом? Или в чём объяснение "точно такое же"? Я же это первым делом спросил: есть там что-то типа [[Scope]] или нет?

                                        • khim
                                          /#20084480

                                          На уровне языка реализации и интерпретации имеем класс для выделения, чтения, записи и освобождения памяти под эти последовательности. Достаточно и необходимо.
                                          Достаточно, но не необходимо. Более того: в V8 строки, поддерживая иллюзию того, что внутри них просто массив (иначе это всё не соответствовало бы спецификацию) имеют сложную структуру, особенности которой иногда прорываются наружу.

                                          Я же это первым делом спросил: есть там что-то типа [[Scope]] или нет?
                                          Осталось только понять что вы подразумеваете под «чем-то типа [[Scope]]». Ничего явно видимого в Developer Tools нету ибо, в отличие от [[Scope]], строка может менять своё строение «на лету» в то время как вы на неё смотрите, так что смотреть на её внутреннее устройство не остановив JavaScript-мир проблематично. Но, разумеется, внутри V8-строки есть богатый внутренний мир… со своими особенностями.

                                          Собственно вся статья — об этих особенностях.

                                          • Bhudh
                                            /#20085976

                                            Ничего явно видимого в Developer Tools нету
                                            А как же это:
                                            image

                • dollar
                  /#20076014 / +1

                  Вы рассуждаете с точки зрения логики языка JavaScript. Но эта особенность — не часть языка, а часть конкретной системы управления памятью. Эта система вам ничего не «должна». В частности, у вас нет гарантий того, какой объем памяти будет ассоциирован со строкой, и как там вообще всё будет оптимизировано.

                • vassabi
                  /#20077192 / +2

                  строка — это строка,
                  массив байтов — это массив байтов (и его размер может быть больше, чем размер строки в нем).
                  а String — это класс, объекты которого ведут себя, как будто они строки. Как они устроены внутри на самом деле, какие внутри них ссылки, байты и массивы — это глубокие детали реализации.

  20. ehots
    /#20075982 / -1

    Жду я значит уже минут 10 в мозилле, вклада увеличила аппетит с 375 Мб до ~600.
    Нагрузка на ОЗУ конечно выросла, но далеко до краша.

    • khim
      /#20077130 / -1

      Ещё один чукча-писатель? Статью читать не пробовали?

      Конкретно вот это:

      Это особенность движка V8, которая позволяет ускорить работу со строками в ущерб, естественно, памяти. То есть это касается Google Chrome и прочих хромиум-браузеров, а также Node.js. Этого уже достаточно, чтобы отнестись серьёзно к этому явлению.


      Нафига медитировать на мозиллой, где этой проблемы нет (там свои, другие, проблемы есть)?

      • ehots
        /#20078374 / -5

        Сдержанность не удел особей вроде тебя?

  21. ExplosiveZ
    /#20076658

    Золотые времена IE6Chrome
    Писать workaround'ы для кривого runtime, тут хотя бы исходники есть.

    • sumanai
      /#20083202 / +1

      Притом полностью забили на браузеры на альтернативных движках. Во времена IE6 хотя бы иногда заботились о других браузерах ((

      • shaukote
        /#20085156

        FGJ, все вышеперечисленные хаки, предназначенные для компенсации проблемы в V8, навроде str.split('').join(''), никак не ломают поведение в других браузерах/движках.

  22. rpiontik
    /#20077014

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

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

    Вопрос №1 — а не может ли такое поведение быть специфичным для режима отладки? Когда по каким-то причинам двигло пытается сохранить побольше инфы для, ну например, профайлера. Буду проверять. Интересно…

    Вопрос №2 — не является ли это осмысленным перерасходом памяти для оптимизации, но при этом память начнет высвобождаться в свободные процессорные тики? Т.е. сначала работаем на производительность, затем на оптимизацию. Нужно тоже проверять…

    • dollar
      /#20077232 / +1

      Devtool кэширует тела ответов ajax, из-за чего память процесса (вкладки) в Chrome, естественно, растёт, породой до огромных размеров. Но чтобы анализировать проблемы памяти именно в JS, нужно смотреть на память JS, она называется «JS Heap» или «Память JavaScript».

      В Devtool во вкладке памяти предлагается анализировать как раз память JS, так что проблемы нет.

      В диспетчере задач Chrome нужно отдельно включить столбик с памятью JS, для этого кликните правой кнопкой по заголовкам и выберите соответствующий пункт:

      Скриншот

  23. 3axap4eHko
    /#20077468

    dollar с учетом того что строка итерируется, то нет нужды ее разбивать через split, можно просто сделать [...string].join('')

  24. flapenguin
    /#20077762 / +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
      /#20077852

      Еще интересно, что самый быстрый способ (' ' + str).slice(1) — самый быстрый ровно по той же причине почему происходит эта утечка. NewProperSubString насильно плющит строку. Итоговый перерасход памяти — 1 символ.
      Остальные варианты создают куда большие промежуточные объекты.

  25. tbl
    /#20079156 / +1

    Интересно, что в Java уже сталкивались с такими утечками (особенно в мире Java EE, когда выгрузка приложения из контейнера неожиданно не освобождала часть памяти), и, начиная с версии 1.7.0_06, провели деоптимизацию: String.substring(int, int) не шарит нижележащий массив символов с новой строкой, а создает новый массив для новой подстроки. Но если тебе все-таки очень надо не создавать копию подстроки (например, исходная строка очень длинная, а подстрока включает большую ее часть и нужна лишь на короткое время), и ты понимаешь и принимаешь все возможные риски подобной оптимизации, то предусмотрели обходной маневр: CharBuffer.wrap(str).subSequence(int, int).

    По-моему, в JavaScript тоже могли бы пойти по подобному пути, причем, это не требует особых приседаний со стороны движка.

    • transcengopher
      /#20079874 / +1

      Альтернативой может стать усложнение GC по отношению к строкам — тогда можно сохранить внутреннее представление в виде ссылки на родительский элемент, но при этом ссылка на родителя не должна препятствовать его сборке GC — и при сборке демон должен будет прозрачно заменить ссылку+оффсеты на функционально и логически эквивалентный «настоящий» массив, полученный в результате слайса.

      В Java такое сделать проблематично из-за строгости реализации — при таких операциях GC должен иметь эксклюзивный доступ к объекту, чтобы другие потоки не могли увидеть её в неконсистентном состоянии (а они могут, т.к. массив и оффсеты это три операции записи, причём в несинхронизированые поля, которые ещё и синхронизировать нельзя из соображений производительности). Это подразумевает чуть ли не подмену значения по всем ссылкам, которая должна быть полностью незаметна для всего пользовательского кода, и прочие весёлости, вроде следующей из такого требования двойной индирекции в самих указателях. В общем, проще деоптимизировать и вернуться к проблеме если она станет совсем уж невыносимой.

      Рискну предположить, что в JS подобные трюки проворачивать всё же проще, так что вдруг у V8 получится.

      • tbl
        /#20079946

        Решили, наверно, не заморачиваться, пока не найдется PoC эксплоита, кладущий целиком ноду.жс, как это было с Java EE контейнерами.

      • tbl
        /#20079968

        Кстати, в java serial gc и parallel gc для old generation умеют выполнять дефрагментацию хипа, останавливая весь мир, так что встроить туда компактификацию строк с заменой нескольких указателей — не очень космическая проблема.

        • transcengopher
          /#20081110

          Уметь-то умеют, но инфраструктура в целом сейчас движется к уменьшению пауз GC — так что надеяться на очистку внутри STW решение не очень хорошее, потому что STW в идеале наступать не должен никогда.

          Я вроде бы где-то читал, что работа над строками по-прежнему идёт. Вот в 9 версии, к примеру, сделали компактификацию в смысле перекодирования в LATIN-1 если в строке находятся только совместимые символы — это уже должно уменьшить потребление по меньшей мере процентов на 20, иногда на 40. Следующим этапом, вроде бы, должна стать замена эквивалентных char[] в разных строках.
          Может быть, когда-нибудь и доживём до триумфального возвращения шаренных массивов. Но однозначно не в виде STW-обработки, и не в ближайшие два-три года. Пусть сначала Coin и Loom закончат.

  26. unC0Rr
    /#20079614

    Технически это не memory leak, а space leak. Отличается тем, что память всё же можно освободить, удалив строку.

  27. dollar
    /#20083564

    Добавил тесты на jsperf.com