[internals] строки в js

Viktor Love
2 min readOct 16, 2018

--

Итак, сижу, почитываю код. Нахожу паттерн, за который в C# можно бить в рожу: чуваки берут и просто конкатенируют кучу строк через +. Оказывается, в js это нормально. Извращенцы.

Решил поизучать поподробнее, обнаружил кучу интересного.

Веревки

Оказывается что, строки реализованы с помощью структуры данных подобной Ropes. Это значит, что объединение строк в js является почти бесплатной операцией (т.е. O(1)).

Представление о том, как работает такая структура данных можно получить в следующих статьях:

См. пруф: https://twitter.com/mourner/status/973664460291411969. Там же говорится, что самые лучшие результаты достигаются, если строки имеют длину ≥ 12. Ну или читайте исходники.

Примечание: исходники v8 говорят, что для создания ConsString (при объединении строк) первая строка должна иметь длину ≥ 13.

Интересные особенности

Дальше, еще более внезапно, но это не совсем Ropes.

В теории извлечение символа из строки должно быть не бесплатное. Но почему-то получаются странные результаты: первое извлечение платное (O(n)), второе бесплатное (O(1)).

Исходники говорят о том, что V8 просто превращает ConsString в нормальную строку. См. код функции “Runtime_StringCharCodeAt”. Функция charCodeAt является потенциальной перфоманс проблемой.

Т.е. это Ropes, но только до тех пор пока не начнешь трогать строки.

Комментарии автора

У меня двоякое ощущение от этой оптимизации. С одной стороны это пиздец как круто, что кто-то подумал и сделал хорошо за тебя. С другой стороны, есть потенциальные редкие проблемные сценарии.

Тем не менее, в контексте сайтиков и говноаппликух это очень хорошее решение.

Слайсы

А для того, чтобы в js быстро работала операции substring и slice, чуваки тоже зафигачили оптимизацию.

Если после такой операции получится строка длины ≥ 13, то v8 делает такой трюк: вместо честной новой строки создается объект такого вида: SlicedString(*parentString, offset, len).

Ловим лулзы

Что внезапно вылезает боком: мы задерживаем ссылку на parentString, т.е. GC не может её собрать. Потенциальный memory leak для того, чтобы обеспечить O(1) вместо O(n) от длины итоговой строки.

На эту штуку даже заведен баг: “Substring of huge string retains huge string in memory”. Там зачетный workaround предложен:

function unleakString(s) { return (' ' + s).substr(1); }

Судя по всему, достаточно много проектов словили эту проблему.

Бонус. В треде упомянута статья “A Tale of a JavaScript Memory Leak”, в которой рассказывается: регулярки работают через SlicedString. Также результаты выполнения regexp доступны через глобальное свойство RegExp.lastMatch. GC смотрит на всех в этой истори как на говно.

Ловим больше лулзов

Но! Если внимательно почитать код, то можно обнаружить интересную деталь. В конечном счете мы придем к функции “NewProperSubString”. В которой есть вызов Flatten. А он будет отрабатывать O(n) от длины исходной строки, если она представлена ConsString.

Этот Flatten и объясняет, почему костыль выше вообще работает.

Комментарии автора

И вот это очень странная оптимизация. Пиздец какая сранная оптимизация.

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

P.S: Совет дня: не пишите на JS обработку текстов.

--

--

Viktor Love
Viktor Love

Written by Viktor Love

Software Engineer from Ukraine. TypeScript, React, C#, Angular.

Responses (1)