[Перевод] Используем GPU для повышения производительности JavaScript
Мы, разработчики, всегда стремимся искать возможности повышения производительности приложений. Когда речь идёт о веб-приложениях, то улучшения обычно вносятся только в код.
Но думали ли вы об использовании мощи 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. Давайте узнаем, как его освоить.
Установка 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-приложение, я использовал его для измерения производительности.
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.