Нажмите на эту ссылку, чтобы просмотреть новую серию:Как работает V8 — конвейер выполнения JavaScript в V8
Эта статья основана на Chrome 73 для тестирования.
предисловие
V8 может быть знакомой, но незнакомой территорией для фронтенд-разработчиков.
К тому времени, когда вы видите эту статью, у нее было три итерации. Есть только одна цель: чтобы обеспечить максимальную точность, она представлена в более интуитивно понятном виде, чтобы каждому было легче ее принять. Эта статья не требует особых предварительных сведений, достаточно базового понимания объектов JavaScript.
Для того, чтобы статья была менее скучной, и чтобы подтвердить правильность точки зрения, статья содержит множество мелких экспериментов, можете насладиться игрой в консоли.
Подготовительные знания — Просмотр снимков памяти в Chrome
Сначала запускаем такую программу на консоли.
function Food(name, type) {
this.name = name;
this.type = type;
}
var beef = new Food('beef', 'meat');
Переключитесь на Память, щелкните маленький кружок слева, чтобы сделать текущий снимок памяти.
Объект создается через конструктор, в основном для того, чтобы его было проще найти на снимке. После нажатия на снимок введите в фильтрFood
можно найти поFood
Построены все объекты, магия.
Структура объектов в V8
В V8 объекты в основном состоят из трех указателей: скрытого класса,Property
а такжеElement
.
Среди них скрытый класс используется для описания структуры объекта.Property
а такжеElement
Он используется для хранения атрибутов объекта, разница между которыми в основном отражается в том, можно ли индексировать имя ключа.
Свойство и элемент
// 可索引属性会被存储到 Elements 指针指向的区域
{ 1: "a", 2: "b" }
// 命名属性会被存储到 Properties 指针指向的区域
{ "first": 1, "second": 2 }
По сути, это удовлетворениеСпецификация ECMAТребуется дизайн. Как описано в спецификации, индексируемые свойства должны располагаться в порядке возрастания размера значения индекса, а именованные свойства — в порядке возрастания в соответствии с порядком их создания.
Давайте проведем простой небольшой эксперимент.
var a = { 1: "a", 2: "b", "first": 1, 3: "c", "second": 2 }
var b = { "second": 2, 1: "a", 3: "c", 2: "b", "first": 1 }
console.log(a)
// { 1: "a", 2: "b", 3: "c", first: 1, second: 2 }
console.log(b)
// { 1: "a", 2: "b", 3: "c", second: 2, first: 1 }
Разница между a и b заключается в том, что a начинается с индексируемого свойства, а b начинается с именованного свойства. В a индексируемые свойства располагаются в порядке возрастания, именованные свойства идут первыми.first
позжеsecond
. В b индексируемые свойства сортируются не по порядку, и именованные свойства идут первыми.second
позжеfirst
.
можно увидеть
- Индексированные свойства сортируются в порядке возрастания размера значения индекса, а именованные свойства сортируются в порядке возрастания в соответствии с порядком их создания.
- В случае использования как индексируемых, так и именованных свойств консоль печатает четкое разделение между двумя разными свойствами.
- Независимо от того, объявлены ли индексируемые или именованные свойства первыми, они всегда отображаются в консоли в одном и том же порядке (в моем браузере индексируемые свойства всегда идут первыми).
Эти два момента можно подтвердить со стороны, что эти два свойства хранятся отдельно.
После того, как сторона подтверждена, давайте посмотрим на переднюю часть. Мы используем этот метод на предварительных этапах, чтобы увидеть моментальный снимок этих двух свойств.
// 实验1 可索引属性和命名属性的存放
function Foo1 () {}
var a = new Foo1()
var b = new Foo1()
a.name = 'aaa'
a.text = 'aaa'
b.name = 'bbb'
b.text = 'bbb'
a[1] = 'aaa'
a[2] = 'aaa'
A, B названа свойствамиname
а такжеtext
, а имеет два дополнительных индексируемых свойства. Из снимка видно, что индексируемые свойства хранятся вElements
, кроме того, a и b имеют одинаковую структуру (эта структура будет введена позже).
Вам может быть немного любопытно, свойства этих двух объектов различны, как они могут иметь одинаковую структуру? Чтобы понять этот вопрос, начните с того, что задайте себе три вопроса.
- Зачем сохранять объект? Конечно, это для последующего использования.
- Что мне нужно делать, когда я использую его? найти это свойство.
- Для чего нужна структура описания? Поиск по карте, легко найти.
Итак, для индексируемого свойства оно уже упорядочено, так почему же нам приходится много раз перебирать его структуру одним махом. Поскольку нам не нужно искать в его структуре, нам не нужно описывать его структуру, верно? Таким образом, не должно быть трудно понять, почемуa
а такжеb
имеют одинаковую структуру, так как их структура описывает только то, что они оба имеютname
а такжеtext
такая ситуация.
Конечно, есть исключения из этого. Давайте добавим еще одну строку в код выше.
a[1111] = 'aaa'
Видно, что в это время скрытый класс изменился,Element
Хранение данных также стало нерегулярным. Это потому, что когда мы добавляемa[1111]
После этого массив становится разреженным. В целях экономии места разреженные массивы будут преобразованы в хранилище хэшей вместо использования полного массива для описания хранилища этого пространства. Таким образом, эти индексируемые свойства больше не могут напрямую вычислять смещение памяти по значению индекса. Что касается скрытых изменений класса, то это может быть описаниеElement
Структура Foo1 изменилась (эту картинку можно сравнить с картинкой свойства slow ниже, вы можете видеть, что Foo1'sProperty
не вырождается в хранилище хэшей, простоElement
Вырождение в хранилище хэшей, вызывающее изменение скрытых классов).
Различные способы хранения именованных свойств
В V8 существует три разных метода хранения именованных свойств: свойства внутри объекта, быстрые свойства и медленные свойства.
- Внутриобъектные свойства хранятся в самом объекте, обеспечивая максимально быструю скорость доступа.
- Быстрые свойства имеют на одно время адресации больше, чем свойства внутри объекта.
- По сравнению с двумя предыдущими атрибутами, атрибут slow будет хранить полную структуру атрибута (структура двух других атрибутов будет описана в скрытом классе, а скрытый класс будет объяснен ниже), а скорость будет самой медленной. (в следующих или других связанных статьях), медленные атрибуты, словари атрибутов и хранилище хэшей — это одно и то же).
Разве это не немного абстрактно. Не волнуйтесь, давайте проиллюстрируем на примере.
// 实验2 三种不同类型的 Property 存储模式
function Foo2() {}
var a = new Foo2()
var b = new Foo2()
var c = new Foo2()
for (var i = 0; i < 10; i ++) {
a[new Array(i+2).join('a')] = 'aaa'
}
for (var i = 0; i < 12; i ++) {
b[new Array(i+2).join('b')] = 'bbb'
}
for (var i = 0; i < 30; i ++) {
c[new Array(i+2).join('c')] = 'ccc'
}
a, b и c имеют соответственно 10, 12 и 30 атрибутов.В текущей версии Chrome 73 они хранятся тремя способами: внутриобъектные атрибуты, внутриобъектные атрибуты + быстрые атрибуты и медленные атрибуты. Текущий снимок этого блока немного длинный, давайте посмотрим на каждый.
Внутриобъектные свойства и быстрые свойства
Сначала давайте посмотрим на a и b. В некоторой степени свойства в объекте и быстрые свойства фактически совпадают. Однако свойства в объекте фиксируются при создании объекта, а пространство ограничено. В моих экспериментальных условиях количество свойств в объекте зафиксировано на десяти, и эти десять пробелов имеют одинаковый размер (что можно понимать как десять указателей). Когда свойства в объекте заполнены, он будет в виде быстрых свойств вproperties
хранятся в том порядке, в котором они были созданы. По сравнению со свойствами в объекте, быстрые свойства требуют дополнительного времени.properties
, за которым следует линейный поиск в соответствии со свойствами внутри объекта.
медленное свойство
Тогда давайте посмотрим на c. Это действительно слишком долго, перехватывается только часть. Видно, что по сравнению с b (быстрый атрибут)properties
Индекс в стал неправильным числом, что означает, что объект стал хэш-структурой доступа.
Итак, вопрос в том, почему существует так много способов хранения? Позвольте мне рассказать вам мое понимание.
Почему существуют три метода хранения? (личное понимание)
На самом деле это вопрос, поднятый одноклассником, когда он поделился им в компании. Я верю, что у всех возникнут подобные сомнения при чтении этого. В то время я не мог объяснить почему, пока не увидел картинку с хранилищем хэшей (картинка из интернета).
В V8 самая фундаментальная причина всех кажущихся невероятными оптимизаций заключается в том, чтобы быть быстрее. - себя
Видно так, ранние JS-движки использовали медленное хранение атрибутов, а первые два появились для оптимизации этого метода хранения.
Мы знаем, что под капотом все данные будут представлены в виде бинарных файлов. Мы также знаем, что если логика программы включает только двоичные битовые операции (включая И, ИЛИ, НЕ), скорость будет максимальной. Ниже мы игнорируем трудоемкие аспекты адресации и сравниваем эти три (два типа) метода чисто по количеству вычислений.
Атрибуты в объекте и быстрые атрибуты делают очень простые вещи Линейно выясняют, является ли каждая позиция заданной позицией Затратность времени на эту часть можно понимать как трудоемкость не более чем N простых битовых операций (N — общее количество количество атрибутов). Медленный атрибут должен быть сначала рассчитан алгоритмом хеширования. Это сложная операция, которая занимает в несколько раз больше времени, чем простая битовая операция. Кроме того, хеш-таблица представляет собой двумерное пространство, поэтому после вычисления координат одного измерения с помощью хэш-алгоритма по-прежнему требуется линейный поиск в другом измерении. Поэтому нетрудно понять, почему не используются медленные атрибуты, когда их очень мало.
Прилагается алгоритм хеширования для строк в V8, где только 60 сдвигов влево и вправо (60 простых битовых операций).
// V8 中字符串的哈希值生成器
uint32_t StringHasher::GetHashCore(uint32_t running_hash) {
running_hash += (running_hash << 3);
running_hash ^= (running_hash >> 11);
running_hash += (running_hash << 15);
int32_t hash = static_cast<int32_t>(running_hash & String::kHashBitMask);
int32_t mask = (hash - 1) >> 31;
return running_hash | (kZeroHash & mask);
}
Так почему бы не всегда использовать свойства в объекте или быстрые свойства?
Это связано с тем, что при слишком большом количестве атрибутов эти два метода могут быть не такими быстрыми, как медленные атрибуты. Если предположить, что стоимость операции хеширования составляет 60 простых битовых операций, алгоритм хеширования работает хорошо. Если я использую только свойства внутри объекта или быстрые свойства, когда мне нужно получить доступ к 120-му свойству, мне нужно 120 простых битовых операций. С медленным атрибутом нам нужен один расчет хэша (60 простых битовых операций) + линейное сравнение второго измерения (намного меньше 60 раз, предполагалось, что алгоритм хеширования работает хорошо, и атрибут равномерно распределен в хеше таблица).
Программист с односторонними дружескими рекомендациями Сяо ХуэйКомикс: Что такое HashMap? 》
скрытый класс
Упомянутое выше описание того, как хранятся именованные атрибуты, то есть «карта» в «Искать по карте», в V8 называется «Карта» и более известно как скрытый класс (Hidden Class).
В SpiderMonkey (движок Firefox) аналогичный дизайн называется Shape.
Зачем вводить скрытые классы?
Первый конечно быстрее.
JavaScript — это динамический язык программирования, который позволяет разработчикам очень гибко определять объекты. Объекты могут изменять типы, добавлять или удалять свойства во время выполнения. Напротив, в статических языках, таких как Java, типы неизменяемы после создания, а доступ к свойствам можно получить с фиксированным смещением.
Как упоминалось ранее, доступ к атрибутам с помощью хеш-таблицы требует дополнительных хэш-вычислений. Чтобы повысить скорость доступа к свойствам объекта и реализовать быстрый доступ к свойствам объекта, в V8 введены скрытые классы.
Еще одно значение введения скрытых классов заключается в том, что они значительно экономят место в памяти.
В ECMAScript,Атрибут свойств объектаописывается следующей структурой.
-
[[Value]]
: стоимость имущества -
[[Writable]]
: определяет, доступно ли свойство для записи (т. е. может быть переназначено) -
[[Enumerable]]
: определяет, является ли свойство перечисляемым -
[[Configurable]]
: определяет, является ли свойство настраиваемым (удалено)
Введение скрытых классов, атрибутовValue
с прочимиAttribute
отдельный. В общем, стоимость объекта часто меняется, иAttribute
Вряд ли изменится. Так почему мы повторяем описания, которые почти не меняются?Attribute
Шерстяная ткань? Очевидно, что это пустая трата памяти.
Создание скрытых классов
В процессе создания объекта каждый раз, когда добавляется именованный атрибут, соответственно будет генерироваться новый скрытый класс. Внизу V8 реализовано дерево переходов, которое соединяет скрытые классы.Если одинаковые атрибуты добавляются в том же порядке, то дерево переходов гарантирует, что в итоге будут получены такие же скрытые классы.
В следующем примере, когда a является пустым объектом, добавьтеname
После атрибута добавитьtext
Атрибуты будут соответствовать различным скрытым классам соответственно.
// 实验3 隐藏类的创建
let a = {}
a.name = 'thorn1'
a.text = 'thorn2'
Ниже приведена схема процесса создания (описан только процесс, конкретные детали могут незначительно отличаться от реальной реализации).
На снимке памяти мы также можем видеть, что скрытый класс 1 и скрытый класс 2 различаются, аback_pointer
Указатель указывает на первое, что также подтверждает анализ потока на рисунке выше.
В некоторых статьях упоминалось, что в реальном хранилище каждый раз, когда добавляется атрибут, вновь созданный скрытый класс фактически описывает только вновь добавленный атрибут, а не все атрибуты, то есть в скрытом классе 2 только описание.
text
,нетname
. По снимкам памяти пока не проверял (плакал без техники), но логический анализ должен быть таким.
Вот еще одна маленькая точка знаний.
// 实验4 隐藏类创建时的优化
let a = {};
a.name = 'thorn1'
let b = { name: 'thorn2' }
Разница между a и b заключается в том, что a сначала создает пустой объект, а затем добавляет к объекту именованный атрибут.name
. И b напрямую создает атрибут с именованным атрибутомname
Объект. Из снимка памяти мы видим, что скрытые классы a и b разные,back_pointer
Не то же самое. В основном это связано с тем, что при создании скрытого класса b пропускается шаг создания отдельного скрытого класса для пустых объектов. Итак, для генерации одного и того же скрытого класса более точным описанием будет - из той же начальной точки, в том же порядке, добавление свойств с той же структурой (кромеValue
снаружи, атрибутAttribute
последовательный).
Если вас особенно интересует создание скрытых классов, в одностороннем порядке рекомендуем перевод Zhihu @hijiangtaoОсновы движка JavaScript: формы и встроенные кэши.
Волшебная операция удаления
Выше мы обсуждали влияние добавления атрибутов на скрытые классы, давайте посмотрим на влияние операций удаления на скрытые классы.
// 实验5 delete 操作的影响
function Foo5 () {}
var a = new Foo5()
var b = new Foo5()
for (var i = 1; i < 8; i ++) {
a[new Array(i+1).join('a')] = 'aaa'
b[new Array(i+1).join('b')] = 'bbb'
}
delete a.a
Как мы экспериментировали ранее, a и b сами по себе являются свойствами объекта. Как видно из снимка, удаленоa.a
После этого a становится медленным свойством и возвращается в хранилище хэшей.
Однако если мы удалим свойства в обратном порядке, в котором они были добавлены, ситуация изменится.
// 实验6 按添加顺序删除属性
function Foo6 () {}
var a = new Foo6()
var b = new Foo6()
a.name = 'aaa'
a.color= 'aaa'
a.text = 'aaa'
b.name = 'bbb'
b.color = 'bbb'
delete a.text
Мы добавляем один и тот же атрибут к a и b по одному и тому же атрибутуname
а такжеcolor
, а затем добавьте дополнительный атрибут вtext
, затем удалите этот атрибут. Можно обнаружить, что скрытые классы a и b в это время совпадают, и a не возвращается в хранилище хэшей.
Заключение и последствия
- Свойства делятся на именованные свойства и индексируемые свойства.Именованные свойства хранятся в
Properties
, индексируемые свойства хранятся вElements
середина. - Существует три различных метода хранения именованных атрибутов: атрибуты внутри объекта, быстрые атрибуты и медленные атрибуты. Доступ к первым двум осуществляется через линейный поиск, а доступ к медленным атрибутам осуществляется через хранилище хэшей.
- Члены объекта всегда инициализируются в одном и том же порядке, используя преимущества одних и тех же скрытых классов и повышая производительность.
- Добавление или удаление индексируемых атрибутов не вызовет изменений в скрытых классах, а разреженные индексируемые атрибуты превратятся в хранилище хэшей.
- Операция удаления может изменить структуру объекта, в результате чего механизм понизит метод хранения объекта до метода хранения хэш-таблицы, что не способствует оптимизации V8 и его следует избегать, насколько это возможно (при удалении атрибутов в противоположном направлении добавления атрибута объект не вырождается в хранилище хэшей).