Эта статья относится к анализу падения производительности React в блоге разработчиков V8, подробностей много, поэтому я поделюсь ими с вами.
JavaScript — язык со слабой типизацией, и мы можем присвоить любой тип значения переменной, но даже в этом случае для различных значений JavaScript V8 по-прежнему необходимо применять определенное представление памяти для различных типов значений. После полного понимания основных принципов мы можем даже начать с того, как используются переменные, для написания более элегантного кода, соответствующего поведению движка.
Начнем с хорошо известных типов переменных JavaScript 8.
Типы переменных JavaScript
Восемь типов переменных
Согласно текущей спецификации ECMAScript, в JavaScript существует восемь типов значений:Number
,String
,Symbol
,BigInt
,Boolean
,Undefined
,Null
,Object
.
Эти типы значений могут быть переданы черезtypeof
обнаружен оператор, за одним исключением:
typeof 42;
// → 'number'
typeof 'foo';
// → 'string'
typeof Symbol('bar');
// → 'symbol'
typeof 42n;
// → 'bigint'
typeof true;
// → 'boolean'
typeof undefined;
// → 'undefined'
typeof null;
// → 'object' 注意这里
typeof { x: 42 };
// → 'object'
почему typeof null === 'объект'
В спецификации,Null
Хотя какnull
сам тип, ноtypeof null
но вернулсяobject
. Чтобы понять принцип проектирования, стоящий за этим, мы должны сначала понять определение в JavaScript, в котором все коллекции типов разделены на две группы:
- объекты (ссылочные типы, такие как
Object
тип) - примитивы (примитивные типы, все нессылочные типы значений)
В определенииnull
означаетno object value
,а такжеundefined
означаетno value
.
Согласно приведенному выше изображению, основатель JavaScript Брендан Эйх будет принадлежать кobjects
а такжеnull
Все значения типа в рамках набора типов возвращаются единообразно'object'
Типы.
Фактически, в то время на него повлияла Java. В Яве,null
никогда не был единым типом,Он представляет собой значение по умолчанию для всех ссылочных типов.. Вот почему, хотя в спецификации указаноnull
иметь свой собственныйNull
тип, покаtypeof null
все еще возвращаюсь'object'
причина.
представление значения в памяти
Механизмы JavaScript должны иметь возможность представлять в памяти произвольные значения с той оговоркой, чтоНа самом деле существуют разные представления памяти для одного и того же типа значения..
например значение42
Типы в JavaScriptnumber
:
typeof 42;
// → 'number'
И есть много способов представить в памяти42
:
representation | bits |
---|---|
8-битное дополнение до двух | 0010 1001 |
32-битное дополнение до двух | 0000 0000 0000 0000 0000 0000 0010 1010 |
двоично-кодированное десятичное число | 0100 0010 |
32-битный IEEE-754 с плавающей запятой одинарной точности | 0100 0010 0010 1000 0000 0000 0000 0000 |
64-битный IEEE-754 с плавающей запятой двойной точности | 0100 0000 0100 0101 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 |
Стандартные соглашения ECMAScriptnumber
Числа нужно рассматривать как 64-битные числа двойной точности с плавающей запятой, но на самом деле постоянное использование 64-битных чисел для хранения любых чисел на самом деле очень неэффективно, поэтомуДвигатели JavaScript не всегда используют 64-битные для хранения чисел, движок может использовать другие представления памяти внутри (например, 32-битные), просто убедитесь, что все объекты за пределами числа, которые можно отслеживать, выровнены по 64-битному представлению.
Например, мы знаем, что в ECMAScriptЮридический индекс массивадиапазон в[0, 2³²−2]
:
array[0]; // Smallest possible array index.
array[42];
array[2**32-2]; // Greatest possible array index.
При доступе к элементам массива по индексу нижнего индекса V8 будет использовать 32-битный метод для хранения этих номеров нижнего индекса в допустимом диапазоне., что является лучшим представлением памяти. Использование 64 бит для хранения индексов массива приведет к большим потерям. Каждый раз, когда происходит доступ к элементу массива, движку необходимо постоянно преобразовывать Float64 в дополнение до двух. В настоящее время использование 32 бит для хранения индексов может сэкономить половину времени преобразования. .
32-битная нотация с дополнением до двух используется не только в операциях чтения и записи массива, но и во всех[0, 2³²−2]
Числа внутри будут храниться преимущественно в 32-битном режиме, и в целом процессор будет обрабатывать целочисленные операции намного быстрее, чем операции с плавающей запятой, поэтому в следующем примере эффективность выполнения первого цикла почти в два раза быстрее как второй цикл:
for (let i = 0; i < 100000000; ++i) {
// fast → 77ms
}
for (let i = 0.1; i < 100000000.1; ++i) {
// slow → 122ms
}
То же самое верно и для операторов, производительность оператора mol в следующем примере зависит от того, являются ли два операнда целыми числами:
const remainder = value % divisor;
// Fast: 如果`value`和`divisor`都是被当成整型存储
// slow: 其他情况
Стоит отметить, что для операции mol, когдаdivisor
Когда значение представляет собой степень двойки, V8 добавляет для этого случая дополнительный путь быстрого доступа.
Кроме того, хотя целые значения могут храниться в 32 битах, результаты операций между целыми значениями могут по-прежнему давать значения с плавающей запятой, а сам стандарт ECMAScript основан на 64 битах, поэтому результаты операций указываются. также соответствуют 64-битной производительности с плавающей запятой. В этом случае движку JS необходимо специально обеспечить корректность результатов следующих примеров:
// Float64 的整数安全范围是 53 位,超过这个范围数值会失去精度
2**53 === 2**53+1;
// → true
// Float64 支持负零,所以 -1 * 0 必须等于 -0,但是在 32 位二进制补码中无法表示出 -0
-1*0 === -0;
// → true
// Float64 有无穷值,可以通过和 0 相除得出
1/0 === Infinity;
// → true
-1/0 === -Infinity;
// → true
// Float64 有 NaN
0/0 === NaN;
Сми, номер кучи
V8 определяет специальное представление для целых чисел в 31-битном диапазоне со знаком.Smi
, любое другое, не принадлежащееSmi
данные определяются какHeapObject
,HeapObject
Представляет физический адрес памяти.
Для цифр неSmi
Числа в диапазоне определяются какHeapNumber
,HeapNumber
это особыйHeadObject
.
-Infinity // HeapNumber
-(2**30)-1 // HeapNumber
-(2**30) // Smi
-42 // Smi
-0 // HeapNumber
0 // Smi
4.2 // HeapNumber
42 // Smi
2**30-1 // Smi
2**30 // HeapNumber
Infinity // HeapNumber
NaN // HeapNumber
Smi
Диапазонные целые числа очень распространены в программах JavaScript, поэтому цели V8Smi
Включена специальная оптимизация:когда используешьSmi
номер, движку не нужно выделять для него выделенный объект памяти, и он позволяет выполнять быстрые операции с целыми числами..
Из вышеперечисленного обсуждения мы можем знать, что даже если значения имеют одинаковый тип JavaScript, двигатель все еще может использовать разные представления памяти для достижения методов оптимизации.
Smi vs HeapNumber vs MutableHeapNumber
Smi
а такжеHeapNumber
Как это работает? Предположим, у нас есть объект:
const o = {
x: 42, // Smi
y: 4.2, // HeapNumber
};
o.x
середина42
будет рассматриваться какSmi
хранятся непосредственно в самом объекте, аo.y
середина4.2
Дополнительный объект памяти должен быть открыт для хранения, иo.y
Указатель объекта указывает на этот объект памяти.
На данный момент, когда мы запускаем следующий фрагмент кода:
o.x += 10;
// → o.x is now 52
o.y += 1;
// → o.y is now 5.2
В этом случае,o.x
Значение обновляется на месте, поскольку новое значение52
ещеSmi
В диапазоне.а такжеHeapNumber
неизменен, когда мы меняемo.y
ценность5.2
, V8 необходимо открыть новый объект памяти, чтобы датьo.y
Цитировать.
с помощьюHeapNumber
Неизменяемые функции, V8 может включать некоторые средства, такие как следующий код, который мыo.y
Ссылка на значение, указанная дляo.x
:
o.x = o.y;
// → o.x is now 5.2
В таком случае V8 не нужноo.x
новое значение5.2
Чтобы открыть объект памяти, но напрямую использовать ту же ссылку на память.
Обладая вышеуказанными преимуществами,HeapNumber
Неизменный характер также имеет недостаток, если нам нужны частые обновления.HeapNumber
значение, эффективность выполнения будет выше, чемSmi
Гораздо медленнее:
// 创建一个`HeapNumber`对象
const o = { x: 0.1 };
for (let i = 0; i < 5; ++i) {
// 创建一个额外的HeapNumber对象
o.x += 1;
}
За этот короткий цикл двигатель должен был создать 6HeapNumber
пример,0.1
,1.1
,2.1
,3.1
,4.1
,5.1
, и к моменту окончания цикла 5 из этих экземпляров будут мусором.
Чтобы предотвратить эту проблему, V8 предоставляет оптимизированный способ обновленияSmi
Значение:Когда область цифровой памяти имеет не-Smi
значение в диапазоне, V8 пометит область какDouble
области и выделено 64-битное представление с плавающей запятойMutableHeapNumber
пример.
Затем, когда вы снова обновите эту область, V8 больше не нужно будет создавать новуюHeapNumber
экземпляр, и может быть непосредственноMutableNumber
Экземпляр обновлен.
Как упоминалось ранее,HeapNumber
а такжеMutableNumber
Оба используют ссылки на указатели, чтобы указывать на объекты памяти, иMutableNumber
является изменчивым, если в этот момент вы будете принадлежатьMutableNumber
значениеo.x
назначить другим переменнымy
, вы не должны хотеть ваше следующее изменениеo.x
час,y
Тоже поменял. Чтобы предотвратить это, когдаo.x
при совместном использовании,o.x
внутриMutableHeapNumber
необходимо переупаковать какHeapNumber
Перейти кy
:
Инициализация, устаревание и миграция формы
Разные представления памяти соответствуют разным
Shape
, Форму можно понимать каккласс структуры данныхтакое же существование.
Проблема возникает, если мы присваиваем значение переменной в началеSmi
диапазон чисел, за которым следует присваиваниеHeapNumber
Диапазон чисел, что будет делать двигатель?
В следующем примере мы создаем два объекта с одинаковой структурой данных и помещаемx
значение инициализируетсяSmi
:
const a = { x: 1 };
const b = { x: 2 };
// → objects have `x` as `Smi` field now
b.x = 0.2;
// → <span class="javascript">b.x</span> is now represented as a <span class="javascript">Double</span>
y = a.x;
Два объекта указывают на одну и ту же структуру данных, гдеx
как дляSmi
.
Сразу после модификацииb.x
Значение0.2
, V8 необходимо выделить новый, отмеченный какDouble
Форма даетb
, и перенаправьте новый указатель Shape обратно на пустую форму, кроме того, V8 также необходимо выделитьMutableHeapNumber
экземпляр для хранения этого0.2
. Затем V8 хочет максимально повторно использовать фигуры, а затем помечает старые фигуры какdeprecated
.
Можно заметить, что в это времяa.x
На самом деле, он по-прежнему указывает на старую форму, а V8 помечает старую форму какdeprecaed
Цель, очевидно, удалить его, но для двигателя,Прямой обход памяти для поиска всех объектов, указывающих на старую фигуру, и заблаговременное обновление ссылок — на самом деле очень затратная операция.. V8 использует ленивую схему обработки:в следующий разa
Когда происходят какие-либо обращения к свойствам и присвоения,a
перенос формы в новую форму. Эта схема может в конечном итоге привести к тому, что старый Shape потеряет все счетчики ссылок и будет просто ждать, пока сборщик мусора освободит его.
резюме
Мы подробно обсудили следующие вопросы знаний:
- Базовая пара JavaScript
primitives
а такжеobjects
различие, иtypeof
причина неточности. - Даже если значения переменных имеют одинаковый тип, базовый движок может использовать разные представления памяти для их хранения.
- V8 попытается найти оптимальное представление памяти для хранения каждого свойства в вашей программе JavaScript.
- Мы обсудили обработку V8 инициализации, устаревания и миграции Shape.
Основываясь на этих знаниях, мы можем вывести некоторые передовые методы кодирования JavaScript, которые могут помочь повысить производительность:
- Старайтесь инициализировать свои объекты одной и той же структурой данных, чтобы использование Shape было максимально эффективным.
- Выберите разумные начальные значения для ваших переменных, чтобы движок JavaScript мог напрямую использовать соответствующее представление памяти.
- write readable code, and performance will follow
Поняв сложные низкоуровневые знания, мы получили очень простые передовые методы кодирования, и, возможно, эти моменты могут немного улучшить производительность. Но так называемое накопление накопленных ресурсов как раз и состоит в том, чтобы знать эти точки оптимизации, поддерживаемые базовой теорией, а мы можем знать только то, что знаем, когда пишем код.
Кроме того, мне очень нравится такой технический момент, который можно рассматривать от мала до велика.В будущем, когда люди будут спрашивать вас, почему вы так объявляете переменные, вы часто можете начать действовать...
Оригинальная ссылка:yangzicong.com/article/14