До сих пор эта книга была посвящена тому, как эффективнее использовать паттерны асинхронности. Но мы не рассматривали напрямую, почему асинхронность действительно важна для JS. Самая очевидная и явная причина - это производительность.
Например, если вам нужно выполнить два независимых Ajax-запроса, но вам нужно дождаться их завершения перед выполнением следующей задачи, у вас есть два варианта моделирования этого взаимодействия: последовательный и параллельный.
Вы можете сделать первый запрос и ждать начала второго запроса, пока не завершится первый. Или, как мы уже видели на примере обещаний и генераторов, вы можете выполнить оба запроса "параллельно" и выразить "ворота" для ожидания выполнения обоих запросов, прежде чем двигаться дальше.
Очевидно, что последний вариант, как правило, будет более производительным, чем первый. А лучшая производительность обычно приводит к лучшему пользовательскому опыту
Возможно даже, что асинхронность (чередующийся параллелизм) может улучшить только восприятие производительности, даже если на выполнение программы в целом уходит столько же времени. Восприятие производительности пользователем не менее - если не более! -- важно, чем реальная измеримая производительность.
Теперь мы хотим выйти за рамки локализованных паттернов асинхронности и поговорить о некоторых деталях производительности на уровне программы.
Примечание: Вам могут быть интересны вопросы микропроизводительности, например, что быстрее - a++
или ++a
. Мы рассмотрим такие детали производительности в следующей главе "Бенчмаркинг и тюнинг".
Если у вас есть задачи с интенсивной обработкой данных, но вы не хотите, чтобы они выполнялись в главном потоке (что может замедлить работу браузера/интерфейса), вы могли бы пожелать, чтобы JavaScript работал в многопоточном режиме.
В главе 1 мы подробно рассказывали о том, что JavaScript является однопоточным. И это по-прежнему верно. Но один поток - не единственный способ организовать выполнение программы.
Представьте, что вы разделили свою программу на две части, запустили одну из них в основном потоке пользовательского интерфейса, а другую - в совершенно отдельном потоке.
Какие проблемы возникнут при такой архитектуре?
Например, вы бы хотели знать, означает ли выполнение в отдельном потоке, что он работает параллельно (в системах с несколькими процессорами/ядрами), так что долго выполняющийся процесс во втором потоке не будет блокировать основной поток программы. В противном случае "виртуальные потоки" не принесут особой пользы по сравнению с тем, что мы уже имеем в JS с асинхронным параллелизмом.
Кроме того, вам нужно знать, имеют ли эти две части программы доступ к одной и той же общей области видимости/ресурсам. Если да, то возникают все вопросы, с которыми сталкиваются многопоточные языки (Java, C++ и т. д.), например, необходимость кооперативной или вытесняющей блокировки (мьютексы(взаимное исключение) и т. д.). Это очень много дополнительной работы, и к ней не стоит относиться легкомысленно.
Кроме того, вы бы хотели знать, как эти две части могут "общаться", если они не могут делиться областью видимости/ресурсами.
Всё это - отличные вопросы для рассмотрения, поскольку мы изучаем функцию, добавленную в веб-платформу примерно во времена HTML5, под названием "Web Workers". Это функция браузера (он же хост-окружение), и на самом деле она почти никак не связана с самим языком JS. То есть в JavaScript на данный момент нет никаких функций, поддерживающих многопоточное выполнение.
Но такая среда, как ваш браузер, может легко предоставить несколько экземпляров движка JavaScript, каждый в со своим потоком, и позволить вам запускать разные программы в каждом из потоков. Каждый из этих отдельных потоковых частей вашей программы называется "(Web) Worker". Такой тип параллелизма называется "параллелизмом задач (Task parallelism)", поскольку акцент делается на разделении фрагментов вашей программы для параллельного выполнения.
Из вашей основной JS-программы (или другого Worker) вы создаете Worker следующим образом:
var w1 = new Worker( "http://some.url.1/mycoolworker.js" );
URL должен указывать на местоположение JS-файла (не HTML-страницы!), который предназначен для загрузки в Worker. После этого браузер запустит отдельный поток и позволит этому файлу выполняться в нем как независимой программе.
Примечание: Тип Worker, созданный с помощью такого URL, называется "Dedicated Worker". Но вместо URL-адреса внешнего файла вы можете создать "Inline Worker", предоставив Blob URL-адрес (еще одна возможность HTML5); по сути, это inline-файл, хранящийся в одном (двоичном) значении. Однако Blob'ы выходят за рамки того, что мы будем обсуждать здесь.
Workers не имеют общей области видимости или ресурсов друг с другом или с основной программой - это вывело бы на первый план все кошмары многопоточного программирования - но вместо этого их связывает простой механизм обмена сообщениями о событиях.
Объект w1
Worker является слушателем событий и триггером, который позволяет вам подписываться на события, посылаемые Worker, а также посылать события Worker.
Вот как прослушивать события (если точнее, фиксированное событие "message"
):
w1.addEventListener( "message", function(evt){
// evt.data
} );
И вы можете отправить событие "message"
в Worker:
w1.postMessage( "something cool to say" );
// TODO: check translation Внутри Worker отправка событий полностью симметрична:
// "mycoolworker.js"
addEventListener( "message", function(evt){
// evt.data
} );
postMessage( "a really cool reply" );
Обратите внимание, что выделенный Worker находится в отношениях один-к-одному с программой, которая его создала. То есть событие message
не нуждается в устранении неоднозначности, потому что мы уверены, что оно могло произойти только в результате этой связи "один к одному" - либо от Worker, либо от главной страницы.
Обычно приложение главной страницы создает Worker'ы, но при необходимости Worker может создавать свои собственные дочерние Worker(ы) - так называемые subworkers. Иногда полезно делегировать такие детали некоему "главному" Worker'у, который порождает других Worker'ов для обработки части задачи. К сожалению, на момент написания этой статьи Chrome все еще не поддерживает subworkers, в то время как Firefox поддерживает.
Чтобы немедленно убить Worker из программы, которая его создала, вызовите terminate()
для объекта Worker (как w1
в предыдущих сниппетах). Резкое завершение потока Worker не дает ему возможности закончить свою работу или очистить ресурсы. Это похоже на то, как если бы вы закрыли вкладку браузера, чтобы убить страницу.
Если в браузере есть две или более страниц (или несколько вкладок с одной и той же страницей!), которые пытаются создать Worker из одного и того же URL-адреса файла, то в итоге они окажутся совершенно отдельными Worker'ами. Вскоре мы обсудим способ "совместного использования" Worker'ов.
Примечание: Может показаться, что вредоносная или невежественная JS-программа может легко провести атаку на отказ в обслуживании системы(denial-of-service attack), породив сотни Worker'ов, каждый из которых, казалось бы, имеет свой собственный поток. Хотя это правда, что есть некоторая гарантия того, что Worker окажется в отдельном потоке, эта гарантия не безгранична. Система вольна решать, сколько реальных потоков/процессоров/ядер она действительно хочет создать. Невозможно предсказать или гарантировать, к какому количеству потоков вы получите доступ, хотя многие люди полагают, что их будет не меньше, чем количество доступных процессоров/ядер. Я думаю, что самое безопасное предположение - это наличие хотя бы одного другого потока, кроме основного потока пользовательского интерфейса, но это не более того.
Внутри Worker у вас нет доступа ни к одному из ресурсов основной программы. Это означает, что вы не можете получить доступ ни к глобальным переменным, ни к DOM страницы, ни к другим ресурсам. Помните: это совершенно отдельный поток.
Однако вы можете выполнять сетевые операции (Ajax, WebSockets) и устанавливать таймеры. Также Worker имеет доступ к собственной копии нескольких важных глобальных переменных/функций, включая navigator
, location
, JSON
и applicationCache
.
Вы также можете загружать в Worker дополнительные JS-скрипты, используя importScripts(...)
:
// inside the Worker
importScripts( "foo.js", "bar.js" );
Эти скрипты загружаются синхронно, что означает, что вызов importScripts(...)
будет блокировать выполнение остальной части Worker'а, пока файл(ы) не закончит(ат) загрузку и выполнение.
Примечание: Также обсуждался вопрос о раскрытии API <canvas>
для Workers, что в сочетании с тем, что холсты будут Transferables(передаваемыми) (см. раздел "Передача данных"), позволит Workers выполнять более сложную внепоточную обработку графики, которая может быть полезна для высокопроизводительных игр (WebGL) и других подобных приложений. Хотя такой возможности пока нет ни в одном браузере, она, скорее всего, появится в ближайшем будущем.
Каковы некоторые типичные области применения Web Workers?
- Обработка интенсивных математических вычислений
- Сортировка больших наборов данных
- Операции с данными (сжатие, анализ аудио, манипуляции с пикселями изображений и т. д.)
- Сетевые коммуникации с высоким трафиком
Вы можете заметить общую характеристику большинства этих применений: они требуют передачи большого количества информации через барьер между потоками с помощью механизма событий, возможно, в обоих направлениях.
В первые дни существования Workers сериализация всех данных в строковое значение была единственным вариантом. Помимо снижения скорости при двусторонней сериализации, другим существенным минусом было то, что данные копировались, а это означало удвоение использования памяти (и последующую возню со сборкой мусора).
К счастью, теперь у нас есть несколько лучших вариантов.
Если вы передаете объект, то для копирования/дублирования объекта на другой стороне используется так называемый "Structured Cloning Algorithm"(Алгоритм структурированного клонирования) (https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/The_structured_clone_algorithm). Этот алгоритм довольно сложен и может даже обрабатывать дублирование объектов с циклическими ссылками. При подходе не приходится платить штраф за производительность при работе к строке/из строки, но дублирование памяти все равно имеет место. Он поддерживается в IE10 и выше, а также во всех других основных браузерах.
Еще лучший вариант, особенно для больших наборов данных, - это "Transferable Objects"(переносимые объекты) (http://updates.html5rocks.com/2011/12/Transferable-Objects-Lightning-Fast). При этом происходит передача "прав собственности" на объект, но сами данные не перемещаются. Как только вы передаете объект Worker'у, он становится пустым или недоступным в исходном месте - это устраняет опасность потокового программирования через общую область видимости. Конечно, передача прав собственности может происходить в обоих направлениях.
На самом деле не так уж много нужно сделать, чтобы выбрать Transferable Object; любая структура данных, реализующая интерфейс Transferable (https://developer.mozilla.org/en-US/docs/Web/API/Transferable), будет автоматически передаваться таким образом (поддержка Firefox и Chrome).
Например, типизированные массивы типа Uint8Array
(см. ES6 & Beyond часть этой серии) являются "Transferables". Вот как вы отправите передаваемый объект с помощью postMessage(...)
:
// `foo` - это, например, `Uint8Array`.
postMessage( foo.buffer, [ foo.buffer ] );
Первый параметр - это необработанный буфер, а второй - список того, что нужно передать.
Браузеры, не поддерживающие Transferable Objects, просто переходят к структурированному клонированию, что означает снижение производительности, а не полный отказ от функций.
Если ваш сайт или приложение позволяет загружать несколько вкладок одной и той же страницы (это распространенная функция), вы вполне можете захотеть снизить потребление ресурсов системы, предотвратив дублирование выделенных Worker'ов. Наиболее распространенным ограниченным ресурсом в этом отношении является сетевого соединения сокетами, поскольку браузеры ограничивают количество одновременных подключений к одному хосту. Разумеется, ограничение множества соединений от клиента также снижает требования к ресурсам сервера.
В этом случае весьма полезно создать единый централизованный Worker, с которым могут обмениваться все экземпляры страниц вашего сайта или приложения.
Это называется SharedWorker
, который вы создаете следующим образом (поддержка этого метода ограничена Firefox и Chrome):
var w1 = new SharedWorker( "http://some.url.1/mycoolworker.js" );
Поскольку общий Worker может быть подключен к нескольким экземплярам программ или страницам на вашем сайте, Рабочий должен знать, от какой программы пришло сообщение. Этот уникальный идентификатор называется "port" - вспомните порты сетевых сокетов. Поэтому вызывающая программа должна использовать объект port
Worker'а для связи:
w1.port.addEventListener( "message", handleMessages );
// ..
w1.port.postMessage( "something cool" );
Кроме того, соединение с портом должно быть инициализировано, как:
w1.port.start();
Внутри общего Worker'а необходимо обработать дополнительное событие: "connect"
. Это событие предоставляет порт object
для данного конкретного соединения. Наиболее удобным способом разделения нескольких соединений является использование замыкания (см. раздел Scope & Closures в этой серии) port
, как показано ниже, при этом событие прослушивания и передачи для этого соединения определяется в обработчике события "connect"
:
// внутри общего Worker
addEventListener( "connect", function(evt){
// назначенный порт для данного соединения
var port = evt.ports[0];
port.addEventListener( "message", function(evt){
// ..
port.postMessage( .. );
// ..
} );
// инициализируем соединение с портом
port.start();
} );
Кроме этого различия, общие и выделенные Worker'ы имеют одинаковые возможности и семантику.
Примечание: Shared Workers переживают разрыв соединения с портом, если другие соединения с портом еще живы, в то время как выделенные Worker'ы разрываются при разрыве соединения с инициирующей их программой.
Web Workers очень привлекательны с точки зрения производительности для параллельного выполнения JS-программ. Однако вы можете оказаться в ситуации, когда ваш код должен работать в старых браузерах, в которых отсутствует их поддержка. Поскольку Workers - это API, а не синтаксис, их можно в определенной степени заменить полифилом.
Если браузер не поддерживает Workers, то с точки зрения производительности подделать многопоточность просто невозможно. Обычно считается, что Iframe обеспечивают параллельную среду, но во всех современных браузерах они на самом деле выполняются в том же потоке, что и основная страница, поэтому их недостаточно для имитации параллелизма.
Как мы подробно рассказывали в Главе 1, асинхронность (а не параллельность) в JS обеспечивается очередью цикла событий, поэтому вы можете заставить поддельные Worker'ы быть асинхронными с помощью таймеров (setTimeout(...)
и т. д.). Затем вам просто нужно предоставить полифил для API Worker. Некоторые из них перечислены здесь (https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills#web-workers), но, честно говоря, ни один из них не выглядит хорошо.
Я написал набросок полифила для Worker
здесь (https://gist.github.com/getify/1b26accb1a09aa53ad25). Он базовый, но для простой поддержки Worker
должен подойти, учитывая, что двусторонний обмен сообщениями работает корректно, как и обработка "onerror"
. Возможно, вы сможете расширить его дополнительными функциями, такими как terminate()
или поддельные Shared Workers, по своему усмотрению.
Примечание: Вы не можете подделать синхронную блокировку, поэтому этот полифил просто запрещает использование importScripts(...)
. Другим вариантом мог бы быть разбор и преобразование кода Worker'а (после загрузки Ajax) для обработки перезаписи в некоторую асинхронную форму полифила importScripts(...)
, возможно, с интерфейсом, учитывающим Promise(обещания).
Single instruction, multiple data (SIMD) - это форма "параллелизма данных", в отличие от "параллелизма задач" в Web Workers, потому что акцент делается не на распараллеливании фрагментов программной логики, а скорее на параллельной обработке нескольких кусочков данных.
При использовании SIMD потоки не обеспечивают параллелизм. Вместо этого современные процессоры предоставляют возможность SIMD с помощью "векторов" чисел - подумайте: специализированные по типу массивы - а также инструкций, которые могут работать параллельно со всеми числами; это низкоуровневые операции, использующие параллелизм на уровне инструкций.
Усилия по раскрытию возможностей SIMD в JavaScript в основном возглавляет компания Intel (https://01.org/node/1495), а именно Мохаммад Хагихат (на момент написания этой статьи), в сотрудничестве с командами Firefox и Chrome. SIMD находится на ранней стадии разработки стандартов и имеет все шансы попасть в будущую редакцию JavaScript, скорее всего, в период ES7.
SIMD JavaScript предлагает предоставлять коду JS короткие векторные типы и API, которые в системах с поддержкой SIMD будут отображать операции непосредственно на эквиваленты процессора, с возвратом к непараллельным операциям "шимми" в системах без SIMD.
Преимущества производительности для приложений с большим объемом данных (анализ сигналов, матричные операции на графике и т. д.) при такой параллельной математической обработке совершенно очевидны!
Ранние предложения SIMD API на момент написания этой статьи выглядят следующим образом:
var v1 = SIMD.float32x4( 3.14159, 21.0, 32.3, 55.55 );
var v2 = SIMD.float32x4( 2.1, 3.2, 4.3, 5.4 );
var v3 = SIMD.int32x4( 10, 101, 1001, 10001 );
var v4 = SIMD.int32x4( 10, 20, 30, 40 );
SIMD.float32x4.mul( v1, v2 ); // [ 6.597339, 67.2, 138.89, 299.97 ]
SIMD.int32x4.add( v3, v4 ); // [ 20, 121, 1031, 10041 ]
Здесь показаны два различных типа векторных данных: 32-битные числа с плавающей точкой и 32-битные целые числа. Видно, что размер этих векторов точно соответствует четырем 32-битным элементам, так как это соответствует размерам SIMD-векторов (128 бит), доступных в большинстве современных процессоров. Также возможно, что в будущем мы увидим x8
(или более крупную!) версию этих API.
Помимо mul()
и add()
, вероятно, будет включено множество других операций, таких как sub()
, div()
, abs()
, neg()
, qrt()
, reciprocal()
, reciprocalSqrt()
(арифметика), shuffle()
(перестановка элементов вектора), and()
, or()
, xor()
, not()
(логические), equal()
, greaterThan()
, lessThan()
(сравнение), shiftLeft()
, shiftRightLogical()
, shiftRightArithmetic()
(сдвиги), fromFloat32x4()
, и fromInt32x4()
(преобразования).
Примечание: Существует официальный "prollyfill" (надеющийся, ожидающий, устремленный в будущее полифил) для SIMD-функциональности (https://github.com/johnmccutchan/ecmascript_simd), который иллюстрирует гораздо больше запланированных SIMD-возможностей, чем мы показали в этом разделе.
"asm.js" (http://asmjs.org/) - это обозначение высоко оптимизируемого подмножества языка JavaScript. Тщательно избегая определенных механизмов и шаблонов, которые трудно оптимизировать (сборка мусора, принуждение и т. д.), код в стиле asm.js может быть распознан движком JS и удостоен особого внимания с помощью агрессивных низкоуровневых оптимизаций.
В отличие от других механизмов повышения производительности программ, рассмотренных в этой главе, asm.js - это не обязательно что-то, что должно быть принято в спецификации языка JS. Спецификация asm.js существует (http://asmjs.org/spec/latest/), но она предназначена в основном для отслеживания согласованного набора выводов-кандидатов на оптимизацию, а не для установления требований к движкам JS.
В настоящее время не предлагается никакого нового синтаксиса. Вместо этого asm.js предлагает способы распознавания существующего стандартного синтаксиса JS, который соответствует правилам asm.js, и позволяет движкам реализовывать свои собственные оптимизации соответствующим образом.
Между производителями браузеров существуют некоторые разногласия по поводу того, как именно следует активировать asm.js в программе. В ранних версиях эксперимента с asm.js требовалась прагма "use asm";
(подобно прагме строгого режима "use strict";
), чтобы указать JS-движку на возможности и подсказки для оптимизации asm.js. Другие утверждают, что asm.js должен быть просто набором эвристик, которые движки распознают автоматически, без необходимости делать что-то дополнительное для автора. Это означает, что существующие программы теоретически могут извлечь пользу из оптимизации в стиле asm.js, не делая ничего особенного.
Первое, что нужно понять об оптимизации в asm.js, - это оптимизация типов и принуждения (см. раздел Типы и грамматика в этой серии). Если JS-движок должен отслеживать несколько различных типов значений в переменной через различные операции, чтобы при необходимости обрабатывать принуждения между типами, это много дополнительной работы, которая делает оптимизацию программы неоптимальной.
Примечание: Мы будем использовать код в стиле asm.js для иллюстрации, но имейте в виду, что обычно не предполагается, что вы будете писать такой код вручную. asm.js больше предназначен для компиляции из других инструментов, таких как Emscripten (https://github.com/kripken/emscripten/wiki). Конечно, можно написать свой собственный код asm.js, но это обычно плохая идея, потому что код очень низкого уровня, и управление им может быть очень трудоемким и чреватым ошибками. Тем не менее, могут быть случаи, когда вы захотите вручную подправить свой код в целях оптимизации asm.js.
Есть несколько "трюков", которые можно использовать, чтобы намекнуть asm.js-aware JS-движку, какой тип предполагается использовать для переменных/операций, чтобы он мог пропустить эти шаги по отслеживанию приведения типов(coercion).
Например:
var a = 42;
// ..
var b = a;
В этой программе задание b = a
оставляет открытой дверь для расхождения типов переменных. Однако вместо этого его можно записать так:
var a = 42;
// ..
var b = a | 0;
Здесь мы использовали |
("двоичное ИЛИ") со значением 0
, которое никак не влияет на значение, кроме как убедиться, что оно является 32-битным целым числом. Этот код, запущенный в обычном JS-движке, работает отлично, но при запуске в asm.js-aware JS-движке он может сигнализировать, что b
всегда должно рассматриваться как 32-битное целое число, поэтому отслеживание принуждения можно пропустить.
Аналогично, операция сложения между двумя переменными может быть ограничена более производительным целочисленным сложением (вместо сложения с плавающей точкой):
(a + b) | 0
И опять же, asm.js-aware JS-движок может увидеть эту подсказку и сделать вывод, что операция +
должна быть 32-битным целым сложением, потому что конечный результат всего выражения в любом случае автоматически будет соответствовать 32-битному целому числу.
Одним из самых больших факторов, снижающих производительность в JS, является распределение памяти, сборка мусора и доступ к областям видимости. asm.js предлагает один из способов решения этих проблем - объявить более формализованный "модуль" asm.js - не путайте их с модулями ES6; см. главу ES6 & Beyond этой серии.
Для модуля asm.js необходимо явно передать строго согласованное пространство имен - в спецификации оно называется stdlib
, поскольку должно представлять необходимые стандартные библиотеки - для импорта необходимых символов, а не просто использовать глобальные переменные через лексическую область видимости. В базовом случае объект window
является приемлемым объектом stdlib
для целей модуля asm.js, но вы можете и, возможно, должны создать еще более ограниченный объект.
Вы также должны объявить "heap"(кучу) - это просто модный термин для обозначения зарезервированного места в памяти, где переменные уже могут использоваться без запроса дополнительной памяти или освобождения ранее использованной памяти - и передать ее, чтобы модулю asm.js не нужно было делать ничего, что могло бы вызвать переполнение памяти; он может просто использовать заранее зарезервированное место.
"Heap" - это скорее типизированный ArrayBuffer
, такой как:
var heap = new ArrayBuffer( 0x10000 ); // 64k heap
Используя эти заранее зарезервированные 64k двоичного пространства, модуль asm.js может хранить и извлекать значения в этом буфере без каких-либо штрафов за выделение памяти или сборку мусора. Например, буфер heap
может быть использован внутри модуля для хранения массива 64-битных значений float следующим образом:
var arr = new Float64Array( heap );
Итак, давайте сделаем быстрый и глупый пример модуля в стиле asm.js, чтобы проиллюстрировать, как эти части сочетаются друг с другом. Мы определим foo(...)
, который берет начальное (x
) и конечное (y
) целое число для диапазона, вычисляет все внутренние смежные умножения значений в диапазоне, а затем, наконец, усредняет эти значения вместе:
function fooASM(stdlib,foreign,heap) {
"use asm";
var arr = new stdlib.Int32Array( heap );
function foo(x,y) {
x = x | 0;
y = y | 0;
var i = 0;
var p = 0;
var sum = 0;
var count = ((y|0) - (x|0)) | 0;
// вычислить все внутренние смежные умножения
for (i = x | 0;
(i | 0) < (y | 0);
p = (p + 8) | 0, i = (i + 1) | 0
) {
// сохраняем результат
arr[ p >> 3 ] = (i * (i + 1)) | 0;
}
// вычислить среднее значение всех промежуточных значений
for (i = 0, p = 0;
(i | 0) < (count | 0);
p = (p + 8) | 0, i = (i + 1) | 0
) {
sum = (sum + arr[ p >> 3 ]) | 0;
}
return +(sum / count);
}
return {
foo: foo
};
}
var heap = new ArrayBuffer( 0x1000 );
var foo = fooASM( window, null, heap ).foo;
foo( 10, 20 ); // 233
Примечание: Этот пример asm.js написан вручную в целях иллюстрации, поэтому он не представляет собой тот же код, который был бы получен от инструмента компиляции, нацеленного на asm.js. Однако он демонстрирует типичную природу кода asm.js, особенно подсказки типов и использование буфера heap
для хранения временных переменных.
Первый вызов fooASM(...)
устанавливает наш модуль asm.js с его heap
расположением. В результате мы получаем функцию foo(...)
, которую можем вызывать столько раз, сколько потребуется. Эти вызовы foo(...)
должны быть специальным образом оптимизированы JS-движком с поддержкой asm.js. Важно отметить, что предыдущий код полностью соответствует стандартному JS и будет прекрасно работать (без специальной оптимизации) на движке, не поддерживающем asm.js.
Очевидно, что природа ограничений, которые делают код asm.js настолько оптимизируемым, значительно сокращает возможные области применения такого кода. asm.js не обязательно будет общим набором оптимизаций для любой конкретной JS-программы. Вместо этого он призван обеспечить оптимизированный способ обработки специализированных задач, таких как интенсивные математические операции (например, используемые при обработке графики в играх).
Первые четыре главы этой книги основаны на мысли, что паттерны асинхронного программирования дают вам возможность писать более производительный код, что в целом является очень важным улучшением. Но асинхронное поведение даёт вам только такую возможность, потому что оно все равно привязано к одному потоку цикла событий.
Итак, в этой главе мы рассмотрели несколько механизмов программного уровня для дальнейшего повышения производительности.
Web Workers позволяют запускать JS-файл (он же программа) в отдельном потоке, используя асинхронные события для передачи сообщений между потоками. Они отлично подходят для разгрузки длительных или ресурсоемких задач на другой поток, оставляя основной поток пользовательского интерфейса более отзывчивым.
SIMD предлагает перенести параллельные математические операции на уровне процессора в API JavaScript для высокопроизводительных параллельных операций с данными, таких как обработка чисел в больших наборах данных.
Наконец, asm.js описывает небольшое подмножество JavaScript, которое избегает трудно оптимизируемых частей JS (таких как сборка мусора и приведение типов) и позволяет движку JS распознавать и выполнять такой код с помощью агрессивной оптимизации. asm.js может быть создан вручную, но это чрезвычайно утомительно и чревато ошибками, сродни ручному написанию языка assembly (отсюда и название (сборка)). Вместо этого, основная цель состоит в том, чтобы asm.js стал хорошей мишенью для кросс-компиляции из других высоко оптимизированных языков программирования - например, Emscripten (https://github.com/kripken/emscripten/wiki), транслирующий C/C++ в JavaScript.
Несмотря на то, что в этой главе не рассматривается в явном виде, есть еще более радикальные идеи для JavaScript, которые обсуждаются на ранних этапах, включая приближение к прямой потоковой функциональности (а не просто скрытой за API структур данных). Независимо от того, произойдет ли это в явном виде, или мы просто увидим, как больше параллелизма проникает в JS за кулисы, будущее более оптимизированной производительности на уровне программ в JS выглядит действительно перспективным.