Мы, разработчики, всегда стремимся искать возможности повышения производительности приложений. Когда речь идёт о веб-приложениях, то улучшения обычно вносятся только в код.
Но думали ли вы об использовании мощи 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.
Автор: Mikhail