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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

image

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

Но думали ли вы об использовании мощи GPU для повышения производительности веб-приложений?

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

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


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

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

  • В основе GPU.js лежит JavaScript, что позволяет использовать синтаксис JavaScript.
  • Библиотека берёт на себя задачу автоматической транспиляции JavaScript на язык шейдеров и их компиляции.
  • Если в устройстве отсутствует GPU, она может «откатиться» к обычному движку JavaScript. То есть вы ничего не потеряете, работая с GPU.js.
  • GPU.js можно использовать и для параллельных вычислений. Кроме того, можно асинхронно выполнять множественные вычисления одновременно и на CPU, и на GPU.

Учитывая всё вышесказанное, я не вижу никаких причин не пользоваться GPU.js. Давайте узнаем, как его освоить.



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


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

Для проектов Node


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'; // Use this for TypeScript
const gpu = new GPU();

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


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

<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>

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

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

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

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


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

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

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

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

const settings = {
    output: [100]
};

Выходные данные функции ядра могут быть 1D, 2D или 3D, то есть можно использовать до трёх потоков. Доступ к этим потокам внутри ядра можно получить с помощью команды this.thread.

  • 1D: [length] — value[this.thread.x]
  • 2D: [width, height] — value[this.thread.y][this.thread.x]
  • 3D: [width, height, depth] — value[this.thread.z][this.thread.y][this.thread.x]

Также созданную функцию можно вызывать как любую функцию JavaScript, по её имени: exampleKernel()

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


Число


Внутри функции GPU.js можно использовать любые integer или float.

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

Boolean


Булевы значения тоже поддерживаются в GPU.js, аналогично JavaScript.

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

Массивы


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

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

Функции


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

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

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


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

Числа


Функциям ядер можно передавать числа integer или float, аналогично объявлению переменных, см. пример ниже.

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

1D-, 2D- или 3D-массивы чисел


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

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

Функции ядер также могут получать сжатые в одномерные (preflattened) 2D- и 3D-массивы. Такой подход сильно ускоряет загрузку, для этого нужно использовать опцию GPU.js input.

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

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


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

//Single Image
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);
};
//Multiple Images
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;
....
//add another 2 images
....
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 -функция ядра


Это самое важное в данном приложении, поскольку все вычисления на 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 против GPU — сравнение производительности


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


CPU и GPU — время выполнения

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

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


CPU и GPU

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

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

Вывод


Из моего эксперимента по использованию GPU.js можно сделать вывод, что он может значительно повышать производительность JavaScript-приложений.

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



На правах рекламы


VDS для проектов и задач любых масштабов — это про наши эпичные серверы! Новейшие технологии и оборудование, качественный сервис. Поспешите заказать!

Подписывайтесь на наш чат в Telegram.

Источник: https://habr.com/ru/company/vdsina/blog/555994/


Интересные статьи

Интересные статьи

Во время последнего локдауна в Великобритании мы с женой играли в GeoGuessr. Эта игра более размеренна, чем те, в которые мы обычно играем, но хорошо подходит для нашей семьи с 11-недельн...
За свою карьеру я успел поработать со множеством языков программирования. Писал flash-игры на ActionScript 3 и Android-игры на Java, сервера на Java, Scala и NodeJS (Java...
Статья о том, как упорядочить найм1. Информируем о вакансии2. Ведём до найма3. Автоматизируем скучное4. Оформляем и выводим на работу5. Отчитываемся по итогам6. Помогаем с адаптацией...
Доброго времени суток, друзья! Представляю Вашему вниманию перевод статьи Kent Dodds «5 JavaScript Features I Couldn't Code Without». Это мой первый перевод, так что буду рад любым заме...
Это короткая, но достаточно полезная статья для продолжающих разработчиков о итераторах в Javascript.