Лучшие практики V8: начните с положения переменных JavaScript

внешний интерфейс
| Введение Сколько приемов оптимизации может быть применительно к переменным в слабо типизированном языке JavaScript? Эта статья начинается с самых основных типов переменных и углубляется в базовый механизм преобразования и оптимизации типов V8. Настоящий старый драйвер, строка кода бросается в глаза. В будущем вы можете сказать, что код, который я написал, даже объявления переменных быстрее, чем ваш...

Эта статья относится к анализу падения производительности 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 потеряет все счетчики ссылок и будет просто ждать, пока сборщик мусора освободит его.


резюме

Мы подробно обсудили следующие вопросы знаний:

  • Базовая пара JavaScriptprimitivesа такжеobjectsразличие, иtypeofпричина неточности.
  • Даже если значения переменных имеют одинаковый тип, базовый движок может использовать разные представления памяти для их хранения.
  • V8 попытается найти оптимальное представление памяти для хранения каждого свойства в вашей программе JavaScript.
  • Мы обсудили обработку V8 инициализации, устаревания и миграции Shape.

Основываясь на этих знаниях, мы можем вывести некоторые передовые методы кодирования JavaScript, которые могут помочь повысить производительность:

  • Старайтесь инициализировать свои объекты одной и той же структурой данных, чтобы использование Shape было максимально эффективным.
  • Выберите разумные начальные значения для ваших переменных, чтобы движок JavaScript мог напрямую использовать соответствующее представление памяти.
  • write readable code, and performance will follow

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

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

Справочная статья:The story of a V8 performance cliff in React

Оригинальная ссылка:yangzicong.com/article/14