7

I wrote the bubble sort algorithm in JavaScript and Wasm using Rust and the JS code is faster than Rust code. How is this possible?

JavaScript code

import * as wasm from "wasm-comparision-sort-algorithms";

function main() {
  const arr = generateRandomArray({size: 50000, min: 1, max: 1000})
  const arr2 = [...arr]
  console.time("Bubble Sort JS")
  bubbleSort(arr)
  console.timeEnd("Bubble Sort JS")
  wasm.handle_bubble_sort(arr2)
}

/**
 * Algorithm to sort an array of numbers using the bubble sort algorithm.
 * @param {Number[]} arr - The array of numbers to sort.
 */
function bubbleSort(arr) {
  const len = arr.length
  for (let i = 0; i < len; i++) {
    for (let j = 0; j < len - i - 1; j++) {
      // Sort numbers
      if (arr[j] > arr[j + 1]) {
        const temp = arr[j]
        arr[j] = arr[j + 1]
        arr[j + 1] = temp
      }
    }
  }
}

/**
 * Generate a random array of numbers between a range of numbers.
 * @param {Number} size - The size of the array to generate.
 * @param {Number} min - The minimum number in the range.
 * @param {Number} max - The maximum number in the range.
 * @returns {Number[]} - The generated array of numbers.
 */
function generateRandomArray({
  size, min, max}) {
  const arr = []
  for (let i = 0; i < size; i++) {
    arr.push(Math.floor(Math.random() * (max - min + 1) + min))
  }
  return arr
}

document.addEventListener("click", main)

Rust code

mod utils;

use wasm_bindgen::prelude::*;

// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn handle_bubble_sort(array: JsValue) {
    let mut elements: Vec<u32> = array.into_serde().unwrap();
    web_sys::console::time_with_label("Bubble Sort WASM");
    bubble_sort(&mut elements);
    web_sys::console::time_end_with_label("Bubble Sort WASM");
    alert(&format!("The sum of all elements is {}", sum));
}

/*
* Create a bubble sort algorithm
*/
pub fn bubble_sort(array: &mut [u32]) {
    let _i = 0;
    let _j = 1;
    let _size_array = array.len();
    for _i in 0.._size_array {
            for _j in 0.._size_array - _i - 1 {
                    unsafe {
                            let t1 = *array.get_unchecked(_j);
                            let t2 = *array.get_unchecked(_j + 1);
                            if  t1 > t2 {
                                    array.swap_unchecked(_j, _j+1);
                            }
                    }
            }
    }
}

Cargo.toml

[package]
name = "wasm-comparision-sort-algorithms"
version = "0.1.0"
authors = ["a simple mortal"]
edition = "2018"

[lib]
crate-type = ["cdylib", "rlib"]

[features]
default = ["console_error_panic_hook"]

[dependencies]
wasm-bindgen = { version = "0.2.63", features = ["serde-serialize"] }
serde = { version = "1.0", features = ["derive"] }
web-sys = { version = "0.3.55", features = ["console"] }

# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.6", optional = true }

# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. It is slower than the default
# allocator, however.
#
# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now.
wee_alloc = { version = "0.4.5", optional = true }

[dev-dependencies]
wasm-bindgen-test = "0.3.13"

[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level=3 # Optimal for time execution

The average times:

  • JavaScript: 5115 ms
  • Rust: 9499 ms

Version of environment:

  • I'm using wasm-pack version 0.10.1
  • Node version used from localhost server is: 16.13.0
  • Cargo and Rustc version: 1.57.0
  • I'm testing on Brave browser version: 1.31.88

I use wasm-pack build --release to build the project.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Eladiorocha
  • 146
  • 1
  • 6
  • Yes I thinks is similar but I build the binary with wasm-pack build wasm --release and I get the same result :( – Eladiorocha Dec 21 '21 at 23:58
  • 4
    Looking at the generated asembly code for the non-WASM case in release, I see that the compiler fails to remove some bounds tests, which then would call panic. Replacing the accesses and swaps with `get_unchecked` and `swap_unchecked` and passing in a `&mut [u32]` rather than a `&mut Vec` produces different assembly - which does not contain any panic tests. It is longer, but might be faster? (see https://godbolt.org/z/sEEsqsafe) – Michael Anderson Dec 22 '21 at 01:24
  • @MichaelAnderson I also noticed that, but to verify this is the reason you need to compare the code V8 generates and whether it can remove then in JS (and also, doesn't remove them in WASM). I don't feel like doing that, but if someone wants to try, use `--print-opt-code --print-opt-code-filter="bubbleSort"` (pass as-is for nodejs, for chrome wrap with `--js-flags="..."` – Chayim Friedman Dec 22 '21 at 06:26
  • 1
    @ChayimFriedman it is probably enough just to test if removing the checks, like I have done in the playground code, makes it faster in the WASM runner that the OP is using. I don't have access to a platform where I can do that right now - which is why I added it as a comment and not an answer - I'm not _sure_ its the issue. – Michael Anderson Dec 22 '21 at 06:29
  • I've just tested with: `let ptr = array.as_mut_ptr(); let a = ptr.add(j); let b = ptr.add(j + 1); if *a > *b { std::ptr::swap(a, b); }` and `--release`: JS=5621ms, WASM=7649ms. Changing the slice to `&mut [u16]` yields 7451ms. So the question stands. – rodrigo Dec 22 '21 at 09:40
  • 1
    I have a feeling that the conversion happening through `array.into_serde()` adds some considerable overhead because the array needs to be serialized and deserialized via JSON, allocating memory and using CPU cycles whereas the JS version doesn't need that. – Marcus Ilgner Dec 22 '21 at 12:26
  • @milgner: But the `into_serde()` is not accounted for in the timing; and in my experiment I didn't even use it, I created the array directly in Rust code. – rodrigo Dec 22 '21 at 13:24
  • 7
    It's hard to answer your question because it doesn't include a [MRE]. You need to include how you are building your code. This includes exact command line invocations, versions of tools used (Rust, Node or browser, Wasm tooling). – Shepmaster Dec 22 '21 at 16:23
  • 4
    You also need to provide all of the relevant code. It appears that `generateRandomArray` is missing (among other things?) – Shepmaster Dec 22 '21 at 16:36
  • 1
    It is not impossible that the JS JIT compiler recognises the bubble sort as a sort and uses a different algorithm. – Andrew Morton Dec 22 '21 at 19:09
  • 4
    You need to provide **all** of the relevant code. (*file not found for module `utils`*; *cannot find value `sum` in this scope*; *use of unstable library feature 'slice_swap_unchecked'*). **Please** take only the contents of your question and ensure you can re-create the problem in a completely new directory. That will help you ensure that you have created a [MRE]. – Shepmaster Dec 22 '21 at 19:17
  • 2
    *I use [...] to build the project* — and then what? How do you get it from there into your browser? You mention a "localhost server", but I don't see any code for that, or even the HTML of the page that would be loaded in the browser. – Shepmaster Dec 22 '21 at 19:23
  • @AndrewMorton I strongly doubt that: this is not something compilers will do in general - it's expensive and for nothing; the standard library already includes a sort and if the programmer wrote their own then they're a) exercising, or b) want a different sort algorithm. – Chayim Friedman Dec 22 '21 at 20:40

0 Answers0