Rust проти JavaScript: досягнення 66% кращої продуктивності за допомогою WebAssembly

pic

Ілюстрація, що порівнює JavaScript та AssemblyScript з Rust і WebAssembly

JavaScript зазвичай працює на одному потоці, який часто називають
"головним потоком". Це означає, що JavaScript виконує одне завдання за раз, синхронно. Головний потік також обробляє завдання рендерингу, такі як малювання та макет, а також взаємодії з користувачем, що означає, що довготривалі завдання JavaScript можуть призвести до того, що браузер стане не чутливим.
Ось чому веб-сторінки можуть "заморожуватися", коли виконується важка функція JavaScript, блокуючи взаємодії з користувачем.

Ми продемонструємо, як заблокувати головний потік, симулюючи важкі обчислення за допомогою алгоритму Фібоначчі, і ми вирішимо проблему заблокованого головного потоку кількома підходами, такими як:

  • багатопотоковість через Web Worker,
  • WebAssembly за допомогою AssemblyScript,
  • WebAssembly за допомогою Rust.

Алгоритм Фібоначчі

Ми будемо використовувати простий і дуже поширений алгоритм Фібоначчі з часовою складністю O(2^n) для всіх наших кейсів у цій статті.

const calculateFibonacci<= 1) return n;  
 return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);  
};

Один потік

Тепер давайте реалізуємо алгоритм Фібоначчі безпосередньо в головному потоці.
Просто викликайте функцію Фібоначчі, коли кнопка натиснута.

"use client";  
import { useState } from "react";  

/**  
 * симулюємо анімацію завантаження  
 */  
function Spinner() {  
 return (  
   <div>Loading...</div>  
 );  
}  

export default function Home() {  
  const [result, setResult] = useState(null);  
  const [isLoading, setIsLoading] = useState(false);  

  const calculateFibonacci = (n: number): number => {  
    if (n <= 1) return n;  
    return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);  
  };  

  const handleCalculate = () => {  
    setIsLoading(true);  
    /**  
     * симулюємо довготривале обчислення  
     */  
    const result = calculateFibonacci(42);  
    setResult(result);  
    setIsLoading(false);  
  };  

  return (  
    <div>  
      <button onClick={handleCalculate}>Calculate Fibonacci</button>  
      {isLoading ? <Spinner /> : <div>Result: {result}</div>}  
    </div>  
  );  
}

Тепер спробуємо натискати кнопку «Calculate Fibonacci», вимірюючи продуктивність.
Щоб виміряти продуктивність нашого коду, ми можемо використати інструменти продуктивності в Chrome DevTools.

Як видно з інтерфейсу, наша кнопка зі спінером навіть не з'являється, і замість цього одразу показується результат обчислення.
Також, використовуючи інструменти продуктивності, ми бачимо, що анімація спіннера блокується важкими обчисленнями алгоритму Фібоначчі на головному потоці приблизно на 2.06 секунди.

pic

pic

Інструменти продуктивності показують, що головний потік заблокований на 2 секунди.

Багатопотоковість (Web Worker)

Звичайний підхід для зменшення навантаження на головний потік — це використання Web Worker.

/**  
 * переміщаємо алгоритм Фібоначчі в web worker  
 */  
self.addEventListener("message", function (e) {  
 const n = e.data;  

 const fibonacci = (n) => {  
   if (n <= 1) return n;  
   return fibonacci(n - 1) + fibonacci(n - 2);  
 };  

 const result = fibonacci(n);  
 self.postMessage(result);  
});
"use client";  
import { useState } from "react";  

function Spinner() {  
 return (  
   <div>Loading...</div>  
 );  
}  

export default function Home() {  
  const [result, setResult] = useState<number | null>(null);  
  const [isLoading, setIsLoading] = useState<boolean>(false);  

  /**  
   * замість того, щоб виконувати функцію Фібоначчі в головному потоці,  
   * ми будемо виконувати її в web worker  
   */  
  const handleCalculate = () => {  
    setIsLoading(true);  

    const worker = new Worker(  
      new URL("./fibonacci-worker.js", import.meta.url),  
    );  

    worker.postMessage(42);  

    worker.onmessage = (e) => {  
      setResult(e.data);  
      setIsLoading(false);  
      worker.terminate();  
    };  
  };  

  return (  
    <div>  
      <button onClick={handleCalculate}>Calculate Fibonacci</button>  
      {isLoading ? <Spinner /> : <div>Result: {result}</div>}  
    </div>  
  );  
}

Тепер, якщо ми спробуємо виміряти продуктивність, анімація спіннера працює плавно.
Це відбувається тому, що ми перемістили важкі обчислення в окремий потік (worker thread), тим самим уникнувши блокування головного потоку.

Як видно, як однопотокові, так і багатопотокові обчислення займають схожий час — близько 2 секунд.
Тепер постає питання: як ми можемо покращити це? Відповідь — за допомогою WebAssembly.

pic

pic

Інструменти продуктивності показують, що важкі обчислення тепер виконуються на worker-потоці.

WebAssembly — AssemblyScript

Як фронтенд-інженер з обмеженим досвідом роботи з іншими мовами, який хоче спробувати WebAssembly, ми зазвичай вибираємо AssemblyScript, оскільки він забезпечує найбільш знайоме середовище для розробника, схоже на TypeScript.

Ось еквівалентний код для алгоритму Фібоначчі, написаний в AssemblyScript.

export function fibonacci(n: i32): i32 {  
  if (n <= 1) return n;  
  return fibonacci(n - 1) + fibonacci(n - 2);  
}

Якщо ми скомпілюємо цей код, він згенерує файл release.wasm.
Ми можемо використати цей файл Wasm у нашій кодовій базі JavaScript.

"use client";  
import { useState } from "react";  

function Spinner() {  
 return (  
   <div>Loading...</div>  
 );  
}  

export default function Home() {  
  const [result, setResult] = useState<number | null>(null);  
  const [isLoading, setIsLoading] = useState<boolean>(false);  

  const handleCalculate = async () => {  
    setIsLoading(true);  

    // Завантажуємо та ініціалізуємо модуль WebAssembly  
    const wasmModule = await fetch("/release.wasm");  
    const buffer = await wasmModule.arrayBuffer();  
    const module = await WebAssembly.instantiate(buffer);  
    const wasm = module.instance.exports;  

    // Викликаємо функцію Фібоначчі з модуля WebAssembly  
    const fibResult = wasm.fibonacci(42);  

    setResult(fibResult);  
    setIsLoading(false);  
  };  

  return (  
    <div>  
      <button onClick={handleCalculate}>Calculate Fibonacci</button>  
      {isLoading ? <Spinner /> : <div>Result: {result}</div>}  
    </div>  
  );  
}

Тепер, якщо ми виміряємо це знову, навіть попри те, що ми все ще на головному потоці, анімація завантаження з'являється і не блокується важкими обчисленнями.
Алгоритм Фібоначчі тепер займає близько 950мс, що на 53% швидше, ніж при використанні лише JavaScript.

pic

pic

Інструменти продуктивності показують, що AssemblyScript працює на 53% швидше за JavaScript.

WebAssembly — Rust

Rust є одним із популярних виборів для WebAssembly, як зазначено в офіційній документації Mozilla.
Спробуємо реалізувати той самий алгоритм Фібоначчі, але написаний на Rust.

use wasm_bindgen::prelude::*;  

// Відкриваємо функцію для JavaScript через WebAssembly  
#[wasm_bindgen]  
pub fn fibonacci(n: u32) -> u32 {  
 match n {  
 0 => 0,  
 1 => 1,  
 _ => fibonacci(n - 1) + fibonacci(n - 2),  
 }  
}
"use client";  
import { useState } from "react";  

function Spinner() {  
 return (  
 \
    \
\    \    );   }      export default function Home() {    const [result, setResult] = (useState < number) | (null > null);    const [isLoading, setIsLoading] = useState < boolean > false;       const handleCalculate = async () => {    setIsLoading(true);       // Завантаження та ініціалізація WebAssembly модуля    const wasmModule = await fetch("/pkg/rust_wasm_fibonacci_bg.wasm"); // Використовуємо фактичний wasm файл    const buffer = await wasmModule.arrayBuffer();       const module = await WebAssembly.instantiate(buffer);    const wasm = module.instance.exports;       // Викликаємо функцію Фібоначчі з модуля WebAssembly    const fibResult = wasm.fibonacci(42); // Припускаємо, що функція експортується як 'fibonacci'       setResult(fibResult);    setIsLoading(false);    };       return (    \
    \    Обчислити Фібоначчі    \    {isLoading ? \ : \
Результат: {result}\}  
 \  
 );  
}

Тепер давайте подивимося на результат використання WebAssembly з Rust.
Ми все ще використовуємо головний потік, але тепер з Wasm. Подібно до AssemblyScript, хоча ми і виконуємо цей Wasm на головному потоці, анімація завантаження все ще відображається і не блокується. Вражаюче те, що ця важка обчислювальна задача тепер займає лише 684мс, що на 66% швидше, ніж при використанні лише JavaScript.

pic

pic

Інструменти продуктивності показують, що Rust на 66% швидше за JavaScript.

Підсумки та висновки

  • Важкі обчислення блокують головний потік і зупиняють усі анімації.
  • Важкі обчислення можна передати на Web Worker (Веб-робітник).
  • Важкі обчислення можна прискорити, переписавши логіку за допомогою WebAssembly.
    Використовуючи алгоритм Фібоначчі як приклад, ми отримали наступні результати:
  • JavaScript: 2с
  • WebAssembly — AssemblyScript: 953мс (53% швидше, ніж JavaScript)
  • WebAssembly — Rust: 684мс (66% швидше, ніж JavaScript)

Джерела

Перекладено з: Rust vs JavaScript: Achieving 66% Faster Performance with WebAssembly

Leave a Reply

Your email address will not be published. Required fields are marked *