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

image

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

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

В этой статье я расскажу о библиотеке ускорения JavaScript под названием 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. Давайте узнаем, как его освоить.
p7-vhhhujbwlgc_1mrvjnkk8t2a.png


Установка 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.

--- or ---



Примечание: если вы работаете в 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 и сравним её производительность.
Скомбинировав всё вышеописанное, я написал небольшое 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.
Настало время проверить, справедлива ли вся эта шумиха вокруг GPU.js и вычислений на GPU. Так как в предыдущем разделе я создал Angular-приложение, я использовал его для измерения производительности.
fdbzbsrso1n7xg_6c0kpt89jdws.png

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

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

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

45jv102fbsbc3qnbwdymmi4dusm.png

CPU и GPU

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

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


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

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


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


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

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

8p3vz47nluspfyc0axlkx88gdua.png

© Habrahabr.ru