Noveo

Наш блог Повышаем производительность JavaScript с помощью GPU

Повышаем производительность JavaScript с помощью GPU

Ускорить приложение в 10 раз? Легко! Для этого нам понадобится всего лишь библиотека GPU.js и перевод поста Чамиры Дуланга (Chameera Dulanga)!

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

 

Но задумывались ли вы когда-нибудь о том, что для этих целей можно в том числе использовать и мощность GPU?

Using GPU to improve JS performance

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

Что такое GPU.js и зачем она нужна?

Если коротко, то GPU.js — библиотека ускорения JavaScript, используемая для выполнения стандартных вычислений на GPU посредством JS. Есть поддержка браузеров, Node.js и TypeScript.

 

Помимо повышения производительности, есть еще ряд причин, по которым я рекомендую обратиться к GPU.js:

  • За основу берется JS, поэтому можно смело использовать его синтаксис.
  • GPU.js автоматически преобразует и компилирует JavaScript в язык шейдеров.
  • Если на устройстве нет графического процессора, операции будут продолжать выполняться на обычном движке JavaScript — словом, использовать GPU.js вам ничто не препятствует.
  • GPU.js может применяться и для параллельных вычислений. К тому же у вас есть возможность асинхронного выполнения массовых вычислений как на CPU, так и на GPU — одновременно.

 

Учитывая все сказанное выше, я не вижу поводов не использовать GPU.js — а потому предлагаю вам рассмотреть, как начать работу с этой библиотекой.

Как установить GPU.js?

Установка GPU.js во многом напоминает установку любой другой библиотеки JavaScript.

 

Для проектов на Node.js

 

npm install gpu.js --save
or
yarn add gpu.js
import { GPU } from ('gpu.js')
--- or ---
const { GPU } = require('gpu.js')
--- or ---
import { GPU } from 'gpu.js'; // Используйте это для TypeScript
const gpu = new GPU();

Для браузеров

 

Скачайте GPU.js локально или же воспользуйтесь CDN (Content Distribution Network — сеть дистрибуции контента).

<script src="dist/gpu-browser.min.js"></script>
--- or ---
<script
 src="https://unpkg.com/gpu.js@latest/dist/gpu- browser.min.js">
</script>
<script
 rc="https://cdn.jsdelivr.net/npm/gpu.js@latest/dist/gpu-browser.min.js">
</script>
<script>
const gpu = new GPU();
...
</script>

NB: Если вы на Linux, не забудьте убедиться в том, что были установлены корректные файлы. Для этого запустите:

sudo apt install mesa-common-dev libxi-dev

Итак, это все, что вам нужно знать об установке и импорте GPU.js. Теперь ваши приложения наконец-то смогут пользоваться возможностями графического процессора.

 

Однако не стоит останавливаться на достигнутом: очень советую вам разобраться в основных функциях и принципах работы GPU.js — и заняться этим предлагаю прямо сейчас.

 

Совет: Создавайте и делитесь независимыми компонентами с помощью Bit

 

Bit — это сверхрасширяемый инструмент, который позволяет создавать действительно модульные приложения с компонентами, разрабатываемыми, изменяемыми и поддерживаемыми вне зависимости друг от друга.

 

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

Bit
Отдельные компоненты Material UI находятся в общем доступе на Bit.dev

Создаем функции

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

const exampleKernel = gpu.createKernel(function() {
   ...
}, settings);

В примере выше показана общая структура функции GPU.js. Как видите, я дал ей название exampleKernel и использовал createKernel для выполнения вычислений посредством GPU.

Также нам в обязательном порядке требуется задать размер выходных данных; в приведенном примере это было сделано с помощью параметра settings:

const settings = {
   output: [100]
};

Выход ядерной функции может быть одно-, дву- и трехмерным — другими словами, включать в себя вплоть до трех потоков. Получить доступ к этим потокам можно с помощью команды this.thread:

  • 1D: [длина] – value[this.thread.x]
  • 2D: [ширина, длина] – value[this.thread.y][this.thread.x]
  • 3D: [ширина, длина, глубина] – value[this.thread.z][this.thread.y][this.thread.x]

 

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

Поддерживаемые переменные ядра

Number (число)

 

В функциях GPU.js можно использовать любые числа integer и float (соответственно, целые числа и числа с плавающей запятой).

const exampleKernel = gpu.createKernel(function() {
const number1 = 10;
const number2 = 0.10;
return number1 + number2;
}, settings);

Boolean

 

Как и в JavaScript, в GPU.js поддерживаются Boolean-значения.

const kernel = gpu.createKernel(function() {
 const bool = true;
 if (bool) {
   return 1;
 }else{
   return 0;
 }
},settings);

Array (массив)

 

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

const exampleKernel = gpu.createKernel(function() {
const array1 = [0.01, 1, 0.1, 10];
return array1;
}, settings);

Function (функция)

 

Помимо этого, GPU.js позволяет использовать приватные функции внутри ядерных.

const exampleKernel = gpu.createKernel(function() {
 function privateFunction() {
   return [0.01, 1, 0.1, 10];
 }
 return privateFunction();
}, settings);

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

В дополнение к вышеперечисленным типам переменных есть несколько типов входных данных, которые вы можете передавать в ядерные функции.

 

Number (число)

 

По аналогии с объявлением переменных в ядерные функции можно передавать числа integer и float:

const exampleKernel = gpu.createKernel(function(x) {
return x;
}, settings);
exampleKernel(25);

Одно-, дву-, трехмерные массивы чисел

 

В ядра GPU.js можно передавать массивы следующих типов: Array,Float32Array,Int16Array, Int8Array, Uint16Array, uInt8Array.

const exampleKernel = gpu.createKernel(function(x) {
return x;
}, settings);
exampleKernel([1, 2, 3]);

В ядерных функциях также допускается использование заранее сглаженных дву- и трехмерных массивов. Такой подход позволяет значительно ускорить выгрузку — нужно лишь не забыть про input:

const { input } = require('gpu.js');
const value = input(flattenedArray, [width, height, depth]);

HTML-изображения

 

В отличие от традиционного JS, в GPU.js существует возможность передачи одного или нескольких HTML-изображений в ядерную функцию в виде массива.

//Одно изображение
const kernel = gpu.createKernel(function(image) {
   ...
})
 .setGraphical(true)
 .setOutput([100, 100]);

const image = document.createElement('img');
image.src = 'image1.png';
image.onload = () => {
 kernel(image); 
 document.getElementsByTagName('body')[0].appendChild(kernel.canvas);
};

//Несколько изображений
const kernel = gpu.createKernel(function(image) {
   const pixel = image[this.thread.z][this.thread.y][this.thread.x];
   this.color(pixel[0], pixel[1], pixel[2], pixel[3]);
})
 .setGraphical(true)
 .setOutput([100, 100]);

const image1 = document.createElement('img');
image1.src = 'image1.png';
image1.onload = onload;
....
//добавляем еще два изображения
....
const totalImages = 3;
let loadedImages = 0;
function onload() {
 loadedImages++;
 if (loadedImages === totalImages) {
   kernel([image1, image2, image3]);
    document.getElementsByTagName('body')[0].appendChild(kernel.canvas);
 }
};

Примеры выше — всего лишь некоторые из множества вариантов экспериментирования с GPU.js; больше интересных деталей для проведения самостоятельных опытов уже ждут вас в официальной документации на GitHub.

 

Теперь же, вооружившись знаниями о нескольких возможных конфигурациях, давайте напишем функцию с использованием GPU.js и посмотрим на ее производительность.

Ваша первая функция в GPU.js

Объединив все освещенные ранее моменты, я написал небольшое приложение на Angular, чтобы сравнить скорость вычислений на GPU и CPU в случае перемножения двух массивов в тысячу элементов.

 

Шаг 1. Функция для генерации массивов чисел с 1000 элементов

 

Генерируем двумерный массив с тысячей чисел для каждого элемента — будем использовать его для последующих вычислений.

generateMatrices() {
this.matrices = [[], []];
for (let y = 0; y < this.matrixSize; y++) {
 this.matrices[0].push([])
 this.matrices[1].push([])
 for (let x = 0; x < this.matrixSize; x++) {
  const value1 = parseInt((Math.random() * 10).toString())
  const value2 = parseInt((Math.random() * 10).toString())
  this.matrices[0][y].push(value1)
  this.matrices[1][y].push(value2)
 }
}
}

Шаг 2. Ядерная функция

 

Это самая важная функция в нашем приложении, так как все вычисления на GPU выполняются именно внутри нее. В примере ниже функция multiplyMatrix на входе получает два массива чисел и размер матрицы; она перемножает данные массивы и возвращает нам произведение, при этом измеряя затраченное на операцию время с помощью API производительности.

gpuMultiplyMatrix() {
 const gpu = new GPU();
 const multiplyMatrix = gpu.createKernel(function (a: number[][], b: number[][], matrixSize: number) {
  let sum = 0;

  for (let i = 0; i < matrixSize; i++) {
   sum += a[this.thread.y][i] * b[i][this.thread.x];
  }
  return sum;
 }).setOutput([this.matrixSize, this.matrixSize])
 const startTime = performance.now();
 const resultMatrix = multiplyMatrix(this.matrices[0],  this.matrices[1], this.matrixSize);

 const endTime = performance.now();
 this.gpuTime = (endTime – startTime) + " ms";

 console.log("GPU TIME : "+ this.gpuTime);
 this.gpuProduct = resultMatrix as number[][];
}

Шаг 3. Функция умножения на CPU

 

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

 

cpuMutiplyMatrix() {
 const startTime = performance.now();
 const a = this.matrices[0];
 const b = this.matrices[1];
 let productRow = Array.apply(null, new Array(this.matrixSize)).map(Number.prototype.valueOf, 0);
 let product = new Array(this.matrixSize);

 for (let p = 0; p < this.matrixSize; p++) {
   product[p] = productRow.slice();
 }

 for (let i = 0; i < this.matrixSize; i++) {
   for (let j = 0; j < this.matrixSize; j++) {
     for (let k = 0; k < this.matrixSize; k++) {
       product[i][j] += a[i][k] * b[k][j];
     }
   }
 }
 const endTime = performance.now();
 this.cpuTime = (endTime — startTime) + “ ms”;
 console.log(“CPU TIME : “+ this.cpuTime);
 this.cpuProduct = product;
}

Полная версия этого демо-проекта есть в моем GitHub.

CPU vs GPU: сравниваем производительность

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

CPU vs GPU

CPU vs GPU. Время выполнения вычислений

Как отлично видно на скриншоте, для выполнения вычислений на GPU потребовалось 799 мс, тогда как на CPU это время увеличилось почти в 10 раз — до 7511 мс.

 

После этого я провел еще несколько циклов тестирования, каждый раз меняя размер массива.

 

CPU vs GPU
CPU vs GPU. Зависимость времени выполнения вычислений от размера массива

Для начала я попробовал брать небольшие массивы чисел — и заметил, что центральный процессор справлялся с задачей быстрее, чем графический. Например, когда размер массива был уменьшен до 10 элементов, вычисления на CPU были выполнены за 0,14 мс, а на GPU — за 108 мс.

 

Однако по мере увеличения размера массива GPU начал стремительно уходить в отрыв: представленный выше график наглядно демонстрирует его победу над CPU.

Заключение

По своему опыту могу сказать, что использование GPU.js помогает значительно увеличить производительность приложений на JavaScript.

 

В то же время не нужно забывать о том, что GPU стоит поручать выполнение только сложных задач: в противном случае мы будем только попусту тратить ресурсы и, как следствие, замедлять работу нашего приложения. Подтверждение тому — рассмотренный ранее график.

 

Если же вы еще ни разу не пользовались GPU.js — настал ваш час! Не бойтесь испытать эту библиотеку в действии и поэкспериментировать с производительностью!

 

Спасибо за внимание!

 

Оригинал статьи: https://blog.bitsrc.io/using-gpu-to-improve-javascript-performance-e5a41c2e129b.

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

НазадПредыдущий пост ВпередСледующий пост

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: