Rust Tutorial: Введение в Rust для JavaScript-разработчиков
Перевод | Автор оригинала: Nick Groenen
Rust - это язык программирования, созданный Mozilla Research в 2010 году. Сегодня он используется всеми крупными компаниями.
И Amazon, и Microsoft одобрили его как лучшую альтернативу C/C++ для своих систем. Но Rust не останавливается на достигнутом. Такие компании, как Figma и Discord, теперь лидируют, также используя Rust в своих клиентских приложениях.
Это руководство по Rust направлено на то, чтобы дать краткий обзор Rust, как использовать его в браузере и когда вам следует подумать о его использовании. Я начну с сравнения Rust с JavaScript, а затем расскажу, как запустить Rust в браузере. Наконец, я представлю быструю оценку производительности моего веб-приложения-симулятора COVID, которое использует Rust и JavaScript.
Rust в двух словах
Rust концептуально сильно отличается от JavaScript. Но есть и общие трэйты, на которые следует обратить внимание. Давайте посмотрим на обе стороны медали.
Сходства
Оба языка имеют современную систему управления пакетами. В JavaScript есть npm, в Rust есть Cargo. Вместо package.json в Rust есть Cargo.toml для управления зависимостями. Чтобы создать новый проект, используйте Cargo init, а для его запуска - cargo run. Не слишком ли чуждо, не так ли?
В Rust есть много интересных функций, которые вы уже знаете по JavaScript, только с немного другим синтаксисом. Возьмите этот общий шаблон JavaScript, чтобы применить замыкание к каждому элементу в массиве:
let staff = [
{name: "George", money: 0},
{name: "Lea", money: 500000},
];
let salary = 1000;
staff.forEach( (employee) => { employee.money += salary; } );
В Rust мы бы написали это так:
let salary = 1000;
staff.iter_mut().for_each(
|employee| { employee.money += salary; }
);
По общему признанию, нужно время, чтобы привыкнуть к этому синтаксису, с вертикальной чертой (|) вместо круглых скобок. Но после того, как я преодолел первоначальную неловкость, мне стало легче читать, чем еще один набор круглых скобок.
Другой пример - деструктуризация объекта в JavaScript:
let point = { x: 5, y: 10 };
let {x,y} = point;
Аналогично в Rust:
let point = Point { x: 5, y: 10 };
let Point { x, y } = point;
Основное отличие состоит в том, что в Rust мы должны указывать тип (Point). В более общем плане Rust должен знать все типы во время компиляции. Но в отличие от большинства других компилируемых языков компилятор по возможности определяет типы самостоятельно.
Чтобы объяснить это немного подробнее, вот код, действующий на C++ и многих других языках. Каждая переменная требует явного объявления типа:
int a = 5;
float b = 0.5;
float c = 1.5 * a;
В JavaScript, как и в Rust, допустим этот код:
let a = 5;
let b = 0.5;
let c = 1.5 * a;
Список общих функций можно продолжать и продолжать:
- Rust имеет синтаксис async + await.
- Массивы могут быть созданы так же просто, как let array = [1,2,3].
- Код организован в модули с явным импортом и экспортом.
- Строковые литералы кодируются в Юникоде, без проблем обрабатывая специальные символы.
Я мог бы продолжить список, но думаю, что теперь моя точка зрения ясна: Rust имеет богатый набор функций, которые также используются в современном JavaScript.
Различия
Rust - это скомпилированный язык, а это означает, что нет среды выполнения, которая выполняет код Rust. Приложение может работать только после того, как компилятор (rustc) совершит свое волшебство. Преимущество этого подхода - обычно лучшая производительность.
К счастью, Cargo позаботится о вызове компилятора за нас. А с помощью webpack мы также сможем скрыть cargo за npm run build. С помощью этого руководства можно сохранить обычный рабочий процесс веб-разработчика после того, как Rust настроен для проекта.
Rust - это строго типизированный язык, что означает, что все типы должны совпадать во время компиляции. Например, нельзя вызвать функцию с параметрами неправильного типа или неправильным количеством параметров. Компилятор перехватит ошибку, прежде чем вы столкнетесь с ней во время выполнения. Очевидное сравнение - TypeScript. Если вам нравится TypeScript, то вам, скорее всего, понравится Rust.
Но не волнуйтесь: если вам не нравится TypeScript, Rust все равно может быть для вас. В последние годы Rust создавался с нуля с учетом всего, что человечество узнало о проектировании языков программирования за последние несколько десятилетий. Результат - освежающе чистый язык.
Сопоставление с образцом в Rust - моя любимая функция. В других языках есть переключатель и регистр, чтобы избежать таких длинных цепочек:
if ( x == 1) {
// ...
} else if ( x == 2 ) {
// ...
}
else if ( x == 3 || x == 4 ) {
// ...
} // ...
Rust использует более элегантное совпадение, которое работает следующим образом:
match x {
1 => { /* Do something if x == 1 */},
2 => { /* Do something if x == 2 */},
3 | 4 => { /* Do something if x == 3 || x == 4 */},
5...10 => { /* Do something if x >= 5 && x <= 10 */},
_ => { /* Catch all other cases */ }
}
Я думаю, что это довольно удобно, и я надеюсь, что разработчики JavaScript также оценят это расширение синтаксиса.
К сожалению, мы также должны поговорить о темной стороне Rust. Говоря прямо, использование строгой системы типов иногда может показаться очень громоздким. Если вы считали системы типов C++ или Java строгими, приготовьтесь к трудному путешествию с Rust.
Лично мне нравится эта часть о Rust. Я полагаюсь на строгость системы типов и, таким образом, могу отключить часть своего мозга - часть, которая сильно мучает каждый раз, когда я пишу JavaScript. Но я понимаю, что новичкам может быть очень неприятно постоянно бороться с компилятором. Кое-что из этого мы увидим позже в этом руководстве по Rust.
Привет, Rust
А теперь давайте познакомимся с Rust, работающим в браузере. Начнем с того, что убедимся, что все необходимые инструменты установлены.
Инструменты
- Установите Cargo + rustc с помощью rustup. Rustup - это рекомендуемый способ установки Rust. Он установит компилятор (rustc) и менеджер пакетов (Cargo) для последней стабильной версии Rust. Он также может управлять бета-версиями и ночными версиями, но в этом примере это не обязательно.
- Проверьте установку, набрав в терминале Cargo --version. Вы должны увидеть что-то вроде Cargo 1.48.0 (65cbdd2dc 2020-10-14).
- Также проверьте Rustup: rustup --version должен выдать rustup 1.23.0 (00924c9ba 2020-11-27).
- Установите wasm-pack. Это необходимо для интеграции компилятора с npm.
- Проверьте установку, набрав wasm-pack --version, что должно дать вам что-то вроде wasm-pack 0.9.1.
- Еще нам нужны Node и npm. У нас есть полная статья, в которой объясняется, как лучше всего установить эти два.
Написание кода на Rust
Теперь, когда все установлено, приступим к созданию проекта. Окончательный код также доступен в этом репозитории GitHub. Мы начинаем с проекта Rust, который можно скомпилировать в пакет npm. Код JavaScript, который импортирует этот пакет, появится позже.
Чтобы создать проект hello-world на Rust, используйте Cargo init --lib hello-world. Это создает новый каталог и генерирует все файлы, необходимые для библиотеки Rust:
├──hello-world
├── Cargo.toml
├── src
├── lib.rs
Код Rust будет помещен в lib.rs. Перед этим необходимо настроить Cargo.toml. Он определяет зависимости и другую информацию о пакете с помощью TOML. Для приветствия в браузере добавьте следующие строки где-нибудь в вашем Cargo.toml (например, в конец файла):
[lib]
crate-type = ["cdylib"]
Это указывает компилятору создать библиотеку в режиме совместимости с C. Очевидно, что в нашем примере мы не используем C. C-совместимый просто означает, что он не зависит от Rust, а это то, что нам нужно, чтобы использовать библиотеку из JavaScript.
Также нам понадобятся две внешние библиотеки. Добавьте их отдельными строками в разделе зависимостей:
[dependencies]
wasm-bindgen = "0.2.68"
web-sys = {version = "0.3.45", features = ["console"]}
Это зависимости от crates.io, репозитория пакетов по умолчанию, который использует Cargo.
wasm-bindgen необходим для создания точки входа, которую мы можем позже вызвать из JavaScript. (Вы можете найти полную документацию здесь.) Значение «0.2.68» указывает версию.
web-sys содержит привязки Rust ко всем веб-API. Это даст нам доступ к консоли браузера. Обратите внимание, что мы должны явно выбрать функцию консоли. Наш окончательный двоичный файл будет содержать только привязки веб-API, выбранные таким образом.
Далее идет фактический код внутри lib.rs. Автоматически созданный модульный тест можно удалить. Просто замените содержимое файла этим кодом:
use wasm_bindgen::prelude::*;
use web_sys::console;
#[wasm_bindgen]
pub fn hello_world() {
console::log_1("Hello world");
}
Операторы использования вверху предназначены для импорта элементов из других модулей. (Это похоже на импорт в JavaScript.)
pub fn hello_world() {...} объявляет функцию. Модификатор pub является сокращением от «public» и действует как экспорт в JavaScript. Аннотация #[wasm_bindgen] специфична для компиляции Rust в WebAssembly (Wasm). Он нам нужен здесь, чтобы компилятор предоставлял функцию-оболочку JavaScript.
В теле функции на консоль выводится «Hello world». console::log_1() в Rust - это оболочка для вызова console.log(). (Подробнее читайте здесь.)
Вы заметили суффикс _1 при вызове функции? Это связано с тем, что JavaScript допускает переменное количество параметров, а Rust - нет. Чтобы обойти это, wasm_bindgen генерирует по одной функции для каждого количества параметров. Да, это может быстро стать уродливым! Но это работает. Полный список функций, которые можно вызывать на консоли из Rust, доступен в документации web-sys.
Теперь у нас все должно быть на месте. Попробуйте скомпилировать его с помощью следующей команды. Это загрузит все зависимости и скомпилирует проект. В первый раз это может занять некоторое время:
cd hello-world
wasm-pack build
Хм! Компилятор Rust недоволен нами:
error[E0308]: mismatched types
--> src\lib.rs:6:20
|
6 | console::log_1("Hello world");
| ^^^^^^^^^^^^^ expected struct `JsValue`, found `str`
|
= note: expected reference `&JsValue`
found reference `&'static str
Если вы видите другую ошибку (ошибка: сбой при связывании с cc: код выхода: 1) и вы работаете в Linux, значит, у вас отсутствуют зависимости кросс-компиляции. sudo apt install gcc-Multilib должен решить эту проблему.
Как я упоминал ранее, компилятор строгий. Когда он ожидает ссылку на JsValue в качестве аргумента функции, он не принимает статическую строку. Чтобы удовлетворить компилятор, необходимо явное преобразование.
console::log_1(&"Hello world".into());
Метод into() преобразует одно значение в другое. Компилятор Rust достаточно умен, чтобы отложить, какие типы участвуют в преобразовании, поскольку сигнатура функции оставляет только одну возможность. В этом случае он будет преобразован в JsValue, который является типом оболочки для значения, управляемого JavaScript. Затем мы также должны добавить &, чтобы передать его по ссылке, а не по значению, иначе компилятор снова пожалуется.
Попробуйте снова запустить wasm-pack build. Если все пойдет хорошо, последняя напечатанная строка должна выглядеть так:
[INFO]: :-) Your wasm pkg is ready to publish at /home/username/intro-to-rust/hello-world/pkg.
Если вам удалось зайти так далеко, теперь вы можете скомпилировать Rust вручную. Затем мы интегрируем это с npm и webpack, которые сделают это за нас автоматически.
Интеграция с JavaScript
В этом примере я решил поместить package.json в каталог hello-world. Мы также могли бы использовать разные каталоги для проекта Rust и проекта JavaScript. Дело вкуса.
Ниже мой файл package.json. Самый простой способ - скопировать его и запустить npm install. Или запустите npm init и скопируйте только зависимости dev:
{
"name": "hello-world",
"version": "1.0.0",
"description": "Hello world app for Rust in the browser.",
"main": "index.js",
"scripts": {
"build": "webpack",
"serve": "webpack serve"
},
"author": "Jakob Meier <inbox@jakobmeier.ch>",
"license": "(MIT OR Apache-2.0)",
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "~1.3.1",
"@webpack-cli/serve": "^1.1.0",
"css-loader": "^5.0.1",
"style-loader": "^2.0.0",
"webpack": "~5.8.0",
"webpack-cli": "~4.2.0",
"webpack-dev-server": "~3.11.0"
}
}
Как видите, мы используем webpack 5. Wasm-pack также работает со старыми версиями webpack или даже без сборщика. Но каждая установка работает по-своему. Я бы посоветовал вам использовать те же самые версии, когда вы будете следовать этому руководству по Rust.
Еще одна важная зависимость - wasm-pack-plugin. Это плагин для веб-пакетов, специально предназначенный для загрузки пакетов Rust, созданных с помощью wasm-pack.
Двигаясь дальше, нам также необходимо создать файл webpack.config.js для настройки webpack. Вот как это должно выглядеть:
const path = require('path');
const webpack = require('webpack');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
},
plugins: [
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, ".")
}),
],
devServer: {
contentBase: "./src",
hot: true,
},
module: {
rules: [{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
}, ]
},
experiments: {
syncWebAssembly: true,
},
};
Все пути настроены так, чтобы код Rust и код JavaScript располагались рядом. Index.js будет в папке src рядом с lib.rs. Не стесняйтесь настраивать их, если вы предпочитаете другую настройку.
Вы также заметите, что мы используем эксперименты с веб-пакетами - новый параметр, представленный в веб-пакете 5. Убедитесь, что для параметра syncWebAssembly установлено значение true.
Наконец, нам нужно создать точку входа JavaScript, src/index.js:
import("../pkg").catch(e => console.error("Failed loading Wasm module:", e)).then(
rust =>
rust.hello_world()
);
Мы должны загружать модуль Rust асинхронно. Вызов rust.hello_world() вызывает сгенерированную функцию-оболочку, которая, в свою очередь, вызывает функцию hello_world Rust, определенную в lib.rs.
Запуск npm run serve должен теперь скомпилировать все и запустить сервер разработки. Мы не определили HTML-файл, поэтому на странице ничего не отображается. Вероятно, вам также придется перейти на http: // localhost: 8080 / index вручную, поскольку http: // localhost: 8080 просто перечисляет файлы без выполнения какого-либо кода.
Получив пустую страницу, откройте консоль разработчика. В журнале должна быть запись с Hello World.
Хорошо, это было довольно сложно для простого привет, мир. Но теперь, когда все готово, мы можем легко расширить код Rust, не беспокоясь об этом. После сохранения изменений в lib.rs вы должны автоматически увидеть перекомпиляцию и оперативное обновление в браузере, как и в случае с JavaScript!
Когда использовать Rust
Rust не является общей заменой JavaScript. Он может работать только в браузере через Wasm, и это немного ограничивает его полезность. Даже несмотря на то, что вы могли бы заменить практически весь код JavaScript на Rust, если бы действительно захотели, это плохая идея и не для чего был создан Wasm. Например, Rust не подходит для взаимодействия с пользовательским интерфейсом вашего веб-сайта.
Я думаю о Rust + Wasm как о дополнительной опции, которую можно использовать для более эффективного выполнения рабочей нагрузки, загружающей процессор. За счет больших размеров загрузки Wasm избегает накладных расходов на синтаксический анализ и компиляцию, с которыми сталкивается код JavaScript. Это, плюс сильная оптимизация компилятора, потенциально приводит к повышению производительности. Обычно поэтому компании выбирают Rust для конкретных проектов. Еще одна причина выбрать Rust - это языковые предпочтения. Но это совершенно другое обсуждение, в которое я не буду вдаваться.
Симулятор заражения коронным разрядом
Пришло время создать настоящее приложение, которое раскрывает всю мощь Rust в браузере! Ниже представлена живая демонстрация CodePen. Если вы предпочитаете смотреть его в полном размере, нажмите здесь. Также доступен репозиторий с кодом.
Сетка внизу содержит ячейки, которые являются объектами моделирования. Красный квадрат представляет клетку, инфицированную вирусом. Нажимая на отдельные ячейки, вы можете добавлять или удалять инфекции.
Кнопка «Начать моделирование» будет имитировать инфекцию между ячейками с небольшой задержкой между днями моделирования. При нажатии кнопки с надписью «Следующий день» будет имитироваться один день.
Правила заражения просты. У здоровой клетки есть определенный шанс заразиться для каждого зараженного соседа в радиусе заражения. После фиксированного количества дней заражения клетка восстанавливается и становится невосприимчивой для остальной части моделирования. Также существует определенная вероятность того, что инфицированная клетка умирает каждый день от заражения до выздоровления.
Форма в правом верхнем углу управляет различными параметрами моделирования. Их можно обновить в любое время и вступить в силу на следующий день. Если вы измените размер сетки, симуляция будет сброшена.
Большая часть того, что вы видите, реализована на JavaScript. Только при нажатии на Next Day или Start Simulation будет вызван код Rust для расчета всех заражений. Я также реализовал эквивалентный интерфейс в JavaScript, чтобы мы могли легко провести сравнение производительности позже. Ползунок, переключающийся между Rust и JS, меняется между двумя реализациями. Даже при запуске автоматизированного моделирования это вступает в силу немедленно (на следующий день).
Интерфейс моделирования
Объект моделирования (реализованный в Rust и JavaScript) предоставляет только один класс с конструктором и два дополнительных метода. Сначала я дам вам обозначение JavaScript:
export class JsSimulation {
constructor() {
/* implementation omitted */
}
reconfigure(
w,
h,
radius,
recovery,
infection_rate,
death_rate,
) {
/* implementation omitted */
}
next_day(input, output) {
/* implementation omitted */
}
}
Конструктор не принимает аргументов. Он просто установит для всех параметров конфигурации значения по умолчанию. Используя reconfigure(), можно установить значения из HTML-формы. Наконец, next_day() принимает входной Uint8Array и выходной Uint8Array.
В Rust класс определяется следующим образом:
#[wasm_bindgen]
pub struct Simulation { /* fields omitted */ }
В Rust нет понятия классов. Есть только struct. Определение структуры Simulation, как указано выше, с аннотацией #[wasm_bindgen] приведет к созданию класса оболочки JavaScript, который можно использовать в качестве прокси.
Чтобы определить методы в структуре, мы используем блок impl в Rust. Таких блоков для структуры может быть много, разбросанных по разным файлам. В этом примере нам нужен только один из них:
#[wasm_bindgen]
impl Simulation {
#[wasm_bindgen(constructor)]
pub fn new() -> Simulation {
/* implementation omitted */
}
pub fn reconfigure(
&mut self,
w: usize,
h: usize,
radius: usize,
recovery: u8,
infection_rate: f32,
death_rate: f32,
) {
/* implementation omitted */
}
pub fn next_day(&mut self, input: &[u8], output: &mut [u8]) {
/* implementation omitted */
}
}
Самое большое отличие от JavaScript состоит в том, что все типы явно упоминаются в сигнатурах функций. Например, w: usize означает, что параметр w имеет тип usize, который представляет собой целое число без знака, размер которого соответствует естественному размеру целевой архитектуры (например, 64-битной). С другой стороны, простое u8 - это 8-битовое целое число без знака на всех платформах.
В функции next_day() два параметра input: &[u8], output: &mut [u8] ожидают каждый фрагмент u8, который по сути является массивом. Заметили ключевое слово mut в типе вывода? Это сообщает компилятору, что мы собираемся изменить содержимое массива, тогда как из входного массива мы будем только читать. (Компилятор остановит нас, если мы попытаемся изменить ввод.)
Еще есть странный параметр &mut self. Это эквивалент этого в JavaScript. Если метод изменяет внутреннее состояние объекта, это должно быть обозначено явным добавлением &mut self в список параметров. Если вы читаете только из поля объекта, но не меняете, то &self тоже подойдет. Если ни к каким полям объекта нет доступа вообще, упоминать себя не нужно.
В качестве возвращаемого типа функции Rust использует обозначение стрелки (->). Итак, fn new() -> Simulation - это функция, возвращающая объект Simulation. И поскольку я добавил аннотацию #[wasm_bindgen (constructor)], эта функция будет вызываться, когда код JavaScript вызывает конструктор в классе-оболочке, как в let sim = new Simulation().
Производительность статуса заражения
Входные и выходные массивы используют однобайтовое целое число без знака для представления статуса заражения каждой ячейки. Нам нужно как-то закодировать всю необходимую информацию о ячейке внутри этого байта.
В JavaScript я определил несколько констант для представления различных возможностей:
const HEALTHY = 0;
const INFECTED_DAY_0 = 1;
const INFECTED_DAY_MAX = 64;
const IMMUNE = 100;
const DEAD = 101;
Для статуса заражения нам нужно подсчитать, сколько дней клетка уже была больна, чтобы применить восстановление через определенное количество дней. Следовательно, константы дают минимальное и максимальное значение. Любое промежуточное значение представляет инфицированную ячейку.
В Rust мы можем сделать то же самое с перечислением вроде этого:
enum InfectionStatus {
Healthy,
Infected(u8),
Immune,
Dead,
}
Значение типа InfectionStatus может принимать любое из указанных выше значений. Rust присваивает значениям уникальные числа, и нам, программистам, не о чем беспокоиться. Кроме того, перечисления Rust более гибкие, чем перечисления в стиле C. Для значения варианта Infected доступно дополнительное связанное число типа u8, представляющее количество дней, в течение которых ячейка была инфицирована.
Реализация на Rust
Давайте посмотрим на реализацию fn next_day() в Rust. Он начинается с двух вложенных циклов for, которые проходят через все ячейки в сетке:
for x in 0..self.w {
for y in 0..self.h {
/* ...more code... */
}
}
В этом примере x изменяется от 0 до self.w (исключая self.w). Две точки между двумя значениями создают диапазон целых чисел, который повторяется в цикле for. Так написаны циклы for в Rust: некоторая переменная x выполняет итерацию по чему-то итерируемому. В этом случае итерация представляет собой диапазон чисел. Точно так же мы могли бы перебирать значения массива (для элемента в срезе {/ * сделать что-нибудь с элементом * /}).
Заглянув внутрь тела цикла, он начинается с поиска во входном массиве:
let current = self.get(input, x, y);
Функция get определена ниже в lib.rs. Он считывает соответствующий u8 из входного массива и преобразует его в InfectionStatus в соответствии с константами, определенными в JavaScript. Таким образом, текущая переменная имеет тип InfectionStatus, и мы можем использовать для нее мощные средства сопоставления с образцом в Rust:
let current = self.get(input, x, y);
let mut next = current;
match current {
Healthy => {
if self.chance_to_catch_covid_today(input, x, y) > self.rng.gen() {
next = Infected(1);
}
}
Infected(days) => {
if self.death_rate > self.rng.gen() {
next = Dead;
} else if days >= self.recovery {
next = Immune;
} else {
next = Infected(days + 1);
}
}
Dead | Immune => { /* NOP */ }
}
self.set(output, x, y, next);
Хорошо, это много кода. Позвольте мне сломать это. Сначала мы устанавливаем рядом то же значение, что и current. (По умолчанию статус выхода должен быть таким же, как статус входа.) Затем соответствующий текущий блок переключает возможности для текущего статуса заражения.
Если ячейка сейчас здорова, мы вычисляем шанс заразиться сегодня в отдельной функции, которая будет перебирать всех соседей в заданном радиусе. (Подробности здесь опущены.) Эта функция возвращает значение от 0,0 до 1,0, которое мы сравниваем со случайным числом в том же диапазоне, созданным на месте. Если шанс заразиться сегодня выше случайного числа, мы устанавливаем рядом с Infected (1), что означает, что это первый день заражения клетки. (Примечание: в Rust мы обычно опускаем скобки вокруг условия if.)
Если клетка уже заражена, мы проверяем две вещи. Во-первых, умрет ли клетка сегодня, опять же на основе случайного числа, сгенерированного на месте. Если ячейке не повезло, для следующей устанавливается значение «Мертвая». 😢 В противном случае мы проверяем, наступила ли дата восстановления, и в этом случае новый статус будет Иммунный. Наконец, если ни одна из двух проверок не дала положительного результата, счетчик дня заражения увеличивается на единицу.
Последний вариант сопоставления с образцом - это то, что клетка уже мертва или имеет иммунитет. В обоих случаях мы ничего не делаем.
После выражения соответствия мы вызываем функцию установки, которая принимает значение InfectionStatus. Он преобразует значение обратно в u8 и записывает его в выходной массив. Я не буду вдаваться в подробности реализации этого здесь, но я рекомендую вам взглянуть на исходный код, если вам интересно.
И это все! Этот цикл выполняется один раз на каждый день моделирования. Больше ничего.
Результаты сравнения
Последний вопрос: стоило ли вообще здесь использовать Rust? Чтобы ответить на этот вопрос, я выбрал пару настроек и сравнил скорость реализации Rust с таковой в JavaScript. Чего бы вы ожидали? Ответ может вас удивить.
При настройках по умолчанию (100 ячеек, радиус заражения = 2) первый день моделирования занимает в среднем 0,11 мс с Rust и 0,72 мс с JavaScript. Так что Rust более чем в шесть раз быстрее! Но по мере того, как я увеличивал размер и продолжал моделирование в течение нескольких дней моделирования, JavaScript внезапно выполнял ту же работу за половину времени, которое требуется Rust.
Ниже приведены графики экспериментов с большим количеством ячеек и измененным радиусом, что увеличивает общую рабочую нагрузку. Я выполнил этот тест на своем настольном ПК.
График показывает, что Rust значительно быстрее JavaScript в первый день моделирования, независимо от настроек. Это когда код JavaScript должен быть проанализирован, скомпилирован и оптимизирован. После этого код JavaScript с каждым днем становится все быстрее, поскольку JIT-компилятор оптимизирует ветки. Версия Rust сохраняет относительно стабильное время выполнения в течение всех дней.
В этом примере JIT-компилятор отлично справляется. Вся рабочая нагрузка - это один огромный цикл, выполняющий одни и те же вычисления снова и снова с незначительно изменяющимися значениями. Оптимизация этого во время выполнения дает лучшие результаты даже, чем оптимизация Rust во время компиляции.
Чтобы немного разобраться, я провел те же тесты на своем Samsung Galaxy S7. Я предполагал, что встроенный в мобильные браузеры JIT-компилятор будет менее агрессивным, что затруднит эффективное выполнение кода JavaScript.
Действительно, результаты на моем телефоне были намного больше в пользу Rust на моем мобильном телефоне! В первый день моделирования с 3000 узлов версия Rust была в 36,9 раза быстрее (1,45 мс против 53,5 мс)! Начиная с четвертого дня, эксперимент с 3000 и 10000 узлов достиг относительно стабильной производительности для JavaScript. Но даже там Rust был быстрее в 2,5–3 раза (около 28 мс против 79 мс для 10 тыс. Узлов).
Хотя приложение COVID Simulation не является репрезентативным примером всех приложений Wasm, оно уже показывает, что выбор между реализациями Wasm и JavaScript зависит от многих факторов. Wasm может быть намного быстрее в определенных обстоятельствах и, как правило, более согласован, чем его аналог на JavaScript. Но без тщательного тестирования трудно предсказать, какой из двух будет лучше работать на устройстве конечного пользователя.
Выполнение необходимых тестов на самом деле может быть довольно простым - при условии, что вы написали быстрый прототип Rust, который хотите сравнить с существующей реализацией JavaScript. Вам нужно измерить, сколько времени требуется для выполнения кода Rust, включая накладные расходы на вызовы из JavaScript. Это можно очень легко измерить, используя только JavaScript:
const t0 = performance.now();
wasm.expensiveCall();
const t1 = performance.now();
console.log(`Rust code executed in ${t1 - t0}ms`);
В этом туториале по Rust я сделал именно это. Версия, развернутая в демоверсии CodePen, которая также была связана ранее, по-прежнему содержит тестовый код. Вы можете просто открыть консоль разработчика, чтобы узнать время, которое вы получаете на своем устройстве.
Заключение и дополнительные ресурсы
В этом руководстве по Rust я показал шаги по интеграции Rust в проект JavaScript. Поддержка Wasm для Rust достигла приличной зрелости, так что ее можно использовать в вашей работе. Шаблон hello world из этого урока должен стать хорошим началом для этого. Кроме того, есть официальное руководство по wasm-bindgen с гораздо более подробной информацией и опциями.
С помощью приложения COVID Simulation я продемонстрировал, как создать законченное приложение с помощью Rust в браузере. Чтобы количественно оценить производительность Rust, я реализовал полное приложение также на JavaScript и провел несколько тестов. Оценка производительности была немного в пользу JavaScript для настольных ПК, но явно в пользу Rust для мобильных устройств. Главный вывод заключается в том, что только сравнительный анализ может точно сказать, какой язык работает быстрее для вашего приложения.
В этом уроке я также немного рассказал о Rust в целом. Rust - сложный язык для изучения, поэтому я намеренно избегал вдаваться в подробности в этом введении. Если вы действительно хотите изучить Rust, я очень рекомендую книгу Rust и руководство по Rust на примерах в качестве ваших основных источников.
Спасибо, что дочитали до конца! Если вам понравился этот учебник по Rust, вам также может понравиться часть содержимого, которое я разместил в своем личном блоге. Будьте моим гостем и посмотрите, как Rust встречается с Интернетом - столкновение парадигм программирования для более критического взгляда на Rust в браузере.
Готовы ли вы запрыгнуть на поезд Rust, пока он все еще набирает скорость?