Все больше и больше область применения языка программирования javascript отходит от движения кнопочками в браузере да перекраски фона в сторону сложных и объемных веб-приложений. Уже во всю по миру шагает технология WebGL, позволяющая отображать трехмерные сцены в браузере прямо на языке js, а вместе с ней и усложняются задачи. Производительность пользовательских машин продолжает расти, а вместе с ней и язык обзаводится новыми выразительными средствами, позволяющими ускорять вычисления. И пока WebAssembly где-то там в далеком и светлом будущем, asm.js застрял в болоте и свернул с пути, в ближайшее время изначально как часть es2015, ныне как отдельный стандарт выходит поддержка векторных операций в JS. Все, кому интересно, что такое SIMD и векторные исчисления, как ими пользоваться в js, а так же что дает их использование — прошу под кат. Отступление от автора: у вас не дежавю, эта статья действительно является перезаливом предыдущей. К сожалению, тогда я не приложил достаточно усилий, чтобы получить качественную статью, а тесты не годились ни для чего, как следствие — я получил в корне неверные выводы, на что справедливо получил критику. "Хабр торт и всякое потребье тут не прокатит" обрадовался я, прикинул, что моя статья пока одна из немногих, которые всплывают по запросы "SIMD JS" и было бы неправильно распространять неверную информацию, да еще и низкого качества. В данной статье пытаюсь исправить, ту беспощадно скрыл и удалил как страшный сон и поучительный урок об соотношении времени, затраченного на статью, к её качеству. Предположим, что вам необходимо произвести какую-то простую операцию с каждым элементом из массива. Возьмем сумму с константой C, напишем пример на языке C Догадливые люди могут попытаться немного оптимизировать этот пример, написав что-то вроде: Оптимизация сомнительная, но, на самом деле, сработает. Причиной этого является не то, что мы гораздо реже делаем операцию i = i + 1; (ведь она все равно происходит при взятии индекса), а то, что процессор в этом случае может загружать и выполнять операции с большим захлестыванием друг на друга в конвеере, покуда операции не зависят друг от друга, что позволяет ему производить внутренние операции, вплоть до выполнения этих операций одновременно и пакетной загрузке следующей части массива. Проблема в том, что он может произвести эти оптимизации, а может и не произвести. Мы на это в явном виде повлиять не можем, а накладки все равно сохраняются. И если сумма вычисляется быстро, то какой-нибудь квадратный корень уже выполняется достаточно заметное время чтобы появилось желание заставить процессор выполнять эту операцию одновременно для нескольких чисел. Примерно так подумали много лет назад (в 70-х годах) где-то в недрах Texas Instruments и CDC, сделав первые векторные супер-компьютеры. Однако до них некий Майкл Флинн предложил свою таксономию (классификацию) компьютеров, одной из которых были SIMD. К сожалению, на этом нить истории теряется, но мы тут не для этого собрались. Таким образом, в 70-х годах прошлого столетия появились первые процессоры, позволявшие за раз считать несколько однотипных операций над числами. Позже это перетянули к себе практически все в виде расширенного набора инструкций. Графически классическая архитектура выглядит следующим образом: Нам нужно сложить 4 пары чисел, поэтому мы вынуждены 4 раза вызвать инструкцию add в процессоре. Векторная операция же выглядит следующим образом: Когда нам нужно сложить 4 пары чисел, мы просто вызываем одну инструкцию, которая складывает их за один такт. Небольшая ремарка по поводу "векторная операция" и SIMD-операция. Дело в том, что SIMD — более общее понятие, подразумевающее под собой выполнение в один и тот же момент времени одной или нескольких одинаковых операций над разными данными. В CUDA в каждый момент времени нити выполняют одну и ту же операцию над разными данными, но этих операций выполняется столько, сколько доступно потоков в видеокарте. Векторная арифметика подразумевает то, что выполняется именно одна операция, причем, фактически, она выполняется просто над двумя расширенными данными, составляющими из себя упорядоченно лежащие несколько чисел в одной ячейке. Таким образом, векторные операции входят как подмножество в SIMD-операции, однако в ES2017 говорится именно о векторной арифметике, не знаю, почему они решили так обобщить, далее мы будем считать эти два понятия одним и тем же в рамках этой статьи. Так что, получается, мы можем увеличить производительность своих js приложений в 4 раза? (Протяженно)Нууу не совсем. Но сперва взглянем, как это делается на C (gcc) Скалярный пример Его векторный аналог: (Примеры взяты с ресурса liranuna.com) Первый вариант станет в asm чем-то типа: Векторный его друг будет в ассемблере чем-то типа: На что тут нужно обратить внимание? На то, что операция "сложить" (инструкция addps) действительно одна, когда в первом примере их 4, при этом в первом примере так же много сложебных пересылок данных туда-сюда. Действительно, в сухом остатке инструкций стало меньше. Впрочем, в x86 время выполнения (как и в остальных CISC), не ограничено, в теории можно в микрокод даже хоть модель всего мира запихнуть. Считаться он будет бесконечно, но инструкция то одна! Однако, что гораздо важнее для нас именно сейчас, это регистры xmm0-7 (0-15 в 64-разрядной архитектуре. Однако мы сейчас не на схемотехнике и даже не про ассемблер говорим, такие подробности нам не важны). Это специальные регистры из расширения SSE (англ. Streaming SIMD Extensions, потоковое SIMD-расширение процессора) размерностью 128 бит, каждый из которых может содержать 4 значения с плавающей точкой одинарной точности (32-х разрядные float), ну а так же всевозможные комбинации из знаковых и беззнаковых целых чисел разной разрядностью. Именно над ними и происходят все SIMD-инструкции.  Это достаточно важно, покуда любая операция подразумевает, что вы сначала каким-либо образом данные туда загрузите. Тут и кроется главное ограничение этой технологии — загрузка этих данных обходится не очень дешево, поэтому смешанная арифметика (векторные и скалярные операции вперемешку) дадут только большие издержки на перегоне данных туда-сюда, отсюда все алгоритмы для векторных исчислений оптимизируются таким образом, чтобы минимизировать точки соприкосновения, что не всегда просто. Однако подключить сюда потоковую обработку — производительность еще больше вырастает за счет того, что DDR позволяют получить от 2-х (для ddr3 это аж 8) слов за одно обращение к памяти. Безусловно, процессор и в скалярных вычислениях попытается прибегать к ним, однако если ему сказать об этом явно, ему проще будет понять что и как загружать. Внутри каждая из инструкций выглядит примерно так: Реальный пример не приведу по двум причинам: во-первых, они все очень объемные, а к делу не относятся, во-вторых — никто из крупных компаний, особенно intel, не особо разогнались им делиться, а знакомых схемотехников оттуда у меня нет (это тонкий намек, к слову). Но это и не важно. Важно другое — подобные строчки кода (точнее, описания аппаратуры) есть где-то в недрах ядра x86 процессора intel, доступного нам через микроархитектуру в виде расширенного набора инструкций sse. Вот только в других архитектурах (да, вроде как, и внутри x86, где-то под микроархитектурой, там, где честный RISC) он является частью со-процессора по арифметике с плавающей точкой. Если же для x86 накладок на вызов инструкций со-процессора особо не накладывается из-за возможности оптимизировать его внутри микроархитектуры, то в других процессорах со-процессор зачастую выполняет отдельную последовательность команд и управляется центральным. Таким образом, вызов его команды будет осуществлен по схеме:  Явно больше трех инструкций, которые мы пытаемся сократить. Отсюда логика работы на специфичной аппаратуре состоит в том, что опять же необходимо минимизировать точки соприкосновения скалярной и векторной арифметики, чтобы уменьшить количество пересылок данных и прочих накладок. Например, описание ARM на википедии: Вообще, мы говорим тут об SIMD в js. Так какого черта? ARM и x86 — это все, что у нас есть, когда java-script-разработчиков волновало то, на какой архитектуре будет выполняться их код? Максимум — какой даже не браузер, а движок.  А вот черта с два. Во-первых, а с какого перепуга вообще потянули SIMD в JS? Во-вторых, спорное решение, но их сейчас как грибов. А раз инструмент есть, то понимать как им пользоваться эффективно все таки нужно. Итак, векторные операции скоро появятся в js. Насколько скоро? В данный момент их поддерживает firefox nightly, edge с флагом "экспериментальные возможности" и chrome с флагом при запуске --js-flags="--harmony-simd", т.е. хоть в каком-то виде, но все браузеры. Помимо этого есть полифилл, так что можно использовать уже прямо сейчас. Небольшой пример как использовать SIMD в js-коде: Полный список доступных функций смотрите на MDN. Хочу обратить внимание, что SIMD.Float32x4 не является конструктором и запись new SIMD.Float32x4(0, 0, 0, 0); не является валидной. Не буду расписывать все возможности по использованию, их не очень много, в целом — арифметика да загрузка с выгрузкой данных, еще немного примеров все на том же MDN, сразу перейду к примерам. Обозначения, введенные мной для простоты примеров, такие: Сверткой в математике называют преобразование из одного набора данных в другой. Для программистов сверткой является обход всего массива и выполнения над каждым из его элементов математического преобразования, например, умножения на другое число, сложение с другим массивом или применение оператора Собеля для изображения (он тоже будет, чуть ниже). Скалярный вариант: 
Векторный: Тут и далее: по вертикальной оси данные, по горизонтальной — время. Оранжевый блок — данные, над которыми происходит операция. В коде это пример сверху в явном виде, на js примерно так (часть из алгоритма рассчета дисперсии): Если не ошибаюсь, этот пример идет первым в книге по CUDA, да и вообще является максимально типовым примером для SIMD (причем, в данном случае как раз больше подходит для вычисления на видеокарте, покуда позволяет перейти к логарифмической сложности, чем к векторным операциям). Расскажу многонитьевой алгоритм, который не сложно адаптировать под векторный: сперва каждый четный (включая нулевой) суммируется с нечетным, следующим за ним. Затем нулевой (в котором теперь лежит сумма a[0] + a[1]) суммируется с вторым четным (a[2], который равен a[2] + a[3]) и так далее. На следующем шаге суммируются 0, 4, 8, ..., затем 0, 8, 16 и так далее по степеням двойки, пока массив не закончится. В случае с векторными операциями можно суммировать первые 4 значения со вторыми 4-мя значениями, в остальном — все так же, т.е. a[0-3] = a[0-3] + a[4-7]; a[8-11] = a[8-11] + a[12-15];  Графически: Скалярный вариант 
Векторный: 
Прирост скорости ожидается достаточно большой. Мой вариант на js: Данный пример имеет ограничение: размер массива должен быть одновременно степенью двойки и кратен 4. В принципе, это не так сложно исправить, но оставим на домашнее задание. Есть смысл полагать, что матричные операции для квадратных матриц размера 4 (а с ними и векторов такой же размерности. Математических векторов, а не вычислительлных) SIMD дадут заметный прирост. Что такое перемножение и как выглядит классический вариант, а так же графически — я не покажу (графически выглядит ненаглядно, классический вариант — использование математического определения перемножения матриц в лоб, его можно посмотреть на википедии), векторный код на js у меня вышел такой:  Сложновато. Замечу, что после просмотра кода, в который компилируется c-шный вариант, не сильно расстроился недостатком _mm_set1_ps, покуда внутри оно компилируется в 4 отдельных команды загрузки, так что потеря не велика. Википедия. Является примером одной из типовых задач, которые должны хорошо ложиться на SIMD, покуда является внутри сверткой над изображением. Картинок, кода и алгоритма не будет, однако замечу, что в тесте я несколько смухлевал. Дело в том, что оператор Собеля оптимизируется гораздо больше и сильнее при использовании SIMD, однако сначала нужно преобразовать данные к другому виду, да и матан там не простой. Однако даже простое применение дало преимущество. Что касается честного преобразования — хоть jpeg и является дискретно-косинусным преобразованием, внутри он не так далеко ушел от свертки оператором Собеля, а именно за счет SSE (и старших) расширений процессора мы можем смотреть видео в высоком качестве на процессоре (и накладывать гораздо более сложные эффекты используя видеокарту), так что данный алгоритм является прямым назначением SIMD-инструкций в процессоре, в том числе, в js. Тест руками можно запустить тут. Не забудьте убедиться, что ваш браузер поддерживает SIMD. Исходный код, не забываем о кнопочке fork, если есть идеи, как улучшить и оптимизировать. К тестам подошел серьезно: каждый тест группируется в 4 запуска, один из которых "холостой", предназначенный на прогрев кеша и чтобы браузер смог скомпилировать то, что он считает нужным, в бинарный код. Три учитываются при рассчете. Затем каждая из этих групп запускается по n раз, порядок выбирается случайным образом, затем считается математическое ожидание и дисперсия (черная полоска на каждом из графиков).  У себя я запускал 500 раз каждую из групп, что позволило исключить такие факторы как сборку мусора, переключение поток, задумавшегося касперского и прочие обновления windows 10, и отражает близкие к действительности числа. К сожалению, на js получить более точные выводы невозможно по той причине, что мы никак не можем заставить планировщик задач системы не переключать задачи и прочие вещи, а получить количество тиков, отведенных исключительно под js, так же нет инструментов (есть в виде профайлера, но он из без того слишком адский. Перепроверить в нем — ссылка на тесты вверху есть, можете проинспектировать). Результаты на моем компьютере (core i5 6400m, firefox nightly 51.0a): 
 Как видно из импровизированного сравнения на производительность, при хоть чуть более-менее грамотном использовании технологии она дает преимущество в 20-40%. В специфичных задачах можно выжать и гораздо большую производительность, как следствие — технология стоит того, чтобы начинать задумываться об её использовании. К сожалению, векторное программирование достаточно сложное, специалистов по нему мало, тем более в мире js, поэтому все и каждый не ринется. Однако если ваш проект включает в себя сложные вычисления — чувствительный прирост скорости лишним не будет и может сэкономить много ресурсов и времени на решение других задач. Сейчас доступен полифилл и нет причин думать, что API для работы с SIMD инструкциями в js поменяется, покуда стандарт завершен на 90%, а оставшиеся 10%, по секрету, это размышления о том, как бы переопределить операторы типа "+" и "*". В js перегрузки операторов нет и не вижу причин тащить их сюда, так что, скорее всего, в прямом виде войдет в стандарт, который после принятия в ближайшем же релизе будет доступен в firefox и edge. Google в Chrome с ним не спешат по какой-то причине, таким образом, если вы сейчас (на момент написания статьи, в августе 2016 года) начинаете свое новое приложение на JS, в котором будет множество вычислений, стоит сразу иметь эту библиотеку ввиду и писать код если не сразу на ней (с полифиллом), то, по крайней мере, с оглядкой на вероятную миграцию и предусмотреть возможность переписывания, чтобы в будущем не потерять много времени на это. Конечно рост производительности наблюдается, однако мне не очень понятен выбор технологии и реализация стандарта. Она чуть более, чем полностью оптимизирована только под x86, на любой другой архитектуре и размер вектора может отличаться, и набор инструкций (очень распространена инструкция вида a + b * c, особенно в всевозможных ДСП). Понятное дело, что какой-нибудь 66AK2x от TI (цифровой сигнальный процессор) никто в здравом уме не будет программировать на javascript, однако в супер-высокоуровневом платформонезависимом языке странно видеть платформозависимые инструкции. (Об этом говорил  Akon32,   Мое лично мнение: гораздо дальновиднее, при этом позволяющее компилятору делать тоже самое, что и в данном стандарте, является MPi (ну или OpenMP). Как бы, к примеру, это выглядело: Собственно, плагиат с готового стандарта OpenMP. Это позволит движку самому выбрать наиболее оптимальный вариант векторизации, оперировать не только 128-ми битными векторами, да и сделает код платформонезависимым. Можно ли сделать это автоматически во время JIT? Не всегда удается, а возможность указать вручную была бы удобной. P.S. В прошлой статье вместо технологии все начали обсуждать математику и сложные вычисления на javascript. Подходящий ли язык для этого или нет, хорошо это или плохо, однако все больше и больше задач на js требуют больших математических вычислений, а технология, которая позволяет их ускорить, играет только на руку, нежели вредит. Прошу воздержаться от обсуждения сложных вычислений на js как таковых. Database administrator