Как стать автором
Обновить

Комментарии 27

Интересно, а зачем с точки зрения дизайна языка вообще ввели отдельно null и отдельно undefined? Не проще ли было обойтись каким-то одним? Для реализации абстракции опционалов вполне достаточно одного.

В этом есть какой-то глубокий (основанный на какой-то умной околоматематической теории) смысл, или просто так сложилось/не сложилось?

null планировался для интеропа с Java. Кстати, typeof null == 'object'.

Согласен, скорее всего, Null - дань другим языкам, популярным во второй половине 90-х.

Ошибка дизайна языка, когда планировалось делать его схожим с Java? Можно обнаружить в старых спеках языка кучу заразервированных слов из Java/C++ на всякий случай. На практике же undefined и null достаточно сильно отличаются и это важно учитывать. Смысл несут они разный

В первых версиях JavaScript не было исключений. Поэтому доступ к несуществующему полю должен был вернуть хоть какое-то значение. null для этой цели не подходил потому, что требовалось, чтобы он вел себя максимально похоже на Java. Поэтому ввели значение-заглушку. undefined тогда даже не было ключевым словом - это было буквально имя переменной, которой договорились ничего не присваивать, чтобы при доступе к ней всегда возвращалось это самое значение-заглушку. В спеке ECMAScript это до сих пор не ключевое слово, а именно глобальная переменная - просто ее сделали доступной только для чтения.

Хороший материал , спасибо

Круто, люблю иногда покопаться в таких дебрях, спасибо!

З.ы.на основании статьи - так почему все-таки 0.1+0.2 !== 0.3 ? ?

Статья было про типы данных. Бинарную арифметику я затрагивал. Но, если уже поднимать этот вопрос, быстрый ответ, потому что бинарная сумма 0.1+0.2 = 0.30000000000000004. Почему так происходит, думаю, в сети найдется много материала на этот счет. Если коротко, при суммировании двух чисел с плавающей точкой их мантиссы суммируются бинарным способом и иногда, вместо 52-битного слова, может получиться 53-битное. Соответственно, для приведения к 52 битам, результат нужно округлить. Более того, если у двух чисел разные экспоненты, вторая сдвигается относительно первой, что тоже может привести к переполнению разряда, тогда их тоже надо округлять. Отсюда и неточности в вычислении.

Что-то где-то автор попутал. Сделать нестандартный double и потерять в 50..10 раз в скорости даже по сравнению с FPU?! Там наверняка стандартный float double, а представление в виде 2x32 сугубо издержки поддержки 32-битных платформ. То есть это абстракция, уровня аллокатора памяти/объектов и отладчика, а не что-то реальное.

Не путаю, так уж решили разработчики V8. Сделано это было еще в 2009-м Эриком Корри для ускорения бинарных операций на архитектуре ARM. Суть как раз в том, чтобы делать их в обход FPU или эмуляторов (см коммит https://codereview.chromium.org/119241). Видимо, в 2009 на ARM это было актуально. Сам я в эту сторону далеко не копал.

Все правильно. Это сугубо представление для того, чтобы упростить поддержку на некоторых платформах. Но это не значит, что там свой плавающий тип. На подавляющем числе платформ используется аппаратная плавающая точка, и только кое где эмулируется. Причем речь идёт именно об описании распределения памяти, так как по стандарту там слишком много неопределенности в C. Приходится изобретать вот такие хитрые представления переменных в памяти, но по факту практически везде там лежит и используется стандартный float double. Но, опять же, есть возможность перехода в любой момент на эмуляцию малой кровью. Включая всякую нестандартную эмуляцию, которая за счёт более слабой поддержки стандарта в разы быстрее может быть (в пределах своего приложения вы просто знаете о сайд-эффектах и обходите их, а в случае передачи куда-то сторонним приложениями проводите нормализацию - стандартный трюк). Это несколько я это всё понимаю.

На самом деле SMI это далеко не только то что это целое представление и оно быстрее FPU. Чтобы этот вопрос отпал сам собой нужно понять как работают идентификаторы в v8. Они всегда работают как указатель на значение в куче. Но каждый раз создавать новое значение в куче / следить за его жизнью / обращаться через особую структуру HeapNumber -- это дорого. В то время как SMI это буквально число вставленное вместо указателя. Те 1ый бит в значении идентификатора говорит: Я не SMI а указатель, иди по нему там реальное значение. А SMI - хей тут уже лежит число, бери его и делай что хочешь. Чем и обусловлено ограничение в 31 бит ибо 32-ой бит это указатель на то что это число. Думаю с этого момента понятно чем обсулавливается кратная разница по перфу

Работа с числами с плавающей точкой транслируется в обычные AVX инструкции на современном x86.

И для этого числа должны быть представлены в стандартном формате.

А когда это у нас мдн стал официальной документацией/спецификацией, если стандартизацией языка занимается организация tc39.

https://tc39.es/ecma262/

Вопрос терминологии. Я, все таки, считаю, что "документация" и "спецификация" - не одно и то же. Спецификацию пишут до имплементации продукта/фичи в жизнь. Документация же - инструкция к тому, что получилось в итоге. Если говорить про спецификацию, да, первоисточник tc39, у меня есть отсылки к ней. Но вот в плане документации, более полной и "централизованной", чем MDN, я не знаю.

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

А вы я думаю и сами разберетесь в обход этих документов, если вам удалось предоставить такой хороший материал:)

Конечно, всё это очень интересно, но в чём практический выхлоп? Я не говорю про то, что писать быстрый код на JS не следует. А скорее про то, как конкретно именно эта информация может помочь писать быстрый код? Скажем, знание о формах объектов (object shapes) или нюансах GC, можно использовать в реальном коде.

Единственное что приходит в голову, это то, что лучше использовать гомогенные массивы со Smi, чтобы они имели оптимальный тип PACKED_*_SMI.

Вопрос риторический. Зачем знать устройство автомобиля и принципы работы основных узлов и агрегатов? Чтобы ехать, достаточно же знать, где руль и педали.

"Практический выхлоп" в том, чтобы понимать механизмы, которые работаю под капотом JS. Опытный водитель, зная, как устроен его автомобиль, в критической ситуации может не допустить поломку или починить, если что-то случилось в дороге. Так же и опытный разработчик, зная, как устроен движок/интерпретатор/компилятор, может, например, найти и устранить утечку памяти и, в целом, не будет заблуждаться на счет элементарных вещей. Например, некоторые разработчики (особенно пришедшие из Backend или под их вилянием) полагают, что использовать 1|0 вместо true|false - оптимальнее. Хотя, как мы видим, в JS это не совсем так, хоть разница в производительности и не велика. И таких кейсов можно набрать много. Дьявол, как говорится, кроется в деталях.

Аналогии очень редко применимы, и сравнивать движок JS с машиной не совсем корректно.

Например, некоторые разработчики (особенно пришедшие из Backend или под их вилянием) полагают, что использовать 1|0 вместо true|false - оптимальнее. Хотя, как мы видим, в JS это не совсем так, хоть разница в производительности и не велика.

Почему же не так? Использование 1/0 в качестве значений для полей объекта быстрее чем true/false просто потому что они хранятся напрямую в словаре свойств объекта, а не как ссылки на базовые true/false объекты. Да, у нас будет некий оверхед на деоптимизационную ловушку для SMI, но в целом 1/0 будет выигрывать у true/false.

Да и в общем, на уровне ассемблера, работать с числами 1/0 компилятор умеет эффективнее чем с указателями и может использовать больше оптимизаций. Так что в большинстве сценариев 1/0 будет быстрее чем true/false.

Занятно, что именно в этом кейсе знание о том, что true-false - ссылочные значения, а не просто числа в памяти, может подсказать ответ. Но странно, что вы сделали противоположный вывод об оптимизации :)

Вопрос риторический

Не совсем. В изначальном комментарии хотел отметить то, что набор практических советов был бы полезнее, чем просто описание небольшого фрагмента движка v8. Если не знать еще множества нюансов (как в примере выше), то использовать информацию о том, как хранятся примитивные типы весьма сложно.

Вот здесь не соглашусь. Что быстрее, вопрос очень спорный, истину может показать только эмпирический замер. Но, самое главное, производительность сценария - это не только скорость бинарных вычислений и кол-во переходов по указателям. На практике есть еще память, связанные процессы и GC, в конце концов. Как бы там ни было, true/false лежат в памяти всегда, вне зависимости от того, будут они использованы в коде или нет, их наличие не надо проверять, их не требуется записывать, они не обрабатываются GC.

Даже интересно стало замерить цифры. Думаю, есть смысл провести эксперимент и посмотреть.

Как бы там ни было, true/false лежат в памяти всегда, вне зависимости от того, будут они использованы в коде или нет, их наличие не надо проверять, их не требуется записывать, они не обрабатываются GC.

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

В любом случае, такие микро-оптимизации нужно проводить ну очень редко. И в целом я соглашусь, что заменять семантически более подходящие true/false на 1/0 ради выигрыша сотых долей процента (и то в лучшем случае) - не стоит.

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

Возможно я неправильно понял, но разве выделять на объявление каждой переменной область памяти на хранение ссылки на константное значение не столь же расточительно, сколько и выделение памяти на хранение самого этого константного значения? (для которого, кстати, по идее достаточно лишь одного байта, вместо четырех для ссылки)

Автор материала, далеко не всегда перепроверял себя, на соответствие заявленного им, официальному источнику.

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

Судите сами:

Исчерпывающая информация есть в официальной документации

Автор материала не знает, что Mozilla Developer Network никогда не являлась официальной документацией. Единственным официальным источником, с 1998 года является сайт спецификации ECMAScript.

Всех нас учили, что в JavaScript есть примитивные и ссылочные типы данных. 

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

Даже если учесть общепринятый жаргон, относительно примитивных типов, то и в его рамках - никаких примитивных типов в языке JavaScript так же не существует. Потому как поведение тех типов, которые по умолчанию возвращают Primitive Value не укладываются в типичный набор критериев распространяемых в общем случае, на примитивные типы.

Главный из которых - строгая детерминированность поведения типа, вне зависимости от особенностей его использования.

То есть примитивный тип - это тип, который должен себя вести везде одинаково. При всех обстоятельствах. Чего в JavaScript нет.

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

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

Примитивные типы данных,

это иммутабельные (не изменяемые) значения, хранимые в памяти и представленные в структурах языка на низком уровне. Собственно, в JavaScript к примитивным типам относится все, кроме Object, а именно: Null, Undefined, Boolean, Number, BigInt, String, Symbol

То что написал автор - не имеет никакого отношения к официальной спецификации. Согласно которой, никаких примитивных типов в языке JavaScript не существует. Такого термина просто нет в официальной спецификации. Не говоря уже о том, что поведение выше заявленных типов, не отвечает общепринятому определению примитивных типов.

Ссылочные типы данных, они же - объекты, это области памяти неопределенного размера, и доступные по идентификатору (ссылке на эту область памяти). В JavaScript, есть только один такой тип - Object

Согласно официальной спецификации абсолютно все типы в языке JavaScript являются ссылочными:

Algorithm steps may declare named aliases for any value using the form “Let x be someValue”. These aliases are reference-like in that both x and someValue refer to the same underlying data and modifications to either are visible to both. Algorithm steps that want to avoid this reference-like behaviour should explicitly make a copy of the right-hand side: “Let x be a copy of someValue” creates a shallow copy of someValue.

Это фундаментальная основа языка JavaScript которая закреплена на уровне главы формирующей операции используемый спецификацией.

Все алгоритмы спецификации, если явным образом не указано другого, описывают Expression или Statement в рамках заявленного в главе Algorithm Conventions алгоритма - а именно ссылочного типа.

Если быть максимальным занудой, то в академической среде, где работа с типами определяется как: By Value, By Reference и By Shared, работа со всеми типами в JavaScript отвечает термину By Shared. Что по сути своей является тем же ссылочным типом, и отличается только способом передачи в целевой индентификатор самой ссылки.

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

Исключением являются типы связанные с TypedArray.

 Object - единственный мутабельный (изменяемый) тип данных в JavaScript. 

Object как тип, ничем не отличается от прочих типов языка JavaScript с точки зрения его описания. Но отличается тем что возвращается по умолчанию при запросе к идентификатору который с ним связан. В случае Object - возвращается структура типа. В случае типов, неправильно названных автором примитивными, по умолчанию возвращается primitive value. Чего не достаточно чтобы считать тип примитивным.

Производя какие-либо манипуляции с объектом, меняется значение непосредственно в области памяти

Ничего подобного не происходит. Это фантазии автора материала. Структура Object ничем не отличается по своему поведению от структуры Record в рамках которой описываются идентификаторы которые связывают с типами вида String, Number etc...

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

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

Официальная спецификации - никак не управляет тем что будет с обьектом или еще чем-то.

Официальная спецификация никаким образом не заявляет поведение Garbage Collector-а или любой другой машинерии про то же самое.

Это все лежит на плечах, конкретного RunTime, работа которого ВООБЩЕ не зависит от того, будет ли уничтожен какой-то обьект на который нет ссылки или не будет.

Больше того, если уж мы говорим о V8, то в его рамках, Garbage Collector, сто раз подумает прежде чем удалять что -то из памяти даже если на это что-то 100500 раз нет никаких ссылок. По одной простой причине - если память есть и ее достаточно, то последнее что нужно делать, это заниматься чисткой чего-то на что нет ссылок. Особенно в контексте того, что то, что потеряло ссылку, буквально завтра может ее снова обрести. А процесс заведения нового обьекта не в пример дороже чем работа Garbage Collecotr-а.

Согласно документация, тип Number в JavaScript является 64-битным числом двойной точности в соответствии со стандартом IEEE 754

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

Одновременно с чем, оно может быть 32-битовым целым со знаком в рамках бинарных операций например and. При этом уничтожая всю метаинформацию о том, какая дробная часть (мантисса) было до этого.

Но и это далеко не все. В рамках спецификации, для сдвигов числовой литерал в выражении может становиться 32 битовым целым БЕЗ знака.

И все это, подчеркиваю, в рамках официальной спецификации.
(Передаем привет секте примитивных типов в JavaScript)

Посмотрим на это число в V8 в режиме дебага. Для этого воспользуемся системным хелпером движка %DebugPrint

DebugPrint: Smi: 0x1 (1)

Выяглядит вполне ожидаемо. Мы видим простое значение 0x1 с неким типом Smi. Но разве тут не должен быть тип Number, как говорится в спецификации ECMAScript? К сожалению, найти ответы на подобные вопросы в официальной документации движка не представляется возможным, 

Еще как представляется.

То, что увидел автор материала, является частью поведения оптимизирующего алгоритма современного RunTime.

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

Ну люди добрые, Вы же программисты. Если у Вас ссылка на структуру представляется 32 битами, при этом эта структура должна описать число 1, которое легко помещается в те самые 32 бита, зачем создавать структуру?

В современном V8 эта оптимизация называется Pointer Compression. Вот ссылка на статью, в официальном блоге инженеров V8, где подробно описывается это поведение.

Все последующие рассуждения автора про SMI в V8, касаются частного случая применения алгоритма оптимизации Pointer Compression. Который не обязательно будет включен для любой HOST среды. Например в Node, где используется ровно тот-же самый RunTime V8, оптимизация Pointer Compression отключена.

Как следствие, поведение в Node, связанное с обработкой численных литералов, отличается от того же поведения в браузере. Например Node может работать с 64 целыми числами если того позволяет архитектура. Чего не может делать Google Chrome. При этом Google Chrome, благодаря тому самому Pointer Compression становится в полтора раза эффективнее Node в случае обработки массивов данных, где каждый индекс укладывается в 31бит со знаком.

В чем легко убедиться используя тот же самый DebugPrint в Node и в браузере для одного и того же числа в пределах 31 бита.

Вместо ИГОГО:

Автору, как мне кажется, нужно переосмыслить весь написанный им материал и исправить допущенные им неточности. Иначе этот материал представляет из себя больше дезинформацию, нежели попытку просветить необразованное JS комьюнити.

Не понял, при чем тут спецификация? Я писал про реализацию на конкретном движке, а не про спецификацию.

Судите сами:

Автор материала не знает, что Mozilla Developer Network никогда не являлась официальной документацией. Единственным официальным источником, с 1998 года является сайт спецификации ECMAScript.

Помоему, вы путаете документацию и спецификацию. Я уже отвечал на этот вопрос и в статье есть отсылки и к спецификации. Спецификация - документ, который пишет еще до каких либо разработок, а документация - после. Можно провести грубую аналогию с ГОСТом и инструкцией к конкретному продукту. Разве это один и тот же документ?

Дабы заркыть недопонимание, изменил формулировку в статье.

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

Может и должен. Однако, учат, в том числе в ВУЗах. Как учить языку, по спецификации, или по каким-то другим источникам - вопрос отдельный, и точно лежит за рамками этой дискуссии.

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

Где в статье говорится, что реализация не соответствует спецификации? Покажите, я исправлю. Наоборот, я говорю следующее

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

Все последующие ваши цитаты и комментарии к ним, опять же, про спецификацию, которая в статье не рассматривается, а EventLoop и GarbageCollector вообще не упоминаются. При чем тут это всё?




Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории