Как деструктуризация массивов может замедлить JavaScript код

Источник: «How Array Destructuring Can Slow Down Your JavaScript»
В статье я расскажу об оптимизации использования деструктурирующего присваивания для повышения производительности. Рассмотрим различия в байткоде между двумя подходами к деструктурирующему присваиванию. Я проведу тест, подчёркивающий эти различия, и в конце вы поймёте, почему ArrayAssignmentPattern не всегда является лучшим выбором.

Сколько шаблонов деструктуризации существует в JavaScript

В JavaScript существует два шаблона для Деструктурирующего присваивания

  1. ArrayAssignmentPattern
  2. ObjectAssignmentPattern

ArrayAssignmentPattern

Давайте рассмотрим алгоритм ArrayAssignmentPattern в спецификации Ecma262 для примера const [ first, second ] = [ 1, 2 ]

ArrayAssignmentPattern : [ AssignmentElementList ]
1. Let iteratorRecord be ? GetIterator(value, sync).
2. Let result be Completion(IteratorDestructuringAssignmentEvaluation of AssignmentElementList with argument iteratorRecord).
3. If iteratorRecord.[[Done]] is false, return ? IteratorClose(iteratorRecord, result).
4. Return result.

Как видим, в алгоритме создаётся итератор, требующий много ресурсов.

function arrayAssignmentPattern(){
const [ first, second ] = [ 1, 2 ]
return first, second;
}

arrayAssignmentPattern()

Теперь посмотрим на байткод (V8) этого кода

CreateArrayLiteral [0], [0], #37
Star2
GetIterator r2, [1], [3]
Star4
GetNamedProperty r4, [1], [5]
Star3
LdaFalse
Star5
LdaTheHole
Star8
Mov <context>, r9
Ldar r5
JumpIfToBooleanTrue [33] (0x2b1a00040115 @ 57)
LdaTrue
Star5
CallProperty0 r3, r4, [11]
Star10
JumpIfJSReceiver [7] (0x2b1a00040104 @ 40)
CallRuntime [ThrowIteratorResultNotAnObject], r10-r10
GetNamedProperty r10, [2], [9]
JumpIfToBooleanTrue [13] (0x2b1a00040115 @ 57)
GetNamedProperty r10, [3], [7]
Star10
LdaFalse
Star5
Ldar r10
Jump [3] (0x2b1a00040116 @ 58)
LdaUndefined
Star0
Ldar r5
JumpIfToBooleanTrue [33] (0x2b1a0004013a @ 94)
LdaTrue
Star5
CallProperty0 r3, r4, [13]
Star10
JumpIfJSReceiver [7] (0x2b1a00040129 @ 77)
CallRuntime [ThrowIteratorResultNotAnObject], r10-r10
GetNamedProperty r10, [2], [9]
JumpIfToBooleanTrue [13] (0x2b1a0004013a @ 94)
GetNamedProperty r10, [3], [7]
Star10
LdaFalse
Star5
Ldar r10
Jump [3] (0x2b1a0004013b @ 95)
LdaUndefined
Star1
LdaSmi [-1]
Star7
Star6
Jump [8] (0x2b1a00040148 @ 108)
Star7
LdaZero
Star6
LdaTheHole
SetPendingMessage
Star8
Ldar r5
JumpIfToBooleanTrue [35] (0x2b1a0004016d @ 145)
Mov <context>, r11
GetNamedProperty r4, [4], [15]
JumpIfUndefinedOrNull [26] (0x2b1a0004016d @ 145)
Star12
CallProperty0 r12, r4, [17]
JumpIfJSReceiver [19] (0x2b1a0004016d @ 145)
Star13
CallRuntime [ThrowIteratorResultNotAnObject], r13-r13
Jump [11] (0x2b1a0004016d @ 145)
Star11
LdaZero
TestReferenceEqual r6
JumpIfTrue [5] (0x2b1a0004016d @ 145)
Ldar r11
ReThrow
LdaZero
TestReferenceEqual r6
JumpIfFalse [8] (0x2b1a00040178 @ 156)
Ldar r8
SetPendingMessage
Ldar r7
ReThrow
Ldar r1
Return

ObjectAssignmentPattern

Рассмотрим алгоритм ObjectAssignmentPattern в спецификации Ecma262 для примера const {0 : first, 1: second } = [ 1, 2 ].

ObjectAssignmentPattern :
{ AssignmentPropertyList }
{ AssignmentPropertyList , }
1. Perform ? RequireObjectCoercible(value).
2. Perform ? PropertyDestructuringAssignmentEvaluation of AssignmentPropertyList with argument value.
3. Return unused.

Как видно, этот алгоритм не создаёт итератор, и можно предположить, что он будет работать быстрее, чем ArrayAssignmentPattern, но не будем на этом останавливаться, а продолжим наше исследование🧐.

function objectAssignmentPattern(){
const { 0: first, 1: second} = [ 1, 2 ]
return first, second;
}

objectAssignmentPattern()

Теперь посмотрим на байткод (V8) этого кода

CreateArrayLiteral [0], [0], #37
Star2
LdaZero
Star3
GetKeyedProperty r2, [1]
Star0
LdaSmi [1]
Star3
GetKeyedProperty r2, [3]
Star1
Ldar r1
Return

В случае ObjectAssignmentPattern, GetKeyedProperty используется для получения значения из объекта по определённому ключу или индексу. И как видим, между ArrayAssignmentPattern и ObjectAssignmentPattern существует большая разница в объёме кода, даже если ничего не понимать в байткоде.

Время для тестирования

Как и предполагалось, ObjectAssignmentPattern будет работать быстрее и с меньшими затратами ресурсов, чем ArrayAssignmentPattern. Давайте проверим это и посмотрим, что покажут цифры.

console.time('ArrayAssignmentPattern');
for (let i = 0; i < 100000; i++) {
let [a, b] = [1,2];
}
console.timeEnd('ArrayAssignmentPattern');

console.time('ObjectAssignmentPattern');
for (let i = 0; i < 100000; i++) {
let { 0: a, 1: b } = [1,2];
}
console.timeEnd('ObjectAssignmentPattern');

Посмотрим, что получится в консоли 👇.

Результаты тестирования ObjectAssignmentPattern и ArrayAssignmentPattern
Результаты тестирования ObjectAssignmentPattern и ArrayAssignmentPattern

Как видим, в этом тесте ObjectAssignmentPattern примерно (как минимум) в 3 раза быстрее, чем ArrayAssignmentPattern, демонстрируя прирост производительности на 200%.

Комментарии


Дополнительные материалы

Предыдущая Статья

Новое в Symfony 7.2: Улучшения языка выражений

Следующая Статья

Новое в Symfony 7.2: Атрибут AsMessage