Как работает boxing unboxing в javascript
Перейти к содержимому

Как работает boxing unboxing в javascript

  • автор:

Методы примитивов

JavaScript позволяет нам работать с примитивными типами данных – строками, числами и т.д., как будто они являются объектами. У них есть и методы. Мы изучим их позже, а сначала разберём, как это всё работает, потому что, конечно, примитивы – не объекты.

Давайте взглянем на ключевые различия между примитивами и объектами.

  • Это – значение «примитивного» типа.
  • Есть 7 примитивных типов: string , number , boolean , symbol , null , undefined и bigint .
  • Может хранить множество значений как свойства.
  • Объявляется при помощи фигурных скобок <> , например: . В JavaScript есть и другие виды объектов: например, функции тоже являются объектами.

Одна из лучших особенностей объектов – это то, что мы можем хранить функцию как одно из свойств объекта.

let roma = < name: "Рома", sayHi: function() < alert("Привет, дружище!"); >>; roma.sayHi(); // Привет, дружище!

Здесь мы создали объект roma с методом sayHi .

Существует множество встроенных объектов. Например, те, которые работают с датами, ошибками, HTML-элементами и т.д. Они имеют различные свойства и методы.

Однако у этих возможностей есть обратная сторона!

Объекты «тяжелее» примитивов. Они нуждаются в дополнительных ресурсах для поддержания внутренней структуры.

Примитив как объект

Вот парадокс, с которым столкнулся создатель JavaScript:

  • Есть много всего, что хотелось бы сделать с примитивами, такими как строка или число. Было бы замечательно, если бы мы могли обращаться к ним при помощи методов.
  • Примитивы должны быть лёгкими и быстрыми насколько это возможно.

Выбранное решение, хотя выглядит оно немного неуклюже:

  1. Примитивы остаются примитивами. Одно значение, как и хотелось.
  2. Язык позволяет осуществлять доступ к методам и свойствам строк, чисел, булевых значений и символов.
  3. Чтобы это работало, при таком доступе создаётся специальный «объект-обёртка», который предоставляет нужную функциональность, а после удаляется.

Каждый примитив имеет свой собственный «объект-обёртку», которые называются: String , Number , Boolean , Symbol и BigInt . Таким образом, они имеют разный набор методов.

К примеру, существует метод str.toUpperCase(), который возвращает строку в верхнем регистре.

Вот, как он работает:

let str = "Привет"; alert( str.toUpperCase() ); // ПРИВЕТ

Очень просто, не правда ли? Вот, что на самом деле происходит в str.toUpperCase() :

  1. Строка str – примитив. В момент обращения к его свойству, создаётся специальный объект, который знает значение строки и имеет такие полезные методы, как toUpperCase() .
  2. Этот метод запускается и возвращает новую строку (показывается в alert ).
  3. Специальный объект удаляется, оставляя только примитив str .

Получается, что примитивы могут предоставлять методы, и в то же время оставаться «лёгкими».

Движок JavaScript сильно оптимизирует этот процесс. Он даже может пропустить создание специального объекта. Однако, он всё же должен придерживаться спецификаций и работать так, как будто он его создаёт.

Число имеет собственный набор методов. Например, toFixed(n) округляет число до n знаков после запятой.

let num = 1.23456; alert( num.toFixed(2) ); // 1.23

Более подробно с различными свойствами и методами мы познакомимся в главах Числа и Строки.

Конструкторы String/Number/Boolean предназначены только для внутреннего пользования

Некоторые языки, такие как Java, позволяют явное создание «объектов-обёрток» для примитивов при помощи такого синтаксиса как new Number(1) или new Boolean(false) .

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

alert( typeof 0 ); // "число" alert( typeof new Number(0) ); // "object"!

Объекты в if всегда дают true , так что в нижеприведённом примере будет показан alert :

let zero = new Number(0); if (zero) < // zero возвращает "true", так как является объектом alert( "zero имеет «истинное» значение. " ); >

С другой стороны, использование функций String/Number/Boolean без оператора new – вполне разумно и полезно. Они превращают значение в соответствующий примитивный тип: в строку, в число, в булевый тип.

К примеру, следующее вполне допустимо:

let num = Number("123"); // превращает строку в число

null/undefined не имеют методов

Особенные примитивы null и undefined являются исключениями. У них нет соответствующих «объектов-обёрток», и они не имеют никаких методов. В некотором смысле, они «самые примитивные».

Попытка доступа к свойствам такого значения возвратит ошибку:

alert(null.test); // ошибка

Что такое упаковка и распаковка(boxing/unboxing)?

Работа с объектами неизбежно влечет за собой накладные расходы по памяти и быстродействию. Чтобы избежать этого, используются переменные примитивных типов. Это, по сути, простые переменные, как в C или С++. byte занимает 1 байт памяти, int и float — по 4 байта, long и double — по 8 байт и т.д. В отличие от операций с объектами, операции с переменными примитивных типов не требуют выделения/освобождения памяти и выполняются быстро — они в в большинстве случаев в конечном счете компилируются в простые процессорные инструкции, что позволяет программам на Java часто работать со скоростью, сравнимой с программами, написанными на простых компилируемых (непосредственно в машинный код) языках (типа тех же C, С++).

Неудобство с ними в том, что с ними нельзя делать то, что можно делать со всеми объектами — они не имеют методов. Нельзя, например написать

int a = 5; ArrayList list = new ArrayList(); String s = a.toString(); // Ошибка list.add(a)) // Можно, но произойдет автоупаковка // и в коллекцию будет помещен Integer 

их нельзя помещать в коллекции и прочее.

Для того, чтобы обойти это неудобство, для всех примитивных типов существуют соответствующие классы-оболочки, объекты которых могут хранить значения примитивных типов, но обладает всеми свойствами нормальных объектов:

Integer a = 5; ArrayList list = new ArrayList(); String s = a.toString(); // OK list.add(a)) // OK 

Создание объекта-оболочки из переменной примитивного типа называется упаковкой (boxing), а получение значения примитивного типа из объекта-оболочки — распаковкой (unboxing). Объектам-оболочкам можно присваивать значения примитивных типов, а переменным примитивных типов — значения переменных-оболочек, при этом при необходимости автоматически создаются объекты-оболочки с соответствующими значениями (автоупаковка) или наоборот, примитивные значения извлекаются из оболочек (автораспаковка):

int a = 5; Integer b = 10; a = b; // OK, атораспаковка b = a * 123; // OK, автоупаковка 

В тех случаях, когда по контексту требуются объекты (присваивание, вызов метода с передачей параметров), а мы используем значения примитивных типов (переменные или выражения типа 2 * 3), всегда происходит автоупаковка.

Все объекты-оболочки — неизменяемые (immutable) типы, т.е. когда мы присваиваем им новое значение, фактически на замену прежнему объекту создается новый.

Отслеживать
13.7k 12 12 золотых знаков 43 43 серебряных знака 75 75 бронзовых знаков
ответ дан 28 окт 2016 в 13:47
3,377 10 10 серебряных знаков 24 24 бронзовых знака

В версиях ниже JDK 1.5 было не легко преобразовывать примитивные типы данных, такие как int, char, float, double в их классы оболочки Integer, Character, Float, Double. Начиная с версии JDK 5 эта функция, преобразования примитивных типов в эквивалентные объекты, реализована автоматически. Это свойство известно как Автоупаковка(Autoboxing). Обратный процесс соответственно – Распаковка(Unboxing) т.е. процесс преобразования объектов в соответствующие им примитивные типы.

Пример кода для автоупаковки и распаковки представлен ниже:

 1 Integer integer = 9; 
1 int in = 0; 2 in = new Integer(9); 

Когда используется автоупаковка и распаковка?

Автоупаковка применяется компилятором Java в следующих условиях:

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

Отслеживать
25.1k 4 4 золотых знака 46 46 серебряных знаков 81 81 бронзовый знак
ответ дан 28 окт 2016 в 12:16
Константин Константин
198 8 8 бронзовых знаков
А int в String когда парсим это тоже упаковка?
28 окт 2016 в 12:52

@антонсорокин смотря как реализован метод. К примеру, String.valueOf() принимает int, поэтому никакого unboxing-а не будет.

28 окт 2016 в 12:57

@антонсорокин нет. Это преобразование только между примитивными типами и соответствующими типами-обёртками. Скажем, между int и Integer .

– user181100
28 окт 2016 в 13:02

Для понимания смысла упаковки-распаковки нужно понимать, как работает исполняемая среда и ваша программа на уровне процессора, а так же понимать, что термин ООП это всего лишь надстройка над классическим структурным программированием. Промежуточный байт-код во время выполнения программы преобразуется исполняемой средой в команды конкретного микропроцессора. Есть 3 основных способа хранения, обработки данных и их взаимодействия с программой: процессор-регистр, процессор-стек, и процессор-память. Команды работы с регистрами самые короткие и быстрые, со стеком — короткие, но выполняются на большее число тактов процессора, а команды работы с памятью — самые длинные и медленные. При этом в большинстве случаев простые типы данных хранятся и обрабатываются в регистрах процессора и в стеке, классы и более сложные типы данных хранятся в так называемой «куче» — динамической памяти, которая контролируется исполняемой средой. Механизм кучи достаточно сложен и работа с кучей по времени всегда дольше. Связка стек-регистры максимально эффективна при циклических алгоритмах, где в каждой итерации имеются длинные сложные вычисления, при этом циклический фрагмент кода с регистровой адресацией выполняется в сотни, а то и в десятки тысяч раз быстрее, чем если бы данные хранились в куче:) зато хранение данных в памяти упрощает программу и уменьшает ее размер, а также позволяет использовать все прелести ООП. Упаковку-распаковку придумали для того, чтобы избежать потери производительности программного обеспечения — если критически важна скорость — распаковываем свои данные в простой компактный формат, если скорость не критична — упаковываем в объектный тип и упрощаем программу. Кроме скорости возможна ситуация со значительными затратами ресурсов — к примеру, вам необходимо создать массив из миллиона однобайтовых элементов: что лучше использовать — примитивный однобайтовый тип или упакованный размером в несколько десятков байт?

Отслеживать
ответ дан 24 фев 2017 в 20:33
Андрей aka OberoN Андрей aka OberoN

  • java
  • java-faq
    Важное на Мете
Связанные
Похожие

Подписаться на ленту

Лента вопроса

Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.

Дизайн сайта / логотип © 2024 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2024.2.16.5008

Boxing и unboxing — что быстрее?

image

Заинтересовавшись вопросом скорости работы операций упаковки и распаковки в .NET решил опубликовать свои небольшие и крайне субъективные наблюдения и измерения по этой теме.

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

Теория

Операция упаковки boxing характеризуется выделением памяти в управляемой куче (managed heap) под объект value type и дальнейшее присваивание указателя на этот участок памяти переменной в стеке.

Распаковка unboxing, напротив, выделяет память в стеке выполнения под объект, полученный из управляемой кучи с помощью указателя.

Казалось бы, в обоих случаях выделяется память и особой разницы быть не должно, если бы не одно но- крайне важной деталью является область памяти.

Вспоминая про то, что за выделение памяти в .NET в управляемой куче отвечает сборщик мусора (Garbage Collector) важно отметить, что делает он это нелинейно, ввиду возможной её фрагментации (наличия свободных участков памяти) и поиска необходимого свободного участка требуемого размера.

Update:

Как заметил blanabrother в комментариях, при выделении памяти/копировании значения в managed heap отсутствует процесс поиска свободного участка памяти и её возможная фрагментация ввиду инкриминирующегося указателя и дальнейшей её компактификации с использованием GC. Однако, опираясь на следующие измерения скорости выделения памяти в C++ посмею предположить, что область (тип) памяти является основной причиной такой разницы в производительности.

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

Вывод из этого я делаю такой, что процесс упаковки должен занимать значительно больше времени, чем распаковки, ввиду возможных side effects связанных с GC и медленной скоростью выделения памяти/копирования значения в managed heap.

Практика

Для проверки этого утверждения я набросал 4 небольшие функции: 2 для boxing и 2 для unboxing типов int и struct.

public class BoxingUnboxingBenchmark < private long LoopCount = 1000000; private object BoxedInt = 1; private object BoxedStruct = new ExampleStruct < Amount = 1000, Currency = "RUB" >; [Benchmark] public object BoxingInt() < int unboxed = 1000; for (var i = 0; i < LoopCount; i++) < BoxedInt = (object) unboxed; >return BoxedInt; > [Benchmark] public int UnboxingInt() < int unboxed = 1000; for (var i = 0; i < LoopCount; i++) < unboxed = (int)BoxedInt; >return unboxed; > [Benchmark] public object BoxingStruct() < ExampleStruct unboxed = new ExampleStruct() < Amount = 1000, Currency = "RUB" >; for (var i = 0; i < LoopCount; i++) < BoxedStruct = (object) unboxed; >return BoxedStruct; > [Benchmark] public ExampleStruct UnBoxingStruct() < ExampleStruct unboxed = new ExampleStruct(); for (var i = 0; i < LoopCount; i++) < unboxed = (ExampleStruct) BoxedStruct; >return unboxed; > >

Для замера производительности была использована библиотека BenchmarkDotNet в режиме Release (буду рад если DreamWalker подскажет, каким образом сделать данные замеры более объективными). Далее представлен результат измерений:

image

image

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

Измерения проводились на нескольких машинах с разным кол-вом LoopCount, однако, скорость распаковки из раза в раз превосходила упаковку в 3-8 раз.

Пример IL кода для упаковки int

.method public hidebysig instance object
BoxingInt() cil managed
.custom instance void [BenchmarkDotNet.Core]BenchmarkDotNet.Attributes.BenchmarkAttribute. ctor() = ( 01 00 00 00 )
// Code size 43 (0x2b)
.maxstack 2
.locals init ([0] int32 unboxed,
[1] int32 i)
IL_0000: ldc.i4 0x3e8
IL_0005: stloc.0
IL_0006: ldc.i4.0
IL_0007: stloc.1
IL_0008: br.s IL_001a
IL_000a: ldarg.0
IL_000b: ldloc.0
IL_000c: box [mscorlib]System.Int32
IL_0011: stfld object ConsoleApp1.BoxingUnboxingBenchmark::BoxedInt
IL_0016: ldloc.1
IL_0017: ldc.i4.1
IL_0018: add
IL_0019: stloc.1
IL_001a: ldloc.1
IL_001b: conv.i8
IL_001c: ldarg.0
IL_001d: ldfld int64 ConsoleApp1.BoxingUnboxingBenchmark::LoopCount
IL_0022: blt.s IL_000a
IL_0024: ldarg.0
IL_0025: ldfld object ConsoleApp1.BoxingUnboxingBenchmark::BoxedInt
IL_002a: ret
> // end of method BoxingUnboxingBenchmark::BoxingInt

Пример IL кода для распаковки struct

.method public hidebysig instance valuetype ConsoleApp1.ExampleStruct
UnBoxingStruct() cil managed
.custom instance void [BenchmarkDotNet.Core]BenchmarkDotNet.Attributes.BenchmarkAttribute. ctor() = ( 01 00 00 00 )
// Code size 40 (0x28)
.maxstack 2
.locals init ([0] valuetype ConsoleApp1.ExampleStruct unboxed,
[1] int32 i)
IL_0000: ldloca.s unboxed
IL_0002: initobj ConsoleApp1.ExampleStruct
IL_0008: ldc.i4.0
IL_0009: stloc.1
IL_000a: br.s IL_001c
IL_000c: ldarg.0
IL_000d: ldfld object ConsoleApp1.BoxingUnboxingBenchmark::BoxedStruct
IL_0012: unbox.any ConsoleApp1.ExampleStruct
IL_0017: stloc.0
IL_0018: ldloc.1
IL_0019: ldc.i4.1
IL_001a: add
IL_001b: stloc.1
IL_001c: ldloc.1
IL_001d: conv.i8
IL_001e: ldarg.0
IL_001f: ldfld int64 ConsoleApp1.BoxingUnboxingBenchmark::LoopCount
IL_0024: blt.s IL_000c
IL_0026: ldloc.0
IL_0027: ret
> // end of method BoxingUnboxingBenchmark::UnBoxingStruct

Auto-boxing and Unboxing

Auto-boxing is the process whereby the JS++ compiler will convert primitive data types to their corresponding object wrapper classes. For example, string will be converted to System.String :

import System;
System.String s1 = «abc» ;
// is equivalent to:
System.String s2 = new System.String( «abc» );

Auto-boxing is important because it enables ease-of-access to Standard Library methods. For instance, JS++ introduces a compare method, which is not available from JavaScript. Thus, the JS++ compiler will auto-box the string value with the System.String object wrapper class in order to provide the functionality:

import System;
Comparison compare1 = «abc» .compare( «def» ); // Valid due to auto-boxing
Comparison compare2 = ( new System.String( «abc» )).compare( «def» ); // Equivalent boxed expression

Primitive data types are auto-boxed in JS++ when they are assigned to a variable with the type of the corresponding object wrapper class or when they are passed as an argument to a parameter of a function, method, or operation that expects the type of the corresponding object wrapper class.

Unboxing

Unboxing occurs when an instance of an object wrapper class is converted to its corresponding primitive data type. For example, System.Integer32 being converted to int :

import System;
int x = new System.Integer32(123);

The above conversion is equivalent to calling the valueOf method of System.Integer32 :

import System;
int x = ( new System.Integer32(123)).valueOf();

Object wrapper classes are unboxed in JS++ when they are assigned to a variable with the type of the corresponding primitive type or when they are passed as an argument to a parameter of a function, method, or operation that expects the type of the corresponding primitive data type (including when they are used as an operand to operations wherein the expected operand’s type is of the corresponding primitive data type).

Conversion Table

The following is a conversion table which shows the primitive data types and their corresponding object wrapper classes:

Name Class
bool System.Boolean
string System.String
byte System.UInteger8
signed byte System.Integer8
short System.Integer16
unsigned short System.UInteger16
int System.Integer32
unsigned int System.UInteger32
long System.Integer64
unsigned long System.UInteger64
float System.Float
double System.Double
char System.Character

See Also

  • Primitive Types
  • Variable Declaration
  • Function Declaration

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *