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

Да, могут.

//.....Какой-то код
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
    24.04.2019 23:10
    +2

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


    1. dagen
      24.04.2019 23:16

      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»).


      1. enabokov
        25.04.2019 21:58

        Status: Assigned (Open)


        1. dagen
          25.04.2019 22:42

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


    1. nrgian
      25.04.2019 16:35
      +1

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

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


      1. keenondrums
        25.04.2019 18:46
        +1

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


    1. OlegTar
      25.04.2019 21:23

      Это не бага


      1. Opaspap
        27.04.2019 06:31

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


  1. pallada92
    24.04.2019 23:20
    +1

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

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


    1. Koneru
      25.04.2019 09:03

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


    1. dollar Автор
      25.04.2019 10:30

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


  1. MSC6502
    24.04.2019 23:23
    -15

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


    1. avost
      25.04.2019 01:03
      +2

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


      1. XenonDev
        25.04.2019 02:37

        deleted


      1. tangro
        26.04.2019 10:47

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


    1. XenonDev
      25.04.2019 03:04

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


    1. Godebug
      25.04.2019 10:43

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


      1. yarkov
        25.04.2019 11:16
        +2

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


        1. dagen
          25.04.2019 11:58

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


        1. justme
          25.04.2019 13:14

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


          1. Godebug
            25.04.2019 13:52

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

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


            1. justme
              25.04.2019 20:13
              +2

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

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


              1. Keyten
                26.04.2019 00:06

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

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


                1. homm
                  26.04.2019 00:47

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


                  1. Keyten
                    26.04.2019 01:03

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


                    1. homm
                      26.04.2019 01:07

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


                      1. Keyten
                        26.04.2019 01:11

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


                        1. justme
                          26.04.2019 12:32

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

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


                1. justme
                  26.04.2019 00:51

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

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

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

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


                  1. Keyten
                    26.04.2019 01:37

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

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

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

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

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


                    1. khim
                      26.04.2019 02:47

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

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

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

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


                      1. Keyten
                        26.04.2019 17:36

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

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

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


                        1. khim
                          26.04.2019 20:21

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

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

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

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


                    1. justme
                      26.04.2019 12:10
                      +1

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

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

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


                      1. khim
                        26.04.2019 20:29

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

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

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


                        1. justme
                          26.04.2019 22:36

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


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

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

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

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


                          1. khim
                            26.04.2019 22:56

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

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

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


                    1. 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

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


                      1. transcengopher
                        26.04.2019 16:05

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

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


                        1. justme
                          26.04.2019 16:44

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

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


                1. khim
                  26.04.2019 00:58

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

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


                  1. Keyten
                    26.04.2019 01:16

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

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

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

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


                    1. khim
                      26.04.2019 02:22

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

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


                    1. justme
                      26.04.2019 12:20
                      +1

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


                1. alix_ginger
                  26.04.2019 11:20

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


                  1. Zenitchik
                    26.04.2019 14:10

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


            1. khim
              25.04.2019 20:30

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


        1. Godebug
          25.04.2019 13:48

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


    1. sapfear
      25.04.2019 13:54
      +1

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


    1. 1c80
      25.04.2019 21:45

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


  1. 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(). Странно почему так получается… В данном случае они должны быть идентичны.


    1. khim
      25.04.2019 03:32

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

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

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


      1. qw1
        25.04.2019 19:10

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


        1. khim
          25.04.2019 20:34

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

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


          1. qw1
            25.04.2019 21:55

            Выше было официальное решение, специфичное для v8.


            1. khim
              26.04.2019 01:22
              +1

              Оно, увы, специфично не для v8, а конкретно для Node.js.

              В браузере его не применить…


          1. red_andr
            25.04.2019 22:59

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


  1. Psychosynthesis
    25.04.2019 05:19

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

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


    1. dollar Автор
      25.04.2019 12:09

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


      1. 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.338623046875ms


        1. dollar Автор
          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, а просто рассчитываю на разумный подход оптимизатора. Возможно, я что-то не учёл, тогда прошу обратить на это моё внимание.


          1. 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
            


            1. WanSpi
              25.04.2019 13:40

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


      1. GavriKos
        25.04.2019 21:53

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


        1. dollar Автор
          25.04.2019 22:10

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


          1. Cerberuser
            26.04.2019 06:01

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


            1. khim
              26.04.2019 06:25

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


              1. shaukote
                26.04.2019 15:15
                +1

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


  1. melodyn
    25.04.2019 05:33

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

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


  1. igormich88
    25.04.2019 08:00
    +1

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


    1. ReklatsMasters
      25.04.2019 11:56

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

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


      1. igormich88
        26.04.2019 18:14

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


        1. yarkov
          26.04.2019 19:34
          +1

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

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

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


          1. mayorovp
            26.04.2019 19:52

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


            1. yarkov
              26.04.2019 20:03

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


      1. shaukote
        28.04.2019 04:22

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


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


  1. jreznot
    25.04.2019 08:17
    +1

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


    1. vsb
      25.04.2019 11:40

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


      1. Prototik
        25.04.2019 13:32

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


    1. Dima_Sharihin
      25.04.2019 16:43
      +1

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


  1. 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 — и с тех пор, возможно, часть утечек перестала воспроизводиться.


    1. khim
      25.04.2019 20:50
      +1

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

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


  1. namikiri
    25.04.2019 10:02

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


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


    1. radist2s
      25.04.2019 10:17
      -2

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


      1. iliazeus
        25.04.2019 12:14

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

        str = Number(str);


        1. vsb
          25.04.2019 13:12

          Почему не parseInt?


          1. Aquahawk
            25.04.2019 14:15

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


            1. vsb
              25.04.2019 15:54

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


          1. f0rmat1k
            25.04.2019 17:42

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


    1. dollar Автор
      25.04.2019 10:32

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

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

      str = str * 1;

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


  1. stereoworlder
    25.04.2019 10:44

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


    1. dollar Автор
      25.04.2019 10:47
      +1

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


  1. vassabi
    25.04.2019 11:57

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


  1. SergeiMinaev
    25.04.2019 13:07

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


    1. dollar Автор
      25.04.2019 15:39

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


      1. SergeiMinaev
        25.04.2019 15:47

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

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


        1. homm
          25.04.2019 16:06
          +1

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


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


          1. SergeiMinaev
            25.04.2019 16:19

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


      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 и не приводит к "отсоединению" от базовой строки, то есть не выполняет свою задачу. Лишний раз видим, что самые быстрые решения зачастую оказываются менее надёжными.


        1. dollar Автор
          28.04.2019 03:25

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


  1. Ogoun
    25.04.2019 14:32
    +3

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


    1. OlegTar
      25.04.2019 22:54

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


      1. Ogoun
        26.04.2019 16:54

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


        1. OlegTar
          27.04.2019 01:02

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


  1. Cerberuser
    25.04.2019 14:50
    +2

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


  1. FFxSquall
    25.04.2019 15:04
    +1

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


  1. 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
    



    1. homm
      25.04.2019 21:52

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


      1. ReklatsMasters
        25.04.2019 22:47
        -1

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


        1. homm
          25.04.2019 22:56

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


          1. ReklatsMasters
            26.04.2019 09:54

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


        1. V1tol
          26.04.2019 13:30

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

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


      1. 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 предпочтительнее.


      1. 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
        


  1. jordansamuil
    25.04.2019 15:48
    -1

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


  1. 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; // Возвращаем маленький кусочек
    }


    1. dollar Автор
      25.04.2019 15:54

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


      1. Bhudh
        25.04.2019 15:57

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


        1. dollar Автор
          25.04.2019 15:58

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


          1. Bhudh
            25.04.2019 16:02

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


            1. dollar Автор
              25.04.2019 16:10

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


            1. homm
              25.04.2019 16:11

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


              1. Bhudh
                25.04.2019 16:17
                -1

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


                1. homm
                  25.04.2019 16:24

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

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


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

                  Но так и есть.


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

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


                  1. Bhudh
                    25.04.2019 16:29

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

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

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


                    1. homm
                      25.04.2019 16:40

                      Здесь:

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


                      А где именно?

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


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

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


                      1. Bhudh
                        25.04.2019 16:49

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


                        1. khim
                          25.04.2019 20:59
                          +3

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

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

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


                          1. Bhudh
                            26.04.2019 02:34
                            -1

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

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


                            1. mayorovp
                              26.04.2019 06:41

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


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

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


                              1. 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». В это определение ссылки на родителей "не помещаются".
                                Почему и спросил, где они и как реализованы.


                                1. homm
                                  26.04.2019 18:18
                                  +1

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


                                1. homm
                                  26.04.2019 18:22
                                  +1

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

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


                                  1. Bhudh
                                    26.04.2019 18:47
                                    -3

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


                                    1. mayorovp
                                      26.04.2019 18:58
                                      +2

                                      Скрытая реализация.


                                    1. homm
                                      27.04.2019 00:06
                                      +1

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

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


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

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


                                      1. Bhudh
                                        27.04.2019 07:59

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

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


                                        1. khim
                                          27.04.2019 16:40

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

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

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


                                          1. Bhudh
                                            28.04.2019 12:38

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


                1. dollar Автор
                  25.04.2019 16:24
                  +1

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


                1. vassabi
                  25.04.2019 21:18
                  +2

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


  1. ehots
    25.04.2019 16:18
    -1

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


    1. khim
      25.04.2019 21:02
      -1

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

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

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


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


      1. ehots
        26.04.2019 06:49
        -5

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


  1. ExplosiveZ
    25.04.2019 18:54

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


    1. sumanai
      27.04.2019 00:52
      +1

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


      1. shaukote
        27.04.2019 22:08

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


  1. rpiontik
    25.04.2019 20:26

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

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

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

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


    1. dollar Автор
      25.04.2019 21:25
      +1

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

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

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

      Скриншот


  1. 3axap4eHko
    25.04.2019 22:13

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


  1. 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 символов.



    1. flapenguin
      26.04.2019 00:25

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


  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 тоже могли бы пойти по подобному пути, причем, это не требует особых приседаний со стороны движка.


    1. transcengopher
      26.04.2019 12:21
      +1

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

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

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


      1. tbl
        26.04.2019 12:38

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


      1. tbl
        26.04.2019 12:43

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


        1. transcengopher
          26.04.2019 15:59

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

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


  1. unC0Rr
    26.04.2019 11:34

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


  1. dollar Автор
    27.04.2019 07:29

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