0

This is the original function that i wanted to translate in rust to compile it in wasm in the idea that it will make it faster ( since it's a hot function in my server )

export const generateRandomGuid = function (): string {
  let guid: string = "0x";
  let guidString: string = uuidv4();
  const bytes = uuidParse(guidString);
  const arrayBytes = new Uint8Array(bytes);
  for (let index = 0; index < arrayBytes.length; index++) {
    if (guid.length === 18) break;
    const byte = arrayBytes[index].toString(16);
    if (arrayBytes[index].toString(16).length === 1) {
      guid += "0" + byte;
    } else {
      guid += byte;
    }
  }
  return guid;
};

I've translated it this way in rust:

use uuid::Uuid;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn generate_random_guid() -> String {
    let my_uuid: Uuid = Uuid::new_v4();
    let array_bytes = my_uuid.as_bytes();
    let mut rand_id: String = String::new();
    rand_id.push_str("0x");
    for byte in array_bytes {
        let formatted_byte: String = format!("{:X}", byte);
        if formatted_byte.len() == 1 {
            let mut formatted_byte_with_additionnal_zero: String = "0".to_string();
            formatted_byte_with_additionnal_zero.push_str(&formatted_byte);
            rand_id.push_str(&formatted_byte_with_additionnal_zero);
        } else {
            rand_id.push_str(&formatted_byte);
        }
        if rand_id.len() == 18 {
            break;
        }
    }
    return rand_id;
}

Compiled in wasm using wasm-pack and with this configuration:

[package]
name = "h1emu-core"
version = "0.1.4"
edition = "2018"
[dependencies]
wasm-bindgen = "0.2.45"
uuid = {version = "0.8.2", features = ["v4","wasm-bindgen"], default-features = false }
getrandom = { version = "0.2.3", features = ["js"] }
[lib]
crate-type = ["cdylib"]
[profile.release]
lto = true
opt-level = 3

The result is not the one wanted, it appear the js version is twice faster than the wasm one. So i'm asking myself if it's just my rust code that is bad or my configuration or just that in my case wasm is not suitable.

Qgruber
  • 137
  • 1
  • 11
  • 3
    You post lacks important details like _exactly how you compiled it_, _exactly how to run the code_ and _how you benchmarked the code_. – Shepmaster Jul 07 '21 at 13:05
  • You are allocating a lot of unnecessary Strings. Also be sure to compile with --release flag. – Ivan C Jul 07 '21 at 13:18
  • 2
    What you are using is **not** a UUID, but only the first half of a version 4 UUID. As this is pretty much just randomness (except 4 bit for the version identifier), it is probably faster to directly generate 8 bytes of randomness with [the rand crate](https://docs.rs/rand/latest/rand/) instead of generating 16 bytes of randomness with the uuid crate. – Elias Holzmann Jul 07 '21 at 13:56

2 Answers2

3

The number of allocations in your code can be greatly decreased from n*2 in the worst case to a constant 1 (ignoring Uuid::new_v4 which I count as a constant factor that's external to the algorithm at hand) by making use of the write! macro and format specifiers:

pub fn generate_random_guid() -> String {
    use std::fmt::Write;
    let my_uuid: Uuid = Uuid::new_v4();
    let array_bytes = my_uuid.as_bytes();
    let mut rand_id = String::with_capacity(18);
    rand_id.push_str("0x");
    for byte in &array_bytes[..8] {
        write!(&mut rand_id, "{:02X}", byte).unwrap();
    }
    rand_id
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Ivan C
  • 1,772
  • 1
  • 8
  • 13
3

While the allocations may account for the bulk of the runtime, all the formatting machinery surely doesn't help as well. This is all guaranteed to be ASCII, so we can operate on the raw numbers, and in the end convert to a String.

I also got rid of the explicit UUID and instead use the rand crate directly, but feel free to do that as you please. Wasm should at least support getrandom.

pub fn generate_random_guid() -> String {
    let random : [u8; 8] = rand::random();

    let mut str_bytes = vec![0u8; 16];

    const ASCII_ZERO: u8 = '0' as u8;
    const ASCII_NINE: u8 = '9' as u8;
    const ASCII_NUMBERS_LETTERS_OFFSET: u8 = 'A' as u8 - '9' as u8 - 1;

    for i in 0..8 {
        let mut leading = random[i] / 16 + ASCII_ZERO;
        let mut trailing = random[i] % 16 + ASCII_ZERO;

        leading += ((leading > ASCII_NINE) as u8) * ASCII_NUMBERS_LETTERS_OFFSET;
        trailing += ((trailing > ASCII_NINE) as u8) * ASCII_NUMBERS_LETTERS_OFFSET;

        str_bytes[2 * i] = leading;
        str_bytes[2 * i + 1] = trailing;
    }

    unsafe { String::from_utf8_unchecked(str_bytes) }
}

A quick look into godbolt reveals that this is compiled into near-optimal asm, I expect similar for wasm.

L. Riemer
  • 644
  • 8
  • 13
  • Also [How do I create a random String by sampling from alphanumeric characters?](https://stackoverflow.com/q/54275459/155423) – Shepmaster Jul 07 '21 at 16:33