Используем GPU для повышения производительности JavaScript

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

Но задумывались ли вы когда-нибудь скомбинировать мощь графического процессора в своих web-приложениях для повышения производительности?

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

***

Что такое GPU.js и зачем нам его использовать?

Источник: https://gpu.rocks/#/

 

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

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

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

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

***

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

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

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

npm install gpu.js --save
--- или ---
yarn add gpu.js

const { GPU } = require('gpu.js')
--- или ---
import { GPU } from 'gpu.js' const gpu = new GPU()

Для браузера:

Скачайте GPU.js локально или используйте CDN.

<script src="dist/gpu-browser.min.js"></script>
--- или ---
<script src="//unpkg.com/gpu.js@latest/dist/gpu- browser.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/gpu.js@latest/dist/gpu-browser.min.js"></script>

<script>
  const gpu = new GPU()
  ...
</script>

Примечание. Если вы используете Linux, вам необходимо убедиться, что у вас установлены правильные файлы, выполнив: sudo apt install mesa-common-dev libxi-dev

Вот всё что вам нужно знать об установке и импорте GPU.js. Теперь вы можете начать использовать GPU в своем приложении.

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

***

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

Bit – это ультра-расширяемый инструмент, который позволяет создавать по-настоящему модульные приложения с независимо созданными, версионированными и поддерживаемыми компонентами.

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

Компоненты Material UI доступны индивидуально на Bit.dev

***

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

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

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

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

Также обязательно определите размер вывода. В приведенном выше примере я использовал параметр, называемый settings, для назначения размера вывода.

const settings = {
    output: [100]
};

Выходные данные функции ядра могут быть одномерные, двухмерные или трёхмерные, что означает, что у нее может быть до 3 потоков. Вы можете получить доступ к этим потокам в ядре с помощью команды 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()

***

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

Числа

В функции GPU.js можно использовать любое целое число или число с плавающей запятой.
const exampleKernel = gpu.createKernel(function () {
  const number1 = 10;
  const number2 = 0.10;
return number1 + number2; }, settings);

 

Булевые

Булевые значения также поддерживаются в GPU.js, как и в JavaScript.
const kernel = gpu.createKernel(function () {
  const bool = true;
if (bool) { return 1; } else { return 0; } },settings);

 

Массивы

Вы можете определять числовые массивы любого размера в функциях kernel и возвращать их.
const exampleKernel = gpu.createKernel(function () {
 const array1 = [0.01, 1, 0.1, 10];
 return array1;
}, settings);

 

Функции

Использование приватных функций внутри функции kernel также возможно в GPU.js.
const exampleKernel = gpu.createKernel(function () {
  function privateFunction() {
    return [0.01, 1, 0.1, 10];
  }
return privateFunction(); }, settings);

***

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

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

Числа

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

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

 

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

Вы можете передавать типы массивов Array, Float32Array, Int16Array, Int8Array, Uint16Array, uInt8Array в kernel GPU.js.

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

exampleKernel([1, 2, 3]);

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

const { input } = require('gpu.js');

const value = input(flattenedArray, [width, height, depth]);

 

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

Передача изображений в функции – это новая вещь, которую мы можем увидеть в GPU.js по сравнению с традиционным JavaScript. С помощью GPU.js вы можете передать одно или несколько изображений HTML в виде массива функции kernel.

// Одно изображение
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. Вы можете найти их в их документации. Поскольку теперь вы понимаете несколько конфигураций, давайте напишем функцию с помощью GPU.js и сравним ее производительность.

***

Первая функция с использованием GPU.js

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

Шаг 1 - Функция для создания числовых массивов из 1000 элементов

Я сгенерирую 2D-массив с 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 – функция ядра

Это самая важная функция в этом приложении, поскольку все вычисления графического процессора происходят внутри него. Здесь функция multiplyMatrix получит в качестве входных данных два числовых массива и размер матрицы. Затем он умножит два массива и вернет общую сумму измеряя время с помощью Performance 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 – функция умножения ЦП.

Это традиционная функция 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 и вычислений на GPU. Поскольку в предыдущем разделе я создал приложение Angular, я использовал его для измерения производительности.

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

Не останавливаясь на этом, я провел те же тесты в течение пары циклов, изменяя размер массива.

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

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

***

Вывод

Основываясь на моем эксперименте с использованием GPU.js, он может повысить производительность приложений JavaScript.

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

Однако, если вы еще не пробовали GPU.js, я приглашаю вас всех попробовать его и поделиться своим опытом.

Спасибо, что прочитали!!!

***

От переводчика

Статься переведена с английского на Medium: Оригинальная статья

Это мой первый пост на этой площадке, так что буду рад вашим советам!

Comments 1

Login to leave a comment