Код JavaScript, занимающий 50 секунд, перекодируется в Rust и преобразуется в WebAssembly, внедряемый обратно в JavaScript для выполнения за 7 секунд.

Я думал, что никогда не свяжу JavaScript с Assembly. Я подумал, конечно. Поскольку, на мой взгляд, JavaScript — это язык сценариев, в наши дни он работает довольно быстро, но я не использовал его для более серьезных вещей, чем веб-разработка. Недавно, изучая некоторые проблемы с поиском путей, я столкнулся с ситуацией, когда часть кода JavaScript могла выполняться в течение 50 секунд, чтобы найти решение. Построение задачи довольно сложное и ориентировано на алгоритмы, но я поделюсь базовой структурой кода в следующих строках:

const buildMaze = (data) => {
  ...  
  return { maze, startPos, numKeys }
}
const findMazeKeys = (maze, srcPos, keysTaken) => {
  ...  
  return dests
}
const findMazeSteps = (maze, startPos, numKeys) => {
  const minMazeSteps = (srcPos, keysTaken) => { 
    ...
    return memo[memoKey]
  }
  
  let memo = {}, usage = 0, actual = 0
  return minMazeSteps(startPos, [])
}
function World(data) {
  const { maze, startPos, numKeys } = buildMaze(data)
  return findMazeSteps(maze, startPos, numKeys)
}

В приведенном выше коде я опустил все детали, выделив имена функций. Одна из функций minMazeSteps используется рекурсивно внутри замыкания другой функции. Время выполнения в основном тратится на функцию findMazeKeys, которая является обычной функцией, основанной на итерациях.

Используемый язык низкого уровня, Rust

Из-за отсутствия лучших слов для описания моего обоснования я сначала хочу посмотреть, может ли язык более низкого уровня, такой как C++, в этом случае я выбрал Rust так как в последнее время он получил много шума. Должен признаться, что у меня не так много опыта в этом. И мне действительно потребовалось некоторое время, чтобы перевести этот код. Скажем так, я переписал его вместо того, чтобы портировать, так как я действительно не знаю, как портировать JavaScript на Rust.

use std::collections::HashMap;
#[derive(Debug,Hash,PartialEq,Eq,Copy,Clone)]
struct Pos(i8, i8);
type CharMat = Vec<Vec<char>>;
type MemoHash = HashMap<String, usize>;
struct Maze {
  mat: CharMat,
  origin: Pos,
  keys_len: usize,
  memo: MemoHash,
  usage: usize,
  actual: usize
}
impl Maze {
  fn new(strs: &str) -> Maze {
    ...      
    Maze {
      mat,
      origin,
      keys_len,
      memo: HashMap::new(),
      usage: 0,
      actual: 0
    }
  }
  
  fn locate_keys(
    &self, p: &Pos, keys: &Vec<char>
  ) -> Vec<(char, Pos, usize)> {
    ...
    dest
  }
   
  fn min_steps(
    &mut self, p: &Pos, keys: &Vec<char>
  ) -> usize { 
    ...
    steps
  }
  
  fn solve(&mut self) -> usize {
    let o = self.origin.clone();
    self.min_steps(&o, &vec![]);
  }
}

Версия Rust относительно более структурирована в том смысле, что она имеет четко определенные типы, такие как Maze . Все функции являются простыми классоподобными методами, реализованными вокруг структуры Maze. Здесь нет замыкания, поскольку я не могу успешно реализовать его так, как его использует JavaScript. В итоге я сделал вывод, что замыкания в обоих языках просто разные, поэтому отказался от порта.

Если вы до сих пор следили за этой статьей, я расскажу вам о первом открытии. Удивительно, но код Rust занимает 5 секунд вместо 50 секунд для версии JavaScript, поэтому разница примерно в одну величину. Версия на Rust для меня намного сложнее в написании.

Ускорьте язык высокого уровня, JavaScript

Хотя я шокирован результатом, я также очень рад узнать, есть ли какая-то область, в которой я могу улучшить свой JavaScript, чтобы повысить его производительность. На самом деле я очень взволнован, потому что я думал, что нашел золотое яйцо, которое поможет мне улучшить свои навыки JavaScript.

Я пошел в нескольких направлениях: a) Закрытие b) Рекурсия c) Распределение векторов. Хотите верьте, хотите нет, но я несколько раз переписывал версию JavaScript, чтобы исключить замыкание, рекурсию и чрезмерное распределение векторов. Я определенно могу попробовать еще что-то, но чем больше я пробовал, тем больше я обнаруживал, что исходный код, который я написал, находится в довольно хорошей форме с точки зрения производительности. Я мог бы улучшить его с 50 секунд до 40 секунд после вышеупомянутой оптимизации, но это все. Я вообще не могу поместить это в категорию 30 секунд!

Короче говоря, движок NodeJS действительно быстрый. Если величина цикла меньше 100,000 , ваш способ написания кода не имеет большого значения. Оптимизация движка должна вам помочь. Определенно есть заметная разница, когда масштаб операции выходит за пределы 100,000 . Я очень доволен скоростью, хотя я намеревался посмотреть, смогу ли я заставить ее работать быстрее. Это также объясняет, почему я не вижу медленного JavaScript в наши дни, потому что он не такой медленный. Учитывая скорость, с которой я могу сформулировать и закодировать алгоритм, JavaScript определенно не относится к категории медленных. Я убедился в этом после этого упражнения.

Встречайте разрыв между 50-ми и 5-ми годами, WebAssembly

Я немного одержим своим собственным Maze. Чтобы продолжить поиск пробела, я пошел в другом направлении, кроме оправдания того, почему версия Rust работает быстро. Если сборка будет быстрее, то пойду со сборкой. Итак, я взял несколько книг и нашел что-то под названием WebAssembly, которое позволяет вставлять ассемблерный код в JavaScript. И оказывается, что Rust поддерживает цель сборки wasm32. Я следовал онлайн-руководству и скопировал версию Rust. Затем вызовите импортированный метод в файле JavaScript index.js:

import * as wasm from "world-interpretation"
console.log(wasm.run())

Wola, версия JavaScript теперь выполняется за 7 секунд. Хотя это и не 5 секунд, как в случае с чистым Rust, но близко. Более того, импортированная ассемблерная функция оказывается такой же простой, как и базовая функция, за исключением того, что все дочерние функции имеют имена вроде wasm-function[2] (min_steps). В нашем случае эта функция сильно рекурсивна, как вы можете видеть на вкладке производительности, и тратится 1,6 секунды. Самая дорогая функция, wasm-function[48] (locate_keys), теперь занимает 2,3 секунды по сравнению с 16 секундами в исходной версии JavaScript.

Краткое содержание

Код JavaScript, который занимает около 50 секунд, перекодируется в Rust и преобразуется в WebAssembly, внедряемый обратно в JavaScript. Окончательная версия, работающая в браузере, занимает 7 секунд, что дает разницу в одну величину в этом тяжелом упражнении.

Больше контента на plainenglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Получите эксклюзивный доступ к возможностям написания и советам в нашем сообществе Discord.