Are there any negative implications of using `u64` in "wasm32"?

Just a general question that I can't seem to find a straight answer to anywhere. My use case is to use the fst crate in a package targeting wasm. (I think) the fst crate uses u64 arithmetic pervasively, but my use case for fst would likely be okay with just u32 (I have very few destinations).

What I think I know:

According to several different articles I was able to find, such as the answer on SO, below. Using a higher bit number on a lower bit processor will lead to the entire data of that number not fitting into a single register, which (IIUC) means that computation can result in multiple machine instructions.

Does this same performance difference exist on wasm32 while performing arithmetic with u64s? Even if the host is running on a 64bit processor?

I could imagine that the wasm runtime might make a difference, so I'm mostly concerned about how Chrome's WebAssembly runtime might perform.

Thanks!

Don't know too much about wasm, so let someone more knowledgable confirm, but from my understanding, wasm does have 64 bit types, so u64 arithmetic ops should compile to single wasm instructions, which I'd guess a reasonable jit on 64-bit architecture will compile to single machine instructions.

The "32" in wasm32 means that the adress space is 32-bit, and thus usize is 32-bit. So this is a little bit different than 32-bitness of, say, x86, in which both address space and registers are 32-bit.

2 Likes

I'm doing a fair bit of work with WebAssembly too and your question piqued my curiosity :slightly_smiling_face:

To see whether u64 operations generate sub-optimal instructions I wrote this trivial program which increments a u64 variable:

#[no_mangle]
pub extern "C" fn foo() {
    let mut number: u64 = 42;
    
    unsafe {
        let new_number = core::ptr::read_volatile(&number);
        core::ptr::write_volatile(&mut number, new_number + 1);
    }
}

(volatile writes are needed so the optimiser doesn't remove our code)

I then compiled the code to WebAssembly and converted it to a human-readable form.

$ rustc --target=wasm32-unknown-unknown lib.rs --crate-type=cdylib -o test.wasm -O
$ wasm2wat test.wasm
  (func $foo (type 0)
    (local i32)
    global.get 0
    i32.const 16
    i32.sub
    local.tee 0
    i64.const 42
    i64.store offset=8
    local.get 0
    local.get 0
    i64.load offset=8
    i64.const 1
    i64.add
    i64.store offset=8)

You can see it's doing i64 stores and adds so presumably LLVM will just treat a u64 as a i64 when compiling to WebAssembly.

I'm assuming that means your application won't be able to use values above 2^63, but other than that it shouldn't be an issue.

3 Likes

LLVM does not make a distinction between signed and unsigned integer types, so I don't think there's such a problem. Signed and unsigned integers differ only in the instructions that manipulate values: you can ask LLVM to generate a wrapping, an unsigned non-wrapping, a signed non-wrapping add, or ask for an explicit zero extension or sign extension. This means whatever compiler happens to generate LLVM can decide (or even change!) the signedness of an integer for each individual operation it emits.

Now I'm no expert at WebAssembly, but this official-looking document asserts that this sort of sign-agnosticism of LLVM carries over to WebAssembly integer types as well. Furthermore, there seem to be two flavors, signed and unsigned, of some integer instructions, while other instructions such as addition and subtraction behave identically due to 2's complement representation of integers.

So, based on all of this, I would conclude that you absolutely can put numbers exceeding 2^63 in a wasm i64 value.

3 Likes