escargo
escargo

Reputation: 13

How do I represent an i64 in the u64 domain?

What is the rusticle way to to represent the an i64 [-9223372036854775808, 9223372036854775807] into the u64 domain [0, 18446744073709551615]. So for example 0 in i64 is 9223372036854775808 in u64.

Here is what I have done.

    let x: i64 = -10;
    let x_transform = ((x as u64) ^ (1 << 63)) & (1 << 63) | (x as u64 & (u64::MAX >> 1));
    let x_original = ((x_transform as i64) ^ (1 << 63)) & (1 << 63) | (x_transform & (u64::MAX >> 1)) as i64;
    
    println!("x_transform {}", x_transform);
    println!("x_original {} {}", x_original, x_original == x);

yielding

x_transform 9223372036854775798

x_original -10 true

Is there a built in way to do this, because it seems too verbose, and error prone?

Upvotes: 1

Views: 3663

Answers (2)

Masklinn
Masklinn

Reputation: 42472

The simplest solution would be to just translate the two's complement representation, rather than use offset-binary:

let x_transform = u64::from_ne_bytes(x.to_ne_bytes());
let x_original = i64::from_ne_bytes(x_transform.to_ne_bytes());

However according to the to the wiki:

Offset binary may be converted into two's complement by inverting the most significant bit.

So you could do that and use the less error-prone two's complement for the actual translation:

pub fn convert1(x: i64) -> u64 {
        ((x as u64) ^ (1 << 63)) & (1 << 63) | (x as u64 & (u64::MAX >> 1))
}

pub fn convert3(x: i64) -> u64 {
    // manipulating the bytes in transit requires
    // knowing the MSB, use LE as that's the most
    // commmon by far
    let mut bytes = x.to_le_bytes();
    bytes[7] ^= 0x80;
    u64::from_le_bytes(bytes)
}

pub fn convert4(x: i64) -> u64 {
    u64::from_ne_bytes((x ^ i64::MIN).to_ne_bytes())
}

all produce the exact same x86_64 code:

        movabs  rax, -9223372036854775808
        xor     rax, rdi
        ret

Upvotes: 1

cafce25
cafce25

Reputation: 27533

From a performance view it doesn't really matter how you write it, a quick check on godbot shows both the wrapping and the bit shifty versions compile to the same machine code. But I'd argue the variants with wrapping are way more readable and convey the intent better.

pub fn wrap_to_u64(x: i64) -> u64 {
    (x as u64).wrapping_add(u64::MAX/2 + 1)
}
pub fn wrap_to_i64(x: u64) -> i64 {
    x.wrapping_sub(u64::MAX/2 + 1) as i64
}
pub fn to_u64(x: i64) -> u64 {
    ((x as u64) ^ (1 << 63)) & (1 << 63) | (x as u64 & (u64::MAX >> 1))
}
pub fn to_i64(x: u64) -> i64 {
    ((x as i64) ^ (1 << 63)) & (1 << 63) | (x & (u64::MAX >> 1)) as i64
}
example::wrap_to_u64:
        movabs  rax, -9223372036854775808
        xor     rax, rdi
        ret

example::wrap_to_i64:
        movabs  rax, -9223372036854775808
        xor     rax, rdi
        ret

example::to_u64:
        movabs  rax, -9223372036854775808
        xor     rax, rdi
        ret

example::to_i64:
        movabs  rax, -9223372036854775808
        xor     rax, rdi
        ret

The lesson to learn is that unless you have a very specific optimization probably the compiler will outperform you.

Upvotes: 2

Related Questions