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

Kate

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

Но думали ли вы об использовании мощи 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. Давайте узнаем, как его освоить.

p7-vhhhujbwlgc_1mrvjnkk8t2a.png


Как настроить 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] * b[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[j] += a[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-приложение, я использовал его для измерения производительности.

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 только для сложных задач. В противном случае мы впустую потратим ресурсы, а в конечном итоге и снизим производительность приложений, что видно из представленного выше графика.


Источник статьи: https://habr.com/ru/company/vdsina/blog/555994/
 
Сверху