Для длинной строки из 512000000 символов требуется ~ 1 МБ памяти
Максимальная длина строки JavaScript
Согласно спецификации ECMAScript строка не может содержать более 9 007 199 254 740 991 (2⁵³-1) символа. Очевидно, что достижимая максимальная длина также зависит от мощности компьютера. Также браузеры могут устанавливать свои ограничения. Максимальная длина строки, установленная в исходном коде Chrome, составляет 536,8 миллиона символов. Чтобы упростить пример кода, в этом посте я экспериментирую с немного более короткой строкой длиной 512 миллионов символов.
Сходство между строками и объектами
В JavaScript строки и другие примитивные типы отличаются от объектов тем, что не могут иметь свойств. Но есть сходство между строкой и объектами, что может быть важно для оптимизации производительности веб-приложения. Как и к объектам, доступ к строковым значениям осуществляется через ссылки, содержащиеся в переменных. Когда строковая переменная присваивается другой переменной, в целевую переменную копируется не строковое значение, а ссылка на место строки в памяти.
Как хранятся результаты конкатенации
В зависимости от характеристик и происхождения в движке JavaScript строки могут быть представлены как экземпляры нескольких классов C ++. Строки, полученные путем конкатенации, представлены специализированным классом ConsString
. Экземпляр ConsString
содержит пару ссылок на исходные строки и занимает всего 20 байт памяти. Две ссылки могут указывать на строковые значения, а также на любой внутренний тип, представляющий строку. Например, когда инструкция
let d = a + b + c;
выполняется, переменной d
присваивается ссылка на ConsString
((_5 _, _ 6 _), _ 7_), в которой одна ссылка указывает на _8 _ (_ 9 _, _ 10_), а вторая ссылка указывает на значение переменной c
. Независимо от исходных строк, только 40 дополнительных байтов используются для хранения значения, присвоенного переменной d
.
Хранение длинной строки 512 миллионов символов в ~ 1 МБ
Чтобы продемонстрировать, что длина строки может быть намного больше, чем объем памяти, занимаемый строкой, я создам строку длиной 512 миллионов символов, используя функцию double(str, times)
:
export function double(str, times) { for (let i = 0; i < times; i++) { str = str + str; } return str; }
Функция сначала удваивает полученную строку, а затем продолжает удваивать результат конкатенации указанное количество раз.
Я измеряю используемую память с помощью функции printMemory(label)
, которую я подробно описал в предыдущем посте. Вкратце, функция основана на новом методе performance.measureUserAgentSpecificMemory()
, который ожидает завершения сборки мусора, а затем точно записывает использованную память. По умолчанию метод запускает сборку мусора только в том случае, если этого не происходит в течение 20 секунд. Таким образом, измерения будут довольно медленными, если не использовать запуск Chrome с дополнительным флагом, разрешающим measureUserAgentSpecificMemory()
принудительно выполнить немедленную сборку мусора.
Я удваиваю строку длиной 1 миллион символов, которая занимает 1 МБ памяти. Строка генерируется функцией makeString(mbs)
:
function makeString(mbs) { let str = 'X'.repeat(mbs * MB); str[0]; return str; }
На первый взгляд бессмысленный оператор str[0];
фактически превращает ConsString
, созданный 'X'.repeat(mbs * MB);
, в непрерывную последовательность символов. ConsString
имеет оптимальную структуру, аналогичную дереву, показанному на верхнем рисунке. Перед сведением корень ConsString
и его дочерние элементы ссылаются в общей сложности на ~ 17 различных ConsString
и, таким образом, занимают менее 1 КБ.
В основном коде используются три функции, описанные выше:
printMemory('baseline') .then(() => { a = makeString(1); console.log('a', a.length); return printMemory('a = makeString(1)'); }) .then(() => { b = double(a, 9); console.log('b', b.length); return printMemory('b = double(a,9)'); }) .then(() => { a = null; return printMemory('a = null'); }) .then(() => { b = null; return printMemory('b = null'); }) .then(() => { plot(getHistory()); });
По сути, код выполняет четыре шага и измеряет используемую память после каждого шага. Код:
- Назначает строку размером 1 МБ переменной
a
и подтверждает в консоли, что длинаa
равна 1 000 000. Ожидается, что объем используемой памяти будет на 1 МБ больше, чем базовый уровень из предыдущего измерения. - Удваивает
a
девять раз, чтобы результирующая строка была в 2⁹ раза длиннее, чемa
, присваивает полученную строку переменнойb
и подтверждает в консоли, что длинаb
равна 512 000 000. Используемая память не должна сильно отличаться от предыдущего измерения, потому что девять созданныхConsString
s занимают дополнительно 180 байтов. - Устанавливает
a
вnull
. Объем используемой памяти при этом не должен сильно измениться. - Устанавливает
b
вnull
. Исходная строка, присвоеннаяa
, собирается сборщиком мусора. Используемое значение памяти должно вернуться к базовому уровню.
После выполнения всех шагов код отображает записанные значения использованной памяти на диаграмме.
Вывод в консоли подтверждает, что даже несмотря на то, что размер используемой памяти не сильно изменился, длина строки, присвоенной b
, действительно составляет 512000000 символов:
Вы можете запустить код на образце веб-сайта https://longstring.onrender.com/. Но наберитесь терпения, потому что, как я разъяснил выше и ранее, по умолчанию измерение памяти занимает до 20 секунд.
В следующих постах я исследую возможности оптимизации использования памяти во время построения DOM. Например, знаете ли вы, какие из них требуют меньше памяти - методы, принимающие HTML, такие как innerHTML
, или методы, такие как appendChild()
, ожидающие узлов? Фактически, это зависит от того, как вы их используете.
Образец кода можно скачать с https://github.com/marianc000/longString.